1. 程式人生 > 其它 >那些年我們一起優化的SQL https://mp.weixin.qq.com/s/sPO-6ULwIfUexLY3V4acBg

那些年我們一起優化的SQL https://mp.weixin.qq.com/s/sPO-6ULwIfUexLY3V4acBg

那些年我們一起優化的SQL https://mp.weixin.qq.com/s/sPO-6ULwIfUexLY3V4acBg

那些年我們一起優化的SQL

作者:麥旋風

一、背景

隨著業務不斷迭代,系統中出現了較多的SQL慢查。慢查雖不致命,但會讓商家感知到系統較慢,影響使用體驗。在進行慢查優化過程中,我們積累了一些經驗。本文將基於我們的實戰經歷,講解工作中比較常見的慢查原因,以及如何去優化。

本文講解基於MySQL 5.7。

二、慢查優化

本節主要針對常見的慢查進行分類,講解怎麼去優化。

2.1建立索引的正確姿勢

資料量較大的時候,如果沒有索引,那麼資料庫只能全表一行一行的遍歷判斷資料,因此優化SQL的時候,第一步要做的就是確定有沒有合適的可用的索引。在業務本身比較複雜的情況下,一個表會涉及各種各樣的查詢方式,因此我們需要建立各種各樣的索引去提高查詢。然而索引數量過多又會影響增刪改的效率,並且也會佔用更多額外的空間去儲存索引,因此我們要懂得怎麼去正確的建立索引,避免索引濫用。

2.1.1 索引不要包含選擇性過低欄位

選擇性過低,即通過該欄位只能過濾掉少部分的資料,是沒必要建立索引的,因為如果該資料只是佔小部分,即使沒有索引直接查詢資料表也不用過多的遍歷即可找到目標資料,沒有必要基於索引查詢。

SQL:

select * from my_table where col_a=1 and col_b=1

索引:
index (col_a,col_b)

col_b為邏輯刪除欄位,包含0未刪除和1已刪除,資料庫中的值只有很少量部分是邏輯刪除的。但是在業務中我們一般都只查未刪除的,那麼這種情況col_b是完全不必要在索引中的,可以把col_b從組合索引中去掉。

2.1.2 選擇性高的欄位前置或者單獨建立索引

SQL:

select * from my_table where col_a=1 and col_b=1 and col_c=1

索引:
index(col_a,col_b,col_c)

假設選擇性col_c>col_b>col_a,拋開業務本身需要,組合索引建立的順序儘可能建為index(col_c,col_b,col_a)。

原因是組合索引底層的儲存先按照第一個進行排序,第一個欄位相同再按照第二欄位排序,如果選擇性低的欄位放在前面,因此選擇性高的欄位放前面相對而言IO的次數可能會減少一些。比如基於第一個欄位過濾完會有10000條資料,基於第二個欄位過濾完只有100條資料,如果先查第一個欄位則需要在10000條資料的基礎上再進行過濾查詢,而基於第二欄位過濾完只需要基於100條資料進行過濾查詢即可。

而如果col_c選擇性特別高,經過col_c過濾後只剩下極少的資料甚至一條資料,單獨建立col_c索引就可以。

需要注意的是這個規則特別提到了拋開業務本身需要,比如如果col_a選擇性比col_b高一點相差不大,但是col_b在業務場景中通用性更強,那麼col_b放在前面更合適,可以減少建立的索引。

2.1.3 儘量使用覆蓋索引

SQL:

SELECT sum(col_c) FROM my_table where col_a=1 and col_b=1

索引:
index(col_a,col_b)

如果col_a和col_b過濾完後還有大量資料,那麼建議建一個index(col_a,col_b,col_c)索引,否則MySQL需要通過大量回表去查詢col_c的資料再去求和。

2.1.4 小結

1、選擇性低的欄位不用建立索引。

2、具有唯一性或者高選擇性的欄位無需與其他欄位建立組合索引。

3、除了業務需求上的考慮,儘量將選擇性高的索引欄位前置。

4、在經過索引過濾後資料量依舊很大的情況下可以考慮通過覆蓋索引優化。

2.2 使用索引的正確姿勢

除了SQL本身沒有適用的索引,有了相關的索引但是對應的索引沒有生效是比較常見的情況,以下列舉一些常見的失效場景,在日常的開發中,我們要儘量避免。


需要注意的是,索引失效這裡指的是沒有利用到索引的二分查詢進行資料過濾。因為存在ICP,所以會存在觸發了失效場景執行計劃還是顯示使用了索引的情況。

2.2.1 最左匹配截斷

SQL:

select * from my_table where col_b=1  select * from my_table order by col_b




索引:
index(col_a,col_b)

組合索引的匹配規則是從左往右匹配,無論是作為過濾條件還是排序條件都要遵循這個原則。如果要使用col_b欄位走索引,查詢條件則必須要攜帶col_a欄位。

補充說明:
1、col_b作為排序欄位如果要走索引,只要保證組合索引中col_b前面的欄位都可以包含在過濾條件或者排序條件中即可,也不需要保證col_b作為組合索引中的最後一個欄位。

比如:

select * from my_table order by col_a,col_b

col_a和col_b都可以走索引。

2、如果col_b是作為過濾條件,則col_b前面的欄位都應該在過濾條件中。
比如:

select * from my_table where col_b=1 order by col_a

col_a和col_b都走不了索引,因為col_a在組合索引左邊,但是col_a不在查詢條件中。

2.2.2 隱式轉換

欄位型別:
col_a(VARCHAR)
col_b(DATETIME)

索引:
index1(col_a)
index2(col_b)

SQL:

select * from my_table where col_a=1  select * from my_table where col_b=1603296000000

失效原因
欄位型別和查詢資料的值型別不一致,會導致欄位上的索引失效。

  • col_a是字元型別,使用了數字型別進行查詢。

  • col_b是datetime型別,針對datetime/date/time型別,MySQL增刪查改都要基於字串形式日期去處理,否則MySQL就需要額外進行轉換。(雖然底層儲存的是數字型別,但是並不是儲存時間戳,底層是處理是統一將外部傳入的字串進行轉換,比如是date型別通過將 “2021-12-01” 字串轉數字 20211201 這種形式去儲存)。

2.2.3 in+ order by 導致排序失效

索引:
index(col_a,col_b)

SQL:

select * from my_table where col_a in (1,2) order by col_b

解決方式:

  • 如果col_a的過濾性不高,在組合索引中可以通過將col_b欄位前置,將col_a移動到組合索引後面,只用於避免或減少回表。

  • 如果col_a的過濾性高,過濾後的資料相對較少,則維持當前的索引即可,剩餘不多的資料通過filesort進行排序。

  • 如果存在大量資料,並且經過col_b過濾後還是存在大量資料,建議基於別的資料儲存實現,比如Elasticsearch。

另外SQL建議調整為只查詢id(或者其他已經在索引中的欄位),再根據id去查對應的資料。可以促使SQL走覆蓋索引進一步優化、也可以促使MySQL底層在進行filesort使用更優的排序演算法。

2.2.4 範圍查詢阻斷組合索引

索引:
index(col_a,col_b)

SQL:

select * from table where col_a >'2021-12-01' and col_b=10



解決方式:
可以調整下索引順序,col_a放在最後面。index(col_b,col_a)

2.2.5 字尾匹配不能走索引

索引:
index(col_a,col_b)

SQL:

select * from table where col_a=1 and col_b like '%name%'


以上SQL會導致索引失效。字首匹配比如name%是可以走索引的,但是字尾匹配比如%name會導致沒辦法基於索引樹進行二分查詢。如果需要進行字尾匹配,資料量較大建議基於Elasticsearch實現。

2.2.6 or查詢導致失效

索引:
index(col_a,col_b)

SQL:

select * from table where col_a=1 or col_b=''

or查詢會導致索引失效,可以將col_a和col_b分別建立索引,利用Mysql的index merge(索引合併)進行優化。本質上是分別兩個欄位分別走各自索引查出對應的資料,再將資料進行合併。

2.2.7 使用函式查詢或運算

索引:
index(col_a,col_b)

SQL:

select * from table where col_a=1 and DATE_SUB(CURDATE(), INTERVAL 7 DAY) <= date(col_b);  select * from table where col_a=1 and col_b+1=10

2.2.8 不等於、不包含(只用到ICP)

索引:
index(col_a,col_b,col_c)

SQL:

select * from table where col_a=1 and col_b not in (1,2)  select * from table where col_a=1 and col_b != 1

2.2.9 選擇性過低,直接走全表

選擇性過低會導致索引失效。由於通過二級索引查詢後還有回表查詢的開銷,如果通過該欄位只能過濾少量的資料,整體上還不如直接查詢資料表的效能,則MySQL會放棄這個索引,直接使用全表掃描。底層會根據表大小、IO塊大小、行數等資訊進行評估決定。

索引:

index(col_a)

SQL:

select * from table where col_a>'2017-10-22'

2.2.10 asc和desc混用

索引:
index(col_a,col_b,col_c)

SQL:

select * from my_table where col_a=1 order by col_b desc,col_c asc

desc 和asc混用時會導致索引失效,不建議混用。

2.2.11 小結

根據以上例子,總結幾個索引失效的場景:

  1. 組合索引左匹配原則

  2. 發生隱式轉換

  3. 組合索引,in + order by in會阻斷排序用索引

  4. 範圍查詢會阻斷組合索引,索引涉及到範圍查詢的索引欄位要放在組合索引的最後面。

  5. 前模糊匹配導致索引失效

  6. or查詢,查詢條件部分有索引,部分無索引,導致索引失效。

  7. 查詢條件使用了函式運算、四則運算等。

  8. 使用了!=、not in

  9. 選擇性過低

  10. asc和desc混用

2.3 編寫SQL的正確姿勢

懂得怎麼建立索引,也懂得了怎麼避免索引失效,但是有些場景即使索引走對了,也會慢查,這時我們要考慮優化我們SQL寫法。

2.3.1 深分頁

索引:

index(col_c)

SQL:

select * from my_table where col_c=1 limit 1000,10



為什麼深分頁會效率變差,首先我們要了解一下分頁的原理。


MySQL limit不會傳遞到引擎層,只是在服務層進行資料過濾。查詢資料時,先由引擎層通過索引過濾出一批資料(索引過濾),然後服務層進行二次過濾(非索引過濾)。


引擎層過濾後會將獲取的資料暫存,服務層一條一條資料獲取,獲取時引擎層回表獲得完成資料交給服務層,服務層判斷是否匹配查詢條件(非索引過濾),如果匹配會繼續判斷是否滿足limit限制的資料範圍,符合並且範圍內的資料都查完了才返回。

所以如果深分頁,會導致大量的無效回表(前1000條進行了回表,實際上只需要1000-1010的資料),因此優化的方式就是避免深分頁帶來的額外回表。

解決方式:

# 避免深分頁select * from my_table where id>上次查詢的資料id值 limit 100
# 延遲關聯 避免大量回表SELECT * FROM my_table t1,(select id from my_table where col_c=1 limit 1000,100) t2 where t1.id=t2.id
避免深分頁:我們可以改成id過濾,每次都只查詢大於上次查詢的資料id。這樣每次只查詢100條,回表也只需要回表100條。

覆蓋索引:如果業務需要的欄位比較少,可以通過保證SQL查詢的欄位和查詢條件都在索引上,避免回表。

延遲關聯:通過延遲關聯,通過編寫完全基於索引查詢資料的SQL,再根據id查詢詳細的欄位資料。

2.3.2 order by id

索引:
index(col_a)

SQL:

select * from table where col_a=1 and col_b=2 order by id

MySQL INNODB二級索引最終葉子結點引用的都是主鍵id,因此我們可以利用這個點去使用id排序。

但是在本場景中,col_b截斷了索引,導致SQL沒法利用id進行索引排序。而主鍵索引的權重會比較高,可能會導致MySQL沒有正確選擇索引,從而選擇了可能效能更差的主鍵索引去做排序,查詢條件通過遍歷掃描資料。


因此在不能保證id排序可以走索引的情況下,建議改用其他欄位排序。如果查詢結果集確定會比較少排序欄位可以不在索引上,如果結果集較大還是要把排序欄位加到索引中。

三、慢查分析

在掌握了SQL優化的理論知識之後,我們怎麼驗證編寫的SQL是否有按照預期使用了比較合適的索引?這就需要學會分析SQL的執行情況。

執行計劃:我們可以通過explain關鍵字檢視SQL的執行計劃,通過執行計劃分析SQL的執行情況。

執行計劃欄位描述:

extra欄位常見值:

using index condition補充說明:
using index condition表示使用了ICP(Index Condition Pushdown索引下推),ICP是MySQL 5.6之後新增的特性,部分欄位因為某些情況無法走索引查詢,則會展示using where(在server層進行資料過濾),ICP是在儲存引擎層進行資料過濾,而不是在服務層過濾,利用索引現有的資料過濾調一部分資料。

using where 和 using index condition的區別:

為什麼需要ICP機制:

索引:

index(col_a,col_b)

SQL:

select * from my_table where col_a="value" and col_b like "%value%"

如果沒有using index condtion,col_a會走索引查詢,匹配到對應的資料後,回表查出剩餘欄位資訊,再去匹配col_b。假設col_a過濾後還有10000條資料,而通過col_b過濾後只會剩餘1000條資料,則有9000條的資料是沒必要的回表。

本質上索引樹上是包含col_b欄位的,只是col_b不能利用索引樹二分查詢特性(因為使用了前模糊匹配),但是可以利用索引上現有的資料進行遍歷,減少無效回表。有了ICP後,基於索引就可以過濾col_a和col_b欄位,過濾後只會剩下1000條資料,減少了大量的回表操作。

小結:
通過執行計劃我們可以分析出SQL最終使用了什麼索引,對索引的使用是處於什麼情況,進而可以得出還有沒有優化空間。

四、總結

我們要有質量意識,做好預防而不是做補救,SQL優化在開發階段就要考慮清楚,而不是等上線後出現慢查了才去優化。


做好SQL優化可以記住一個口訣,有用高。SQL要有索引(建立正確的索引),索引要可用(避免索引失效),最後要考慮高效(覆蓋索引、索引的選擇性)。