Java架構—SQL優化實踐丨查詢速度提高300倍!

大部分開發人員都熟悉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語句,而非僅僅是從天然語義出發的「查詢」。

天天都會分享乾貨,記得點個關注哦!!!

相關文章
相關標籤/搜索