Sargable(可走索引的條件形狀)
Sargable 指一個條件能不能被翻譯成索引上的一次查找,名字來自 search argument able——這個條件當不當得成搜尋的引數。同一個問題的兩種寫法回同一批列,而其中一種讓引擎在執行計畫裡走查找、另一種讓它掃過整張表。
引擎要先有一個具體的值可以比對
索引存的是欄位的值與它們對應的列的位置,所以查找要拿一個值進去比對。條件寫成 下單日 = '2024-03-01' 時那個值就在條件裡。
條件寫成 date(下單日) = '2024-03-01' 時,索引存著 下單日 的值而條件問的是 date() 算完的結果,兩者對不起來。引擎要把每一列的 下單日 取出來各算一次才知道答不答得上,而那就是掃過整張表。同一張表、同一個索引,五萬列:
1-- SQLite 3.51,訂單表五萬列(訂單編號 / 顧客編號 / 下單日 / 金額)
2-- CREATE INDEX ix_日 ON 訂單(下單日),查的是 SELECT *
3下單日 = '2024-03-01' SEARCH 訂單 USING INDEX ix_日 (下單日=?)
4date(下單日) = '2024-03-01' SCAN 訂單這個形狀能排除,確認要看計畫
索引欄位被包進函式或運算裡的時候,那個索引走不了查找(引擎仍可能把它整個掃過去當成一份較窄的資料來源,計畫上是 SCAN ... USING COVERING INDEX),這一步照字面就判得出來:問那個欄位在條件裡是裸著的還是被包住的。上面那兩行取的是 SELECT *,索引蓋不住它才印成 SCAN 訂單;同一組條件換成只取索引欄與主鍵,兩行都會多出 COVERING。除非另外對同一個運算式建了索引——那時走的是另一個索引,對運算式本身建索引那一條路寫的就是它。
裸著則不保證走得了。同一張表上有幾種裸欄位的條件照樣掃全表:開頭是萬用字元的比對(LIKE '%03-01')、OR 接上另一個沒有索引的欄位、以及 LIKE '2024-03%' 這種前綴比對。最後一種有兩條出路:SQLite 打開 case_sensitive_like 之後它自己就走得了,或者手動拆成 >= 與 < 的範圍。實測五萬列上,原本的 LIKE '2024-03%' 掃全表而拆成範圍之後走索引查找,兩者回同一批列。
所以形狀是排除法的一步,答案仍然要向引擎要計畫。要不到計畫的時候,看條件的形狀是僅剩能做的那一步——它只看條件的文字,不需要連線、不需要看資料,也不需要建立任何物件的權限(SQL.16 SQL 的權限模型:角色、GRANT 的授權單位與最小權限)。
被包住與本來就裸著這兩種命中,能動的位置不同。被包住的(函式包住、運算搬得動)改寫條件本身就能讓它走索引,下一節那三條解決的都是同一件事——把索引欄位還原成裸的,或者換一個索引來配合它。三條的代價並不相同:拆成範圍要讀的人自己拼邊界,運算搬邊與運算式索引不動原本的讀法,而運算式索引把代價換到寫入端(哪一條先試在 SQL.21 可讀性與執行代價:兩者一致與分岔的情形)。
本來就裸著而仍然掃全表的,差別在那個條件描不描得出索引上的一段連續區間。前綴比對描得出,所以它換成範圍之後走得了;萬用字元開頭要比對的值散在索引的各處,OR 接無索引欄則有一半根本不在索引裡。這兩種改寫條件本身幫不上忙,剩下的變數在統計與選擇性上。
同一個問題換一個形狀
date() 那個條件要的是某一天,而同一件事可以用範圍表達,兩端都是具體的值:
1下單日 >= '2024-03-01' AND 下單日 < '2024-04-01'
2 SEARCH 訂單 USING INDEX ix_日 (下單日>? AND 下單日<?)把運算搬到不含索引欄位的那一邊也成立:下單日 = date('2024-03-15','-14 day') 的右邊先算成一個值,左邊仍然裸著,計畫走查找。
條件本身改不動的時候,還有一個方向是讓索引去配合條件——對運算式本身建索引。CREATE INDEX ix_月 ON 訂單(substr(下單日,1,7)) 之後,原本掃全表的 substr(下單日,1,7) = '2024-03' 變成 SEARCH 訂單 USING INDEX ix_月 (<expr>=?)。它的代價同樣落在寫入端,而且比一般索引多一項:每次寫入要多算一次那個運算式。
與統計資訊是兩個成因
索引建了而引擎不走它有兩個常見的成因,處置不同。統計資訊過舊時引擎估錯代價、判斷它不划算,那時條件的形狀本來就走得了,重收統計就會改;條件把欄位包住時引擎沒有路可走,統計再新也一樣,那時要改的是查詢。
兩者分得開,是因為形狀決定引擎有沒有路可走、統計只決定它想不想走。把條件換成裸欄位的形狀之後計畫跟著變,代表原本擋住的是路;計畫沒變,代表路一直都在而引擎判斷它不划算。
概念位置
SQL.17 查詢的代價:資料分布、索引與寫法各自的影響 的結論是代價由資料與索引決定、不由寫法決定,而這張卡是那個結論有界的例外。邊界很窄:寫法在這裡決定的是引擎能不能用那個索引,而最佳化器在幾條可走的路裡挑哪一條仍然由資料與統計決定。
往下走
索引本身是什麼、代價為什麼落在寫入端,在 Index(索引)。SCAN 與 SEARCH 這些字怎麼讀,在 Query Plan(執行計畫)。同一段查詢的排名隨索引與資料分布重排的實測,在 SQL.17 查詢的代價。好讀的寫法與走得了索引的寫法分岔時該動哪一層,在 SQL.21 可讀性與執行代價:兩者一致與分岔的情形。