了解最新公司動態(tài)及行業(yè)資訊
這篇主要介紹對慢 SQL 優(yōu)化的一些手段,而在講解具體的優(yōu)化措施之前,我想先對 EXPLAIN 進行介紹,它是我們在分析查詢時必要的操作,理解了它輸出結(jié)果的內(nèi)容更有利于我們優(yōu)化 SQL。為了方便大家的閱讀,在下文中規(guī)定類似 key1 的表示二級索引,key_part1 表示聯(lián)合索引的第一部分,unique_key1 則表示唯一二級索引,primary_key 表示主鍵索引。
Explain 是我們在對慢 SQL 進行優(yōu)化前常用語句,它能分析具體的查詢計劃,從而讓我們有目的地去進行優(yōu)化。本節(jié)則主要是讓大家看懂 Explain 查詢結(jié)果的每一列是干啥用的,我們先簡要的來看一下各個列的作用:
列名
描述
id
在一個大的查詢語句中,每個 SELECT 關鍵字都對應一個唯一的 id。在連接查詢中,記錄的 id 值都是相同的;在多個 SELECT 關鍵字的查詢中,查詢優(yōu)化器可能會對子查詢進行優(yōu)化,使得多條 SELECT 記錄的 id 值相同
select_type
查詢類型
table
表名
partitions
匹配的分區(qū)信息
type
針對單表的訪問方法
possible_keys
可能用到的索引
key
實際使用的索引
key_len
實際使用的索引長度
ref
當使用索引列等值查詢時,與索引列進行等值匹配的對象信息
rows
預估的需要讀取的記錄條數(shù)
filtered
針對預估的需要讀取的記錄,經(jīng)過搜索條件過濾后剩余記錄條數(shù)的百分比。在單表查詢中沒什么意義,在連表查詢中可以計算出在驅(qū)動表執(zhí)行完查詢后,還需要對被驅(qū)動表執(zhí)行多少次查詢
Extra
額外的備注信息
其中大部分列在描述信息中已經(jīng)解釋的足夠清楚,下面我們主要對一些必要的列進行詳述:
當訪問方法是 const、ref、ref_or_null、eq_ref、unique_subquery 和 index_subquery 其中之一時,ref 列展示的是與索引列進行 等值匹配 的東西是啥:
const: 表示是一個常數(shù)func: 表示是一個函數(shù)DBName.TableName.columnName: 表示某個數(shù)據(jù)庫某個表中的某個列在前文中我們已經(jīng)詳細介紹了 EXPLAIN 語句中的訪問類型(type),如果一個查詢的訪問類型并不是我們預期的,那么最簡單直接的解決辦法是為搜索條件列 增加合適的索引。
在有些情況下,簡單地增加索引并不能解決問題,比如執(zhí)行如下 SQL:
select name, count(name) from specific_table group by key1;這條 SQL 執(zhí)行完畢后可能只返回幾行數(shù)據(jù),但是因為有 COUNT 聚合函數(shù),需要掃描的數(shù)據(jù)可能會有成千上萬行,這取決于表中數(shù)據(jù)量總數(shù)。對于這種 掃描大量數(shù)據(jù)卻只返回少數(shù)行 的情況,通??梢酝ㄟ^ 增加單獨的匯總表 進行優(yōu)化,當然這需要在應用層增加相應的邏輯對匯總表的數(shù)據(jù)進行維護。
除此之外,還可以通過 重寫復雜查詢 的方式來優(yōu)化,下面我們對重寫查詢時需要考慮的方向進行介紹:
這是一個值得考慮的問題。將復雜查詢拆成多個簡單查詢,盡可能地減少數(shù)據(jù)庫的工作,并將一些處理邏輯拿到應用層處理,因為 MySQL 處理簡單查詢很高效,所以通常情況下這么做能夠提高效率。
在實際工作中,對數(shù)據(jù)量較大的數(shù)據(jù)庫表進行結(jié)轉(zhuǎn)(或刪除)時通常會采用 切分處理 的方法,將一個大查詢分成小查詢,每個查詢的作用是一樣的,只不過操作的數(shù)據(jù)量不同,各個小查詢執(zhí)行完畢后,大查詢的任務也就處理完成了。
一次性結(jié)轉(zhuǎn)大量數(shù)據(jù)可能會鎖住很多數(shù)據(jù)、占滿整個事務日志、耗盡系統(tǒng)資源和阻塞很多小的查詢等,為了避免這種情況,通常在一次數(shù)據(jù)結(jié)轉(zhuǎn)任務中只操作 一萬條左右 數(shù)據(jù),這樣對服務器影響最小,而且可以在每次結(jié)轉(zhuǎn)完成時,都 暫停一會兒 再去執(zhí)行下一次任務,這樣做可以將壓力分散到一個比較長的時間段中,大大降低對服務器的影響和減少持有鎖的時間。
阿里巴巴開發(fā)手冊中提到過一點,聯(lián)表查詢時聯(lián)表的數(shù)量不超過 3 個。如果聯(lián)表過多,我們需要將其拆成多個查詢或多個單表查詢(單表查詢的 緩存效率會更高),查詢被分解后,查詢間的鎖競爭會減少。除此之外,聯(lián)表查詢還需要注意以下兩點:
確保 ON 或者 USING 子句中的列上有索引確保任何 GROUP BY 和 ORDER BY 中的表達式只涉及一個表中的列,這樣 MySQL 才有可能使用索引來優(yōu)化這個查詢一般情況下我們認為 IN() 完全等價于多個 OR 條件,但是在 MySQL 中這兩者是有區(qū)別的。MySQL 在處理 IN() 條件時,會將列表中的數(shù)據(jù)先進行排序,然后通過二分查找的方式來確定列表中的值是否滿足條件,這是一個時間復雜度為 O(logn) 的操作,如果等價地轉(zhuǎn)換成 OR 查詢,它的時間復雜度為 O(n),所以在 IN() 條件中有大量取值時,MySQL 的處理速度會更快。
當需要 Min() 和 Max() 操作時,索引列不為空可以讓它們更高效。比如要找到某一列的最小值,只需要查詢對應 B-Tree 索引的最左端記錄,查詢優(yōu)化器會將這個表達式看做一個常數(shù)對待,而且能夠在 ESPLAIN 結(jié)果的 Extra 列中發(fā)現(xiàn) “Select tables optimized away”。
重復索引指的是在相同的列上按照相同順序創(chuàng)建的相同類型的索引,如下 SQL 所示:
create table specific_table ( id int not null primary key, unique key(id) )engine=InnoDB;它在 id 列上創(chuàng)建了兩個相同的索引,需要將其中的唯一索引移除。
冗余索引通常發(fā)生在為表添加新的索引時,比如在已有索引(column_a),再添加一個索引(column_a, column_b),這就是發(fā)生了冗余索引的情況,因為第二個聯(lián)合索引能夠發(fā)揮和單列索引一樣的作用。
大多數(shù)情況下都不需要冗余索引,我們應該盡量擴展已有的索引而不是創(chuàng)建新的索引。
在多列上獨立地創(chuàng)建多個單列索引,大部分情況下并不能提高 MySQL 的查詢性能。
MySQL 中有一種 “索引合并” 的策略,它可以 使用表中的多個單列索引 來定位指定的數(shù)據(jù)行,并將掃描結(jié)果進行合并。索引合并的策略有時候非常不錯,但更多的時候,它說明了表中的 索引建的比較糟糕:
當查詢優(yōu)化器需要對多個索引合并時,通常意味著需要一個包含所有相關列的聯(lián)合索引,而不是多個獨立的單列索引當優(yōu)化器需要對多個索引做合并操作時,通常需要在算法的緩存、排序和合并操作上耗費大量 CPU 和內(nèi)存資源,尤其是當其中有些索引列值的選擇性不高且需要合并掃描返回的大量數(shù)據(jù)時優(yōu)化器不會將這些操作算在查詢成本中,這會使得查詢的成本被“低估”,導致執(zhí)行計劃還不如進行全表掃描通常來說,我們需要考慮 重建索引 或者 使用 UNION 改寫查詢。除此之外,可以通過修改 optimizer_switch 參數(shù)來關閉索引合并功能,如下 SQL:
SELECT @@optimizer_switch; -- 改成 index_merge=off set optimizer_switch = index_merge=off, ...;還可以使用 IGNORE INDEX 語法讓優(yōu)化器來忽略到某些索引,從而避免優(yōu)化器使用包含該索引的索引合并執(zhí)行計劃:
select * from specific_table ignore index(index_name) where column_name = #{value};除了在發(fā)生索引合并時考慮忽略索引,也需要在執(zhí)行查詢時因無法形成合適的掃描區(qū)間,達不到減少掃描記錄的數(shù)量的目的時,考慮忽略索引而使用全表掃描。
下面我們介紹三種索引合并的類型,讓大家對索引合并有一個更加充分的了解:它們分別是 Intersection 索引合并、Union 索引合并 和 Sort-Union 索引合并。
我們看如下查詢:
select * from specific_table where key1 = a and key2 = b;我們都能清楚的是:在索引列值相同的情況下,二級索引記錄是按照主鍵值的大小排序的,那么可以將 key1 篩選出的主鍵值和 key2 篩選出的主鍵值 取交集,根據(jù)結(jié)果再去執(zhí)行回表操作,這相比于分別對 key1 和 key2 篩選出的主鍵值都去做回表的開銷要低,這種情況使用的是 Intersection 索引合并策略。
我們看如下查詢:
select * from specific_table where key1 = a or key2 = b;將 key1 篩選出的主鍵值和 key2 篩選出的主鍵值 取并集,再根據(jù)結(jié)果去做回表操作,這種做法被稱為 Union 索引合并,它可能相比于直接做全表掃描的開銷要低。需要注意的是:Union 索引合并要求二級索引篩選出的主鍵值是有序的,如果主鍵值無序則需要考慮 Sort-Union 索引合并。
有如下查詢:
select * from specific_table where key1 < a or key2> b;我們將上述查詢條件更改成了范圍查詢條件,現(xiàn)在各索引篩選出的主鍵值是無序的,所以無法使用 Union 索引合并,而 Sort-Union 索引合并正是在 Union 索引合并的基礎上添加了排序操作:將 key1 篩選出的主鍵值和 key2 篩選出的主鍵值 進行排序,這樣就能夠繼續(xù)使用 Union 索引合并了。
當我們需要 統(tǒng)計有值的結(jié)果 時,需要在 COUNT() 條件內(nèi)指定列名或 COUNT(0);當我們需要 統(tǒng)計所有的行數(shù) 時,需要指定為 COUNT(*),它會忽略所有列而直接統(tǒng)計所有行數(shù)。明白了這兩點之后,我們做數(shù)據(jù)統(tǒng)計能夠更清晰的傳達意圖。
通常來說,COUNT() 查詢需要掃描大量的數(shù)據(jù)行才能獲得精確的結(jié)果,所以比較難優(yōu)化。如果業(yè)務場景不要求完全精確,我們可以 使用 EXPLAIN 估算的行數(shù) rows 來代替;或者,我們?nèi)サ粢恍┎樵儣l件中的約束,刪除 DISTINCT 來避免排序操作,這些做法都可能使統(tǒng)計查詢性能提高。
在我們使用 UNION 查詢時,如果不需要消除重復的行,一定要使用 UNION ALL,因為如果沒有 ALL 關鍵字,MySQL 會給臨時表加上 DISTINCT,這會對數(shù)據(jù)做去重,代價比較高。此外,我們可以將 WHERE、LIMIT 和 ORDER BY 語句應用到每個查詢中,這樣能夠讓 MySQL 對它們更好地進行優(yōu)化。
在分頁查詢中,OFFSET 會導致 MySQL 掃描大量不需要的行然后再拋棄掉,比如 LIMIT 1000, 20 這個表達式,它會查詢 1020 條數(shù)據(jù)然后將前 1000 條拋棄掉,這樣做的代價非常高。
我們可以通過采用 書簽 的方式記錄上次讀取數(shù)據(jù)的“位置”,那么下次查詢就能直接從該位置開始掃描,避免使用 OFFSET。比如說,每頁展示 20 條數(shù)據(jù),我們記錄下來當前所在頁面的數(shù)據(jù) ID 值為 200,那么我們看下一頁的數(shù)據(jù)時,查詢 SQL 如下:
select * from specific_table where id <= 180 limit 20;不過,這種情況也有不足,它沒有辦法指定頁碼進行查詢,比如說我現(xiàn)在想看第 5 頁的數(shù)據(jù),我們沒辦法計算對應頁具體的 ID 值范圍。除非我們能夠保證 ID 值是單調(diào)遞增且沒有刪除過數(shù)據(jù)的,這樣的話, ID 值是連續(xù)的,我們就能輕易的計算出第 5 頁的數(shù)據(jù)的 ID 值是從 120 開始的。這樣做的好處是無論翻頁到多么靠后,它的性能都很好。
我們通常使用 GROUP BY 做分組聚合查詢,如果還要對分組后的結(jié)果再次求和,可以使用 WITH ROLLUP 操作,但是更好的辦法還是將 WITH ROLLUP 的處理拿到應用層去做。
如果我們 刪除了很多數(shù)據(jù),或者在插入數(shù)據(jù)時,不是按照主鍵的遞增順序插入的,很可能會因此產(chǎn)生很多內(nèi)存碎片,影響數(shù)據(jù)查詢的效率。這是因為在刪除數(shù)據(jù)時,MySQL 并不會立即將它們清除并整理空間,而是將它們標記為刪除,通過 OPTIMIZE TABLE 可以將空間進行整理,減少內(nèi)存碎片。
InnoDB 引擎并不支持 OPTIMIZE TABLE 操作,它會提示如下信息:
OPTIMIZE TABLE specific_table; -- Table does not support optimize, doing recreate + analyze instead我們可以通過不做任何操作的 ALTER 命令來重建表達到以上目的:
alter table specific_table engine=InnoDB;執(zhí)行完成后,我們通過如下 SQL 查看執(zhí)行情況,如果 data_free 列為 0,說明我們空間碎片整理成功
show table status from specific_db like specific_table;不過,多數(shù)情況下不需要執(zhí)行該操作。
可能因硬件問題、MySQL 本身的缺陷或者操作系統(tǒng)的問題導致索引損壞,當然這種問題非常少見,我們可以通過如下 SQL 來檢查大多數(shù)表和索引的錯誤:
check table specific_table;如果發(fā)現(xiàn)異常的話,可以通過如下 SQL 進行修復:
repair table specific_table; -- 如果存儲引擎不支持上述操作的話,也可通過表重建來完成 alter table specific_table engine=InnoDB;作者:京東物流 王奕龍
來源:京東云開發(fā)者社區(qū) 自猿其說Tech 轉(zhuǎn)載請注明來源