Explain 顯示 Count(*) 使用了索引,實際卻是全表掃描
這篇文章依然源于一位讀者的提問:explain 顯示 count(*) 使用了索引,optimizer trace 卻顯示為全表掃描,這是為什么?
還記得當(dāng)時調(diào)試源碼的過程中,如果 explain 顯示會使用二級索引進(jìn)行全索引掃描,執(zhí)行時也確實只會從二級索引中讀取記錄,不會進(jìn)行全表掃描。
不過,那會沒有關(guān)注過 optimizer trace 是怎么顯示的。
既然不能從記憶里找到答案,那就只能從源碼里找答案了。
擼完源碼發(fā)現(xiàn):和 5.7.35 版本相比,8.0.32 的 count(*) 實現(xiàn)邏輯,確實有了一些變化。
接下來,我們一起來看看。
本文基于 MySQL 8.0.32 源碼,存儲引擎為 InnoDB。如需轉(zhuǎn)載,請聯(lián)系『一樹一溪』公眾號作者。
1、準(zhǔn)備工作
創(chuàng)建測試表:
CREATE TABLE `t1` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`i1` int DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
插入數(shù)據(jù):
INSERT INTO `t1`(`id`, `i1`)
VALUES (10, 101), (20, 201), (30, 301);
2、問題重現(xiàn)及分析
explain 查看執(zhí)行計劃:
EXPLAIN SELECT COUNT(*) FROM `t1`;
結(jié)果如下(只截取了部分字段):
圖片
再來看看 optimizer trace 描述的執(zhí)行計劃,依次執(zhí)行以下 3 條 SQL:
-- 開啟 optimizer trace
SET optimizer_trace = "enabled=on";
-- 執(zhí)行 SELECT 語句
SELECT COUNT(*) FROM `t1`;
-- 獲取 optimizer trace
SELECT * FROM information_schema.optimizer_trace;
結(jié)果如下(只截取了部分內(nèi)容):
{
"considered_execution_plans": [
{
"plan_prefix": [
],
"table": "`t1`",
"best_access_path": {
"considered_access_paths": [
{
"rows_to_scan": 3,
"access_type": "scan",
"resulting_rows": 3,
"cost": 0.55,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 3,
"cost_for_plan": 0.55,
"chosen": true
}
]
}
我們來對比下 explain 和 optimizer trace 的結(jié)果:
- explain 輸出結(jié)果中,type 字段值為 index、key 字段值為 idx_i1,表示會使用 idx_i1 作為覆蓋索引執(zhí)行 select 語句。由于沒有 where 條件,select 語句會對二級索引 idx_i1 進(jìn)行全索引掃描,以獲取 t1 表的記錄數(shù)量。
- optimizer trace 輸出結(jié)果中,沒有顯示會使用索引 idx_i1,而且,access_type 為 scan,看起來像是會進(jìn)行全表掃描。
我在 5.7.35 中調(diào)試了這條 SQL:
SELECT COUNT(*) FROM `t1`
可以證實,select 語句執(zhí)行過程中,確實對 idx_i1 進(jìn)行了全索引掃描,和 explain 輸出的執(zhí)行計劃一致。
同時也確認(rèn)了:不管是對主鍵索引進(jìn)行全索引掃描(也就是全表掃描),還是對二級索引進(jìn)行全索引掃描,optimizer trace 的輸出結(jié)果中,access_type 字段值都是 scan。
我又在 8.0.32 中調(diào)試了上面的 SQL,發(fā)現(xiàn)了新情況:InnoDB 對不包含 where 條件的 select count(*) from table 語句進(jìn)行了特殊處理。
跟隨調(diào)試過程,我們一起來看看 InnoDB 做了什么特殊處理。
程序執(zhí)行到 ha_records() 方法時,我們可以看到,index 參數(shù)的值是 1,這就是執(zhí)行計劃確定要使用的索引 ID。
圖片
我們在調(diào)試控制臺打印索引名字,可以看到 ID = 1 的索引就 idx_i1:
圖片
ha_records() 會調(diào)用 records_from_index(),代碼如下:
圖片
從以上代碼可以看到,ha_records() 把索引 idx_i1 的 ID 傳給了 records_from_index() 的第 2 個參數(shù),但是,該方法的第 2 個參數(shù),只有類型(uint),沒有名字,這說明第 2 個參數(shù)不能被使用。
也就是說,雖然執(zhí)行計劃確定了要使用索引 idx_i1 來統(tǒng)計 t1 表的記錄數(shù)量,records_from_index() 卻沒有真正使用 idx_i1。
從代碼注釋也可以看到:在實現(xiàn)二級索引的并行掃描之前,records_from_index() 會強制使用主鍵索引來統(tǒng)計表中的記錄數(shù)量。
在 github 中追溯代碼提交歷史,發(fā)現(xiàn) records_from_index() 是 8.0.17 版本新加的。
從這個版本開始,到最新的 8.0.33,對于不包含 where 條件的 select count(*) from table 語句,都會強制使用主鍵索引(也就是會進(jìn)行全表掃描)。
之所以這么做,是為了使用多個線程對主鍵索引進(jìn)行并行掃描,以提升執(zhí)行速度。
3、總結(jié)
雖然本文內(nèi)容比較短,但是本著完整的原則,還是進(jìn)行個簡單的總結(jié):
- 8.0.16(含)版本之前,對于 select count(*) from table 語句,如果表中有二級索引,InnoDB 會選擇對某個二級索引進(jìn)行全索引掃描,以獲取表中的記錄數(shù)量。
- 從 8.0.17(含)版本開始,直到目前的最新版本(8.0.33),如果表中有二級索引,explain 輸出的執(zhí)行計劃也表示會使用二級索引,然而,實際執(zhí)行過程中,InnoDB 卻會強制進(jìn)行全表掃描,以使用主鍵索引的并行掃描能力。
- optimizer trace 的結(jié)果中,對于 select count(*) from table 語句,二級索引的全索引掃描和全表掃描同等對待,執(zhí)行計劃的 access_type 字段值都是 scan。