某手機微博 4040 端口長期高延遲,pt-query-digest 定位到一條 COUNT(*) 慢查詢。EXPLAIN 顯示 type=index_merge、Using intersect(idx_weiboId,idx_type),理應走索引,實際卻掃描百萬行、耗時 570ms、CPU 飆升 96%。本文完整復盤從現象觀測、執行計劃分析、數據分佈推理、索引重構到負載驗證的全過程,揭示 Index Merge Intersection 在低選擇性欄位上的致命缺陷 及破解之道。
-- 慢查詢 SELECT COUNT(*) AS total FROM user_rec_07 WHERE type = 5 AND weiboId = '1934676487'; -- 表結構(TokuDB 引擎) CREATE TABLE `user_rec_45` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `softId` INT(11) NOT NULL DEFAULT '0', `weiboId` BIGINT(20) NOT NULL DEFAULT '0', `type` TINYINT(4) NOT NULL DEFAULT '0', `content` VARCHAR(512) NOT NULL DEFAULT '', `ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_softId_weiboId` (`softId`,`weiboId`), KEY `idx_weiboId` (`weiboId`), KEY `idx_type` (`type`) ) ENGINE=TokuDB DEFAULT CHARSET=utf8 ROW_FORMAT=TOKUDB_LZMA; -- EXPLAIN 顯示 index_merge EXPLAIN SELECT COUNT(*) ... WHERE type=5 AND weiboId=...; +----+-------------+--------------+-------------+-------------------+-------------------+---------+------+------+---------------------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------------+-------------+-------------------+-------------------+---------+------+------+---------------------------------------------------+ | 1 | SIMPLE | user_rec_07 | index_merge | idx_weiboId,idx_type| idx_weiboId,idx_type| 8,1 | NULL | 1 | Using intersect(idx_weiboId,idx_type); Using where; Using index | +----+-------------+--------------+-------------+-------------------+-------------------+---------+------+------+---------------------------------------------------+
-- 單獨 weiboId:極高選擇性 EXPLAIN SELECT COUNT(*) FROM user_rec_07 WHERE weiboId=1934676487; -- rows=18, type=ref, 0.00 sec -- 單獨 type:極低選擇性 EXPLAIN SELECT COUNT(*) FROM user_rec_07 WHERE type=5; -- rows=114,834, type=ref, 0.19 sec
| 查詢條件 | 耗時 | 掃描行數 |
|---|---|---|
| weiboId 單列 | 0 ms | ~18 行 |
| type 單列 | 190 ms | ~11.5 萬行 |
| 雙列 AND (Index Merge) | 570 ms | ~數十萬行(交集計算開銷) |
MySQL 官方文檔定義:
index_merge:對多個獨立索引分別進行範圍掃描,將各自結果集的 Row ID 取交集,再回表/覆蓋索引過濾。
本案中:
idx_weiboId → 得到 18 個 Row IDidx_type → 得到 114,834 個 Row ID-- 刪除單列索引,建立 (weiboId, type) 聯合索引 ALTER TABLE user_rec_45 DROP INDEX idx_weiboId, ADD INDEX idx_weiboId_type (weiboId, type);
設計原則:高選擇性列(weiboId)前置,低選擇性列(type)後置,B+Tree 直接定位到極小範圍,無需合併計算。
| 指標 | 優化前 | 優化後 |
|---|---|---|
| 查詢耗時 | 570 ms | 0 ms |
| CPU 使用率 | 96% (usr) | 24% (usr) |
| 磁盤讀取 | 24-27 MB/s | 3.7-4.5 MB/s |
| EXPLAIN type | index_merge | ref |
| EXPLAIN key | idx_weiboId,idx_type | idx_weiboId_type |
| EXPLAIN rows | 1 (但實際掃描大量 Row ID) | 1 (真實僅掃描 1 行) |
若業務絕無單獨 WHERE type=5 查詢,DROP INDEX idx_type 同樣能讓優化器退回 idx_weiboId 单列索引,效果相同且節省索引空間。但聯合索引 (weiboId, type) 同時服務:
WHERE weiboId=... AND type=...(最優)WHERE weiboId=...(最左前綴匹配,同樣優)WHERE type=...(無法使用,但業務無此查詢)故聯合索引為「無損增強」,優於單純刪除。
若無法 DDL(如第三方系統表),可會話/全局關閉:
-- 會話級(立即生效,斷開失效) SET SESSION optimizer_switch='index_merge_intersection=off'; -- 全局級(持久化需寫入 my.cnf) SET GLOBAL optimizer_switch='index_merge_intersection=off';
關閉後優化器將不再考慮 Intersection 策略,強制走單列索引或全表掃描,本案中會自動選擇 idx_weiboId,同樣達到 0ms。
(高選擇性列, 低選擇性列) 覆蓋 WHERE 所有等值條件,徹底規避 Index Merge。index_merge 顯示 rows=1 是交集後預估,實際掃描量可能達十萬級。index_merge 典型特徵是 高 CPU(用戶態)、高磁盤讀、低網絡,區別於鎖等待(高 IO wait)或網絡瓶頸。index_merge_intersection 為低成本緩解方案。optimizer_switch 完整參數列表:SHOW VARIABLES LIKE 'optimizer_switch';