大部分開發人員都熟悉SQL,不管用什麼語言開發系統,只要用到了關係型數據庫,都會涉及到SQL的使用。算法
在某些系統中,主要的程序邏輯都體現一個個存儲過程裏,例如數據中心產品,這時候,你們都認爲該產品主要的開發語言是SQL,因而咱們把SQL看成程序自己來看待。可是在更多的業務系統中,咱們一般只須要進行普通的增刪改查,SQL更多隻是插入在Java或者XML文件裏的一些查詢語句,這個時候,開發人員只把SQL看成查詢分析的工具,而不是程序來看待。數據庫
接下來爲你們講述一個工做中發生的關於SQL優化的真實故事。編程
這是一個用戶行爲分析的系統,其中有三張表(簡化字段後),見下圖。數據結構
Java架構—SQL優化實踐丨查詢速度提高300倍! 在daily_access表中,記錄了當天的用戶訪問狀況,一條記錄就是一次訪問請求;架構
在ip_range表中,存着IP地址的分段,從ip_start到ip_end之間的IP地址,屬於同一個地區;數據結構和算法
area表則記錄着area_id和所對應的地區,因爲同一個地區可能有不少個IP段,因此area表會有重複數據。工具
3個表的數據量狀況:daily_access表的數據量約10萬,area表和ip_range表約50萬。這裏的先決條件:ip_range表和area表是一對一關係,而且ip_start和ip_end必然互斥,不存在重疊區間。性能
如今的需求是,從三張表中統計出來自每一個地區的訪問者人數。大數據
若是按照「查詢」的思惟來看,這個實現很是簡單,不考慮未命中的話,daily_access表的ip_access字段必然落在ip_range的某個ip_start和ip_end之間,進行三個表連表查詢便可,查詢語句以下:優化
select COUNT (*), a.addr
from daily_access d, ip_range r, area a
where 1 =1
and d.ip_access between r.ip_start and r.ip_end
and r.area_id = a.area_id
group by a.addr;
這個SQL當然是正確的,它曾經在系統中使用過一段時間,可是效果欠佳,由於在前述數據量下, SQL一次的執行時間大約是15分鐘。
或許你會以爲,對於一個後臺分析系統來講,查詢結果並不須要實時查看,輸出到報表或者存入結果表備查均可以——確實如此——可是10萬的訪問量實際上是一個很是小的數字,若是訪問量有百萬,千萬呢,那麼消耗的時間會成指數上升,甚至執行一夜也出不了報表。
所以,查詢語句進行了必定的優化:數據量少的表先過濾,再去關聯數據量多的表:
select COUNT (*), a.addr
from ( select t1.ip_access , t2.addr
from (select d.ip_access ,
( select r.area_id
from ip_range r
where d.ip_access between r.ip_start and r.ip_end) as area_id ,
from daily_access d) t1,
area t2
where t1.area_id = t2.area_id ) d,
area a
where d.area_id = a.area_id
group by a.addr;
通過優化以後,因爲首先處理了數據較多的表,篩選出較少的結果後再和另外一個表關聯,因此速度有所提高,執行一次大約是6分鐘左右。雖然第二條方案比第一條效率提升了一倍以上,可是很顯然,不論是哪一條,性能都很難被接受。
接下來,咱們來看看實際生產系統中使用的查詢語句是怎樣的(一樣簡化了字段,以便看更清晰):
with vstat_details as ( select /*+ all_rows materialize */ distinct ip_access from daily_access ),
vstat_ip_range as (
select /*+ all_rows materialize */
v2.ip_start n_ip, v2.area_id
from (select v1.dataset, v1.ip_start,
last_value(v1.range_start ignore nulls ) over (order by v1.vc_ip_start,v1.dataset) range_start,
last_value(v1.range_end ignore nulls ) over (order by v1.vc_ip_start,v1.dataset) range_end,
last_value(v1.area_id ignore nulls ) over (order by v1.vc_ip_start,v1.dataset) area_id
from (select 1 dataset,
t1.ip_start,
t1.ip_start range_start,
t1.ip_end range_end,
t1.area_id
from ip_range t1
union all
select /*+ leading(d) use_hash(r) no_merge(d) full(r) */
2 dataset,
t2.n_ip ip_start,
null range_start,
null range_end,
null area_id
from daily_access t2) v1) v2
where v2.ip_start >= v2.range_start
and v2.ip_start <= v2.range_end
and v2.dataset = 2)
select /*+ all_rows leading(v,d) use_hash(d,a) no_merge(v) */
count (*) as n_pageviews,
a.addr
from vstat_ip_range v,
daily_access d,
area a
where v.n_ip = d.ip_access
and v.area_id = a.area_id
group by a.addr;
爲何一個簡單的查詢語句有那麼長呢?
前面兩段查詢語句,開發人員在編寫的時候,潛意識裏把SQL看成一種查詢和分析數據的手段和工具,而不是編程,而這段SQL,不只僅從「查」這個視角來看問題,更是利用數據結構和算法來解決問題。這種出發點的不一樣,致使了編程思路的不一樣。
接下來,咱們來把上面這段SQL拆解開研究一下它的解題思路。
首先,從最內層入手,內層的子查詢,對ip_range表的數據進行了預處理,添加了一個標記「1」:
select 1 dataset,
t1.ip_start,
t1.ip_start range_start,
t1.ip_end range_end,
t1.area_id
from ip_range t1
假設ip_range的數據以下(爲了方便,咱們把IP簡化爲簡單整數表示):
id area_id ip_start ip_end
1 1 15 20
2 2 22 25
3 3 30 35
4 4 36 40
那麼標記完成後的數據結構將是以下
標記 area_id ip_start ip_end start2
1 1 15 20 15
1 2 22 25 22
1 3 30 35 30
1 4 36 40 36
再接下來,要將訪問記錄表daily_access,也按照來訪IP記錄,整理成相同格式,而且添加標記「2」:
select 2 dataset,
t2.n_ip ip_start,
null range_start,
null range_end,
null area_id
from daily_access t2
咱們假設有如下4條訪問記錄,那麼整理後的臨時數據結構以下:
標記 area_id ip_start ip_end start2
2 null 16 null null
2 null 22 null null
2 null 24 null null
2 null 39 null null
若是把兩個表合併(union all),而且按照ip_start和標記字段進行排序,就能獲得下面這個數據結構:
標記 area_id ip_start ip_end start2
1 1 15 20 15
2 null 16 null null
1 2 22 25 22
2 null 22 null null
2 null 24 null null
1 3 30 35 30
1 4 36 40 36
2 null 39 null null
其實咱們要取的內容,就是標記爲2的ip所對應的area_id,但此時還看不出來,因此接下來最關鍵的一步是,將全部的「null」用數據填滿,填充的規則是,用它上面一條相鄰的標記爲1的數據的對應字段的值來填充,因而獲得下圖:
標記 area_id ip_start ip_end start2
1 1 15 20 15
2 1 16 20 15
1 2 22 25 22
2 2 22 25 22
2 2 24 25 22
1 3 30 35 30
1 4 36 40 36
2 4 39 40 36
從上面這個臨時表中剔除標記爲「1」的數據後,就獲得了咱們須要的數據:
標記 area_id ip_start ip_end start2
2 1 16 20 15
2 2 22 25 22
2 2 24 25 22
2 4 39 40 36
從中能夠看到,須要統計的area_id已經一目瞭然,任何ip_start的值落在同一條數據中ip_end和start2之間的數據,其area_id都是咱們要取得數據。整個過程沒有作任何大數據量的連表查詢,效率很是高。
將上述過程預構形成一個臨時表,就是前述查詢語句上半段所作的事:
with vstat_details as ( select /*+ all_rows materialize */ distinct ip_access from daily_access ),
vstat_ip_range as (
select /*+ all_rows materialize */
v2.ip_start n_ip, v2.area_id
from (select v1.dataset, v1.ip_start,
last_value(v1.range_start ignore nulls ) over (order by v1.vc_ip_start,v1.dataset) range_start,
last_value(v1.range_end ignore nulls ) over (order by v1.vc_ip_start,v1.dataset) range_end,
last_value(v1.area_id ignore nulls ) over (order by v1.vc_ip_start,v1.dataset) area_id
from (select 1 dataset,
t1.ip_start,
t1.ip_start range_start,
t1.ip_end range_end,
t1.area_id
from ip_range t1
union all
select /*+ leading(d) use_hash(r) no_merge(d) full(r) */
2 dataset,
t2.n_ip ip_start,
null range_start,
null range_end,
null area_id
from daily_access t2) v1) v2
where v2.ip_start >= v2.range_start
and v2.ip_start <= v2.range_end
and v2.dataset = 2)
而最後,只須要用這個臨時表進行簡單關聯查詢:
select /*+ all_rows leading(v,d) use_hash(d,a) no_merge(v) */
count (*) as n_pageviews,
a.addr
from vstat_ip_range v,
daily_access d,
area a
where v.n_ip = d.ip_access
and v.area_id = a.area_id
group by a.addr;
因爲沒有between 比較,數據量也被預先篩選處理,整個查詢過程很是的快速,前述數據量下,查詢大約耗時3秒,比最初的查詢語句性能要高出300倍。
實際上,目前用戶行爲分析系統已經用大數據平臺進行了重製,IP地址比較也能夠用非關係型數據庫來得到更高的性能,但這段舊系統中的查詢語句,能帶給咱們的啓發,仍然很是有意義,它用事實讓咱們從新認識到這樣一個道理:SQL也是程序。
記住這一點,能幫助在咱們從此的程序開發中,寫出更符合「程序」思惟的SQL語句,而非僅僅是從天然語義出發的「查詢」。
天天都會分享乾貨,記得點個關注哦!!!