1. 背景
在實際生產中,因為SQL較慢、SQL關聯不合理、不了解索引的性質、不熟悉mysql執行計劃分析,可能會出現一些生產事故,本文會簡單說明SQL通常的優化分析思路。
基本的優化原則:
- 先優化SQL
- 再優化mysql server
- 最后優化硬件
2. 優化sql從執行計劃開始
執行計劃是mysql語句優化中最長使用的方式。相比于開發人員依賴經驗、感覺進行優化,explain將會提供精確的分析結論。
其使用非常簡單:
explain select data from A where id = "asdaqsdqw"
explain后面加上你想要分析的語句,執行即可。
重點在于執行計劃結果的分析。
來看一個典型的explain結果:
2.1 table字段
2.1.1 分析
涉及到的表。一般而言,只涉及一張表就只有一行,但涉及多張表如n張表時,則至少有n行,因為多表join的過程可能出現中間臨時表,這也是表,雖然不會出現在sql中,但執行時是會計算在執行計劃內的。
對于inner join情況,先出現的表是驅動表,后出現的表是被驅動表。
2.1.1.1 驅動表
驅動表(Driving Table) 是作為外層循環的表,被驅動表(Driven Table)是作為內層循環的表。選擇合適的驅動表能顯著減少連接的總計算量,核心原則是:“小表驅動大表”(用數據量更小的表作為驅動表)
理由:
總操作次數 ≈ 驅動表行數 × 被驅動表中每行的匹配次數。
若驅動表是小表(如 100 行),被驅動表是大表(如 100 萬行):總操作次數約為 100 × 平均匹配次數。
若反過來(大表驅動小表):總操作次數約為 100萬 × 平均匹配次數,是前者的 1 萬倍。
顯然,小表作為驅動表能大幅減少外層循環次數,降低總計算開銷。
偽代碼輔助理解:
#偽代碼:嵌套循環連接
for 驅動表中的每一行 a in A:for 被驅動表中匹配 a 的行 b in B:輸出 (a, b)
所以,explain的結果,行數越少越好,用于驅動的表規模越小越好。
注意,這里的 “小” 不是指表的原始大小,而是 “經過 WHERE 過濾后參與連接的行數”,比如A表原本有100萬數據,但謂詞下推過濾后剩30條,那么驅動規模按30計算。
此外,被驅動表中,即使一個字段被加了索引,也很有可能在查詢中不生效。
比如:
A表內連接B表,在where子句中使用B表的create_time作為時間范圍限制,B.create_time已經加了索引,但為什么這個索引沒生效?
因為MySQL 會先掃描 A表 的行,再逐行到 B表 中匹配連接條件(如 A.id = B.a_id)。此時,WHERE B.create_time 的過濾條件可能在 連接之后 執行(即先匹配所有滿足連接條件的行,再過濾時間范圍),導致索引失效,退化為普通全集掃描。
這個要點在后面線上事故分析時會看到實際案例。
2.1.2 使用
多數數據庫(如 MySQL、PostgreSQL、SQL Server)的優化器會根據表的統計信息(行數、索引分布等)自動選擇 “成本最低” 的驅動表,無需人工干預。
但在以下場景可能需要手動指定:
- 統計信息過時(如剛批量插入數據未更新統計信息),導致優化器誤判表大小(可以執行ANALYZE TABLE Table_name來解決);
- 復雜多表連接(3 張以上),優化器可能因計算復雜度選擇次優順序。
手動指定方式(以 MySQL 為例):
用 STRAIGHT_JOIN 強制左表為驅動表:
-- 強制 A 作為驅動表,B 作為被驅動表
SELECT * FROM A STRAIGHT_JOIN B ON A.id = B.a_id;
2.2 type字段(重要)
type 字段 描述了 表的訪問方式(即 MySQL 如何查找表中的行),是判斷查詢效率的核心指標之一。type 的值從 “性能最優” 到 “性能最差” 有明確的優先級,直接反映了查詢對索引的利用效率
type 可能出現的值及含義(按性能從優到劣排序)
- system:表中只有一行數據(系統表),是 const 類型的特例,幾乎不會在生產中出現
- const:通過主鍵或唯一索引的 等值查詢 匹配到一行數據(最多一行),唯一主鍵且不join時會有
- eq_ref:在 多表 JOIN 中,被驅動表的連接列是 主鍵或唯一非空索引,且每個驅動表的行只能匹配到被驅動表的一行
- unique_subquery:子查詢中使用了唯一索引,替代了 eq_ref 的 JOIN 方式(適用于 IN 子查詢),一般認為和eq_ref效率基本一致
- ref:非唯一索引的等值查詢,或 JOIN 中被驅動表的連接列是 非唯一索引,可能匹配到多行
- ref_or_null:類似 ref,但額外包含對 NULL 值的查詢(即條件中包含 IS NULL)
- index_merge:MySQL 使用 索引合并優化,即同時使用多個單列索引,將結果合并(交集 / 并集)后返回
- range:使用索引進行 范圍查詢,只檢索索引中某一范圍內的行,范圍越小(匹配行數越少)效率越高。若范圍過大(接近全表),可能退化為全表掃描
- index:掃描 整個索引樹(而非全表),但未利用索引篩選,僅通過索引覆蓋數據(即 “索引全掃描”)
- ALL:全表掃描(Full Table Scan),MySQL 會遍歷整個表的所有行來查找匹配的記錄,最差的情況
目標是至少確保 type 達到 range 級別,最好能達到 ref 或更高。若出現 ALL 或 index,需檢查是否缺少索引、索引失效或查詢條件不合理,及時優化。
2.3 possible_keys字段
mysql執行計劃預先判斷某一次查詢可能涉及的索引,如果你期望命中的索引沒有命中,可以先在這里看你的索引是否被排除在預期之外,并據此優化。
有時mysql的執行計劃如果只看keys字段,看起來好像有索引使用,但其實如果把possible_keys和keys連起來看,那么就會發現possible_keys為null時,即使keys有值也不一定走索引。
2.4 key字段(重要)
關鍵字段,代表實際命中的索引。如果為null則代表沒有命中索引。要結合possible_keys字段一起分析。
2.5 ref字段
ref 字段用于描述:在使用索引查找匹配行時,哪些 “值”(可能是列名、常量或函數結果)被用來與索引列進行比較。它直觀地展示了 MySQL 如何利用索引來定位符合條件的行,是理解索引匹配邏輯的重要依據
- 取值為 常量(如 const、具體值),當前表的索引列與常量進行等值匹配 時,ref 會顯示為 const 或具體的常量值,說明用 “常量” 與索引列比對
- 取值為 其他表的列名:在 多表 JOIN 場景中,當被驅動表的索引列與驅動表的某列進行匹配時,ref 會顯示驅動表的 “表名。列名”,說明用驅動表的列值與被驅動表的索引列比對。
- 取值為 函數或表達式結果:當查詢條件中,索引列與函數 / 表達式的結果比對時,ref 會顯示函數或表達式的相關信息(不同 MySQL 版本可能顯示方式不同)
- 取值為 NULL:特殊情況下(如 type 為 index 或 ALL 時),ref 可能為 NULL,表示沒有使用 “具體值” 與索引列比對(可能是全索引掃描或全表掃描,無需比對值)
總而言之,這個字段是key字段的延展,用來告訴你你的索引和什么進行比對
2.6 rows字段
rows 表示 MySQL 優化器估計需要掃描的行數(即 “預計要檢查多少行才能找到符合條件的記錄”),rows 值越小,說明查詢需要檢查的行數越少,效率通常越高
2.7 filtered字段
filtered 表示經過表級條件過濾后,剩余行所占的百分比(取值范圍 0~100),filtered = (滿足條件的行數 / 掃描的行數) * 100
2.8 Extra字段(重要)
Extra 字段用于提供查詢執行計劃的額外細節信息,補充 type、ref 等字段未涵蓋的執行邏輯(如索引使用細節、排序方式、臨時表使用等)。它是判斷查詢是否需要優化的關鍵依據,許多 Extra 值直接反映了查詢的性能瓶頸(如是否需要臨時表、是否需要額外排序等)。
- Using index:使用了覆蓋索引(Covering Index),即查詢所需的所有列(SELECT 后的列 + WHERE/JOIN 條件的列)都包含在索引中,無需回表訪問主鍵索引獲取數據,性能極佳,是索引優化的理想狀態,避免了回表的 IO 開銷
- Using where:MySQL 使用了 WHERE 條件過濾數據,但未使用索引(或雖使用索引但索引未覆蓋所有過濾條件,需在服務器層進一步過濾),性能:若 type 為 ALL(全表掃描),則性能較差,需添加索引;若 type 為 range 等,需結合具體場景優化
- Using index condition:啟用了索引條件下推(Index Condition Pushdown, ICP) 優化,將部分 WHERE 條件下推到存儲引擎層,在索引掃描時直接過濾不符合條件的行,減少回表次數
- Using temporary:MySQL 需要創建臨時表來存儲中間結果(如用于 GROUP BY、DISTINCT 或 JOIN 時的排序 / 去重),性能較差,臨時表(尤其是磁盤臨時表)會增加 IO 和內存開銷,數據量大時可能導致嚴重性能問題。必須優化(如為 GROUP BY 列添加索引)
- Using filesort:MySQL 需要對結果進行額外排序(無法利用索引的自然順序完成排序),排序操作可能在內存或磁盤中進行(數據量大時會寫入臨時文件),性能較差,排序是 CPU 密集型操作,數據量大時會嚴重影響性能。需優化(如為 ORDER BY 列添加索引,或調整排序順序與索引一致)
- Using filesort with small result:類似 Using filesort,但排序的結果集很小,排序開銷較低(MySQL 認為無需優化)
- Using join buffer (Block Nested Loop) 或 Using join buffer (Batched Key Access):JOIN 操作中,被驅動表無可用索引,MySQL 使用連接緩沖區(Join Buffer)存儲驅動表的數據,再與被驅動表的行逐行比對(嵌套循環連接),性能極差,無索引的 JOIN 是性能殺手,數據量大時需為連接列添加索引
- Range checked for each record (index map: N):MySQL 無法確定哪個索引更高效,因此對驅動表的每一行,都會檢查被驅動表的多個索引(index map: N 表示候選索引的位圖),選擇當前行最匹配的索引。可以嘗試手動執行此語句解決:ANALYZE TABLE
- Impossible WHERE:WHERE 條件永遠為 FALSE,MySQL 無需掃描表(如 WHERE 1=0),性能極高但毫無意義
- No tables used:不涉及表查詢,比如select 1+1
總結而言:
- Using temporary:避免臨時表,通過索引優化 GROUP BY/DISTINCT;
- Using filesort:添加排序索引,或調整 ORDER BY 順序與索引一致;
- Using join buffer:為 JOIN 的 ON 條件列添加索引;
- Using where 且 type=ALL:為過濾列添加索引,避免全表掃描。
3. Mysql索引
特別地,任何索引在經過函數計算后都會失效。
3.1 主鍵索引
唯一標識表中的記錄,不允許重復,且不允許為 NULL,每個表只能有一個主鍵索引。
InnoDB 中,主鍵索引是聚簇索引(葉子節點直接存儲整行數據),性能最優。
執行計劃:
{
"explain\r\nselect * from A where id = 1944811872387059713": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "const","possible_keys" : "PRIMARY","key" : "PRIMARY","key_len" : "8","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
3.2 唯一索引
確保索引列的值唯一(允許 NULL,但 NULL 只能出現一次)。Mysql非主鍵索引中最高效的鍵,它在建表時表達為UNIQUE,可以唯一定位一條數據。
對于唯一鍵,建議的使用方式只有一種,就是等于。
舉例:
A表的唯一鍵是id,那么查詢時應當:
select data from A where id = xxx
其執行計劃表達為:
{
"explain\r\nselect * from service_entity where service_address=\"127.0.0.1\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "service_entity","partitions" : null,"type" : "const","possible_keys" : "UKjety3jc210qsbnuij9yl61nf2","key" : "UKjety3jc210qsbnuij9yl61nf2","key_len" : "1022","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
特別地,如果此唯一索引為int或bigint,且符合嚴格遞增,那么我們遇到深分頁問題時,可以嘗試用唯一索引的范圍來優化查詢,這同樣也適用在主鍵索引為數值的情況:
select id from A where id >= xxx and id < yyy and ...
其執行計劃表達為:
{
"explain\r\nselect * from test_a where connections > 1 and connections <= 3\r\n\r\n\r\n": [{"id" : 1,"select_type" : "SIMPLE","table" : "test_a","partitions" : null,"type" : "range","possible_keys" : "idx_conn","key" : "idx_conn","key_len" : "5","ref" : null,"rows" : 2,"filtered" : 100.0,"Extra" : "Using where; Using index"}]
}
此處我們可以看到Extra中仍然存在using where,這是因為除了范圍沒有使用別的索引,如果查詢還有其他索引,則還有優化空間
3.3 時間索引
時間索引一般而言使用模式比較固定,例如create_time,大多數場合都是create_time > xxx and create_time < yyy的使用模式。
不過也有一些細節可以注意。
看下面的SQL
select data from A where create_time >= '2025-09-01 00:00:00' and create_time < '2025-09-02 23:59:59'
這是我們在實際生產中見到的一類寫法,這一類寫法沒有什么大問題,在必須精確到毫秒數的場合一定是這種寫法。
其執行計劃表現為:
{
"explain\r\n select * from A where create_time >= '2025-09-01 00:00:00' and create_time < '2025-09-02 23:59:59'": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "range","possible_keys" : "timer_cre","key" : "timer_cre","key_len" : "4","ref" : null,"rows" : 45534,"filtered" : 100.0,"Extra" : "Using index condition; Using MRR"}
]}
特別地,Extra 列出現 “Using MRR” ,表示優化器啟用了 MRR(Multi-Range Read,多范圍讀取)優化策略。這是 MySQL 針對 “二級索引查詢 + 回表” 場景設計的性能優化手段,核心目標是將 “隨機 IO” 轉化為 “順序 IO” ,減少磁盤 IO 開銷,提升查詢效率。總之,是一種比using where更優的表現。
如果是限定時間范圍查詢,比如某些場合已經預先確定一次查詢必然查且只查一個自然日,那么更好的寫法是:
select data from A where create_time = '2025-09-02'
其執行計劃表現為:
{
"explain\r\nselect * from async_main_task_0 where create_time = \"2025-09-09\"\r\n\r\n\r\n": [{"id" : 1,"select_type" : "SIMPLE","table" : "async_main_task_0","partitions" : null,"type" : "ref","possible_keys" : "timer_cre","key" : "timer_cre","key_len" : "4","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
3.4 字符串索引
這是實際生產中最常見到的一類索引,這一類索引不像數字索引那樣可以簡單的比較大小,其效率極大地取決于用戶的使用方式。
3.4.1 最高效的方式
當然是直接等于:
select data from A where trace_id = "asdqwdzscqwfcqf"
其執行計劃表現為:
{
"explain\r\n\r\nselect * from A where task_id = \"47f8fc36febb44f8afb5d08240bf4868\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "ref","possible_keys" : "idx_task_id","key" : "idx_task_id","key_len" : "1022","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : null}
]}
3.4.2 次優的方式
左like:
select data from A where trace_id like "asdwd%"
其執行計劃表現為:
{
"explain\r\n\r\nselect * from A where task_id like \"47f8fc36febb44f8afb5d08240bf4%\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "A","partitions" : null,"type" : "range","possible_keys" : "idx_task_id","key" : "idx_task_id","key_len" : "1022","ref" : null,"rows" : 1,"filtered" : 100.0,"Extra" : "Using index condition"}
]}
3.4.3 不生效的方式
- 兩側like: select data from A where trace_id like “%asdqwd%”
- 對列做函數計算:select data from A where len(trace_id) > 100
以上兩種,絕對不要在大數據量、大QPS場合出現必死。
3.5 聯合索引
3.5.1 聯合索引的建立
聯合索引涉及到多個字段,由于mysql基礎數據結構的限定,必須要在建立索引時就決定好從左到右的順序。
以A表為例,如果其包含字段x,y,z,j,k,l,當我們想使用聯合索引時,就必須預先分析下面兩個問題:
- 要用哪些字段加入到聯合索引
- 這些索引的排序
這兩個問題極其重要,會極大影響索引的過濾效果。
下面給出一些分析標準來確定哪些字段應當加入聯合索引:
- 區分度極高(如各種ID),且固定出現在where條件里
- 可以大規模過濾掉非目標數據(如時間段,如各種數值區間)
- 在條件查詢的where中高頻出現
- 絕不參與任何函數計算
假如根據上面的標準,我們篩選出x,z,j三個作為聯合索引,那么排序應當按照如下原則從左到右排列:
- 固定出現的查詢條件
- 高頻出現的查詢條件
- 可以過濾更多數據的查詢條件
- 未必會出現的查詢條件
比如,z條件必定出現在每次查詢中,則z排第一位;x大概率出現,且為id字段,則排第2位;j不長出現,則排第三位,那么創建索引的語句為:
create index `udx_z_x_j` on A (z,x,j)
一定要注意,越靠左的索引越重要,使用聯合索引查詢時,左邊的索引一旦缺失,整個查詢效率會有巨幅下降。
創建該索引后,只要z,x出現,那么j不出現,也至少可以走兩個索引。
3.5.2 聯合索引的查詢
查詢過程務必遵守創建聯合索引時的左右順序,盡最大可能保證建立索引時左邊的索引在where中出現:
select data from A where z="asdqdqw" and x like "niqdwnu%"
如果沒有遵循這個原則,那么從左側哪一個查詢條件開始缺失,就會從哪一個索引開始失效。例如:
select data from A where x = 'aedqw'
看起來好像使用了聯合索引中的x,但由于最左的z缺失,所以整個聯合索引都失效了。
實際SQL:
-- test_jpa.test_union_index definitionCREATE TABLE `test_union_index` (`id` bigint(20) NOT NULL,`task_type` varchar(128) NOT NULL COMMENT '任務分類:智批改任務,劃題任務,直接干預任務等',`task_status` varchar(32) NOT NULL COMMENT '任務狀態,INIT:初始化,DOING:進行中,FAILED:失敗,SUCCEED:成功',`task_stage` varchar(32) NOT NULL COMMENT '任務階段,LAYOUT_ANALYSIS:版面分析,GET_TOPIC:取題,RECOGNIZATION:識別,CORRECTION:批改,INTERENTION:干預',PRIMARY KEY (`id`),KEY `udx_type_status_stage` (`task_type`,`task_status`,`task_stage`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;explain
select * from test_union_index
where task_status="STATUS_092" andtask_stage = "STAGE_092"
實際的執行計劃為:
{
"explain\r\nselect\r\n\t*\r\nfrom\r\n\ttest_union_index\r\nwhere\r\n\t\r\n\ttask_status=\"STATUS_092\" and\r\n\ttask_stage = \"STAGE_092\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "test_union_index","partitions" : null,"type" : "index","possible_keys" : null,"key" : "udx_type_status_stage","key_len" : "774","ref" : null,"rows" : 10119,"filtered" : 1.0,"Extra" : "Using where; Using index"}
]}
看似type為index好像走了索引,但實際上possible_keys為null,實際掃描的行數10119,就是全表的行數,根本未走索引。
而真正命中索引的效果為:
{
"explain\r\nselect\r\n\t*\r\nfrom\r\n\ttest_union_index\r\nwhere\r\n\t\r\n\ttask_type=\"TYPE_092\"": [{"id" : 1,"select_type" : "SIMPLE","table" : "test_union_index","partitions" : null,"type" : "ref","possible_keys" : "udx_type_status_stage","key" : "udx_type_status_stage","key_len" : "514","ref" : "const","rows" : 1,"filtered" : 100.0,"Extra" : "Using index"}
]}
3.5.3 合理使用聯合索引避免回表
mysql中同一個索引涉及到的內容維護在同一棵索引樹,如果要查詢的字段包含在索引里且索引生效,那么就可以避免查詢到索引后還需要回查真實數據。
3.6 多種索引沖突
有時SQL太過于復雜,join的表太多,索引之間會有沖突,未必真就會命中預期的索引。這種時候需要從未加索引的狀態開始,一點點加索引做測試,千萬不要偷懶。
4. 跨表JOIN問題
4.1 使用join的基本原則
這是整個Mysql生產中最容易出問題的部分。這里提出如下一些基本思想:
- 首先判斷是否只能使用join解決問題,而不能使用任何別的手段。只要還有別的更優方案,盡量避開join
- 如果一定要使用join,那么inner join優先其他join(內連接取交集,數據量相對小)
- 盡最大可能避免join超過2張表(多表情況,mysql執行器會產生優化障礙和驅動表選擇障礙)
- Join on的字段一定要對兩張表都是索引,必須要用等于,比如task_id對于A和B都是索引,那么select data from A inner B where A.task_id=B.taskId
- join后的表絕對不要寫select *,一是未必需要全字段,二是不同表也許有同名字段,一旦和實體字段映射錯了那么查詢直接崩潰報錯,三是有時候精確查詢是可以省略回表的,全查則沒有這種可能性
- 一定要注意A和B表連接的字段是否是同樣的數據類型,比如A中taskId為varchar,B中為BIGINT,那么就會發生隱式轉換,這是一種函數計算,會導致索引失效
- 一定要使用explain分析執行計劃,多方比較,直到explain的所有行都走索引、Extra字段中幾乎沒有using temporary、using where情況才算優化完成
4.2 謂詞下推
在數據庫查詢優化中,謂詞下推(Predicate Pushdown) 是一種通過將過濾條件(謂詞)盡可能提前應用來減少數據處理量的優化技術。其核心思想是:讓數據在進入后續處理階段(如連接、排序、聚合等)之前,就被過濾掉不需要的部分,從而降低整體計算和 IO 開銷。
4.2.1 謂詞
指查詢中用于篩選數據的條件,通常出現在 WHERE、ON 或 HAVING 子句中,例如 age > 30、status = ‘active’ 等
4.2.2 下推
指將這些過濾條件從查詢的 “上層”(如外層查詢、聚合階段)“推” 到 “下層”(如子查詢、表掃描階段、數據源讀取階段)執行,讓過濾操作在數據處理的早期階段完成
4.2.3 為什么需要謂詞下推
沒有謂詞下推時,查詢可能會先讀取大量無關數據,再在后續階段過濾,導致資源浪費。例如:
假設查詢是 A和B join并查詢A.a大于10,B.b小于100 的行:
-- 無優化時的執行邏輯(低效)
select * from A Join B on A.id = B.id where A.a > 10 and B.b < 100;
此時子查詢會先返回表中所有行,再在外部過濾。
應該改為:
select * from (select * from A where a>10)A1 Join (select * from B where b<100)B1 on A1.id = B1.id;
4.3 不使用join的一些解決思路
4.3.1 從復雜join更替為多個簡單查詢,在代碼中篩選
有時候一條語句實在太過復雜,沒有優化的思路,那么也許可以試試把復雜sql拆成多個簡單查詢。
比如A和B和C表關聯查詢,A表的join字段需要做函數計算導致索引失效。
也許可以嘗試先查一下A表,如果規模并不大,比如結果集只有幾十條上百條,那不妨先查A,而后在代碼中對A的結果做你想要做的函數計算,再把計算結果作為過濾條件參與到B和C的關聯查詢中。
4.3.2 將join改為where中的exists
比如需要查詢A表中符合B表某些條件的數據,如果用join則涉及到關聯,但如果替換為exists,mysql會自動優化為單表查詢,直接優化掉一整張表的掃描,這比任何索引優化都強。
4.3.3 合理冗余字段
如果存儲數據時預先考慮到join可能出現的情況,那么設計表時可以考慮冗余一些字段,比如原本要AjoinB來查詢B中的一兩個字段,那么有沒有可能直接在A中添加這兩個字段來避免JOIN呢?
現在隨著固態硬盤的發展和降價,少量空間上的冗余并不耗費多少成本,但慢SQL一旦出現線上問題,損失可能比存儲貴得多。
5. 分治策略
對于特別復雜的查詢情況,涉及多表多條件,不妨試試看用分治的思想來應對。
核心思路是把一個統一大查詢拆分成幾種不同的分支情況,在代碼里決定不同情況走不同查詢,而后針對不同的查詢專項優化。
比如某種查詢,涉及到AB表關聯查詢,B表查詢條件視情況發生變化,未必出現,最終的結果要按照A表的時間字段進行排序分頁。
那么就可以分出兩種情況:
- 不存在B表的查詢條件
- 存在
針對1情況,結合上面的知識,就可以比較容易的得出優化方案,首先把A表做一次查詢和分頁,記為小表再inner joinB表,只要A表查詢中索引設計合理,該查詢的整體效率可以堆到近乎于單表索引查詢的程度。
針對2情況,再針對B不同查詢條件出現的可能性,針對性調整驅動表、聯合索引等部分。
這樣,通過部分的優化,逐漸逼近整體的最優效果。
6. 涉及分庫分表
6.1 選擇優良的分庫分表鍵
優良的分庫分表鍵的標準:
- 分布均勻:分片鍵需能將數據盡可能均勻地分散到所有分庫分表中,避免某幾個庫 / 表數據量過大(“數據傾斜”)或過小(資源浪費)。例:若用 “用戶性別” 作為分片鍵,僅能分為 2 組,必然導致數據傾斜;而用 “用戶 ID 哈希” 則可分散到 N 個分片。
- 匹配高頻查詢場景:分片鍵需與業務中最頻繁的查詢條件強關聯,確保絕大多數查詢能通過分片鍵直接定位到目標庫 / 表,避免 “跨庫全表掃描”。例:訂單查詢 90% 場景是 “按用戶 ID 查詢我的訂單”,則優先用 “用戶 ID” 作為分片鍵,而非 “訂單創建時間”。
- 貼合用戶需要:統計業務中 80% 以上的查詢 SQL,提取WHERE條件中最常出現的字段(如用戶 ID、訂單 ID、租戶 ID)。典型案例:電商訂單表中,“用戶 ID” 是高頻查詢字段(用戶查自己的訂單),適合作為分片鍵
- 實體唯一標識優先:這類字段天然具備 “唯一性” 和 “穩定性”,且與業務邏輯強綁定,便于理解和維護,如用戶表的user_id、商品表的product_id
- 如若必須用多字段組成分庫分表鍵,則建議數據雙寫,雙表查詢。例如:通過 “數據冗余”(如訂單表按用戶 ID 分片,同時冗余一份按時間分片的表)實現。理由:組合分片鍵會增加查詢復雜度,且難以保證所有查詢都能匹配組合條件,容易引發跨庫表查詢
6.2 確保查詢攜帶分庫分表鍵
如果熱點查詢沒有攜帶分庫分表鍵,會觸發跨庫跨表全掃描,這個操作的性能開銷是線上高并發業務不可承受的
7. mysql服務端的配置(DBA操作部分)
有時查詢語句本身囿于業務強需求已經沒有太多優化空間,那么考慮調優服務端配置也是一種思路。
需要注意,這種調整相對復雜,影響比較廣,一定要充分和DBA協商清楚;如果數據庫存在多業務復用,那么所有相關業務方都需要納入討論。
7.1 InnoDB專項優化
7.1.1 InnoDB 緩沖池(innodb_buffer_pool_size)
緩存 InnoDB 表的數據、索引、插入緩沖等,是 MySQL 中最重要的內存配置。命中緩沖池的查詢可直接從內存獲取數據,無需讀磁盤。
對于專用 MySQL 服務器,設置為系統總內存的 50%~70%(預留部分內存給操作系統和其他進程)
7.1.2 日志相關配置
- innodb_log_buffer_size:事務日志(redo log)的內存緩沖區,滿了會刷盤。
- innodb_flush_log_at_trx_commit:控制事務日志的刷盤策略(平衡性能與安全性)。
- 1(默認):每次事務提交立即刷盤,最安全但性能最低(適合金融等核心業務)。
- 2:事務提交后寫入操作系統緩存,每秒刷盤一次,安全性與性能平衡(推薦)。
- 0:每秒刷盤一次,性能最高但可能丟失 1 秒內的數據(適合非核心業務)。
- innodb_log_file_size:單個 redo log 文件的大小。可酌情提高至 512M~2G(需同時調整innodb_log_files_in_group,通常 2~3 個文件),減少日志切換頻率(頻繁切換會導致 IO 波動)。
7.1.3 IO 并發配置
- innodb_read_io_threads 和 innodb_write_io_threads:控制 InnoDB 的讀寫 IO 線程數,提升 IO 并發處理能力。
- innodb_flush_method:控制 InnoDB 與磁盤 IO 的交互方式(減少雙重緩存)。建議:Linux 系統:O_DIRECT(繞過操作系統緩存,避免 InnoDB 緩沖池與 OS 緩存的重復緩存)。Windows 系統:async_unbuffered。
7.2 連接與排序緩存
- join_buffer_size:用于表連接(JOIN)的緩存,非索引連接(ALL或index類型)會使用此緩存。
- sort_buffer_size:用于ORDER BY或GROUP BY的排序緩存,不足時會使用磁盤臨時文件排序。
- read_buffer_size:用于順序掃描(filesort)的緩存,提升全表掃描或大范圍查詢的效率。
7.3 臨時表緩存
tmp_table_size 和 max_heap_table_size:控制內存臨時表的最大大小(兩者取較小值),超過則轉為磁盤臨時表(存儲在tmpdir目錄)。
默認值 16M,可酌情提高至 64M~256M(根據業務中臨時表的使用頻率調整),減少磁盤臨時表的 IO 開銷。
8. 硬件上的優化
有時軟件上的優化已經沒有更多空間,這時可以考慮從硬件層面下手把查詢效率帶上來。
需要注意,并非所有云廠商都可以指定mysql服務器的硬件,這一點需要預先確認是否可用以及預算是否支持。
8.1 選擇更合適的CPU
8.1.1 Intel 至強 6 性能核處理器(OLTP 首選)
代表型號:Platinum 6787P(56 核 112 線程,全核睿頻 3.5GHz,L3 緩存 140MB)
核心優勢:
單核性能突出:全核睿頻 3.5GHz,配合 Intel AVX-512 指令集,單線程執行效率比上一代提升 36%,尤其適合高并發事務中的單行查詢、鎖競爭場景。
硬件加速技術:內置數據保護與壓縮加速技術(QAT),可將 SSL 加密和數據壓縮卸載至硬件,減少 CPU 占用率 20%-30%。例如,MySQL 備份速度可提升 2.56 倍。
內存帶寬優化:支持 8 通道 DDR5-6400 內存,帶寬高達 204.8GB/s,比上一代提升 2.3 倍,顯著緩解內存瓶頸。
服務器整合能力:單臺服務器可替代 17 臺舊代服務器,TCO 降低 87%,適合企業級集中式數據庫部署。
典型場景:電商秒殺、金融交易、實時訂單系統等需毫秒級響應的 OLTP 場景。
8.1.2 AMD EPYC 9004 系列(OLAP 首選)
代表型號:Genoa-X 9384X(48 核 96 線程,主頻 2.4GHz,L3 緩存 1.152TB)
核心優勢:
3D V-Cache 技術:堆疊式 L3 緩存容量達 1.152TB,是傳統 CPU 的 3 倍,可將復雜分析查詢的內存訪問延遲降低 50% 以上,尤其適合需頻繁掃描大表的 OLAP 場景。
多核并行處理:64 核 / 128 線程設計,配合 Zen4 架構的高 IPC(每周期指令數),在 sysbench 測試中 OLAP 性能比 Intel 同核數 CPU 提升 27%。
高擴展性:支持 12 通道 DDR5-4800 內存和 128 條 PCIe 5.0 通道,可直連高速 NVMe 存儲或 GPU 加速卡,構建分析型數據湖。
能效比領先:5nm 工藝使功耗比上一代降低 30%,在高密度數據中心中每瓦性能比 Intel 高 60%。
典型場景:供應鏈分析、用戶行為畫像、金融風控等需多表關聯和復雜聚合的 OLAP 場景。
8.2 選擇更合適的硬盤
基本原則:
- 固態硬盤 > 機械硬盤
- 新一代固態硬盤 > 前代
- OLTP 用 PCIE更新的版本
- OLAP -> U.2 及以上
- 需求均衡 -> SAS 4.0 及以上