不要用 in - 子查詢

你好,我是 yes。

前兩天我的 VIP 用戶向我拋出了一個 SQL 問題,他的 MySQL 是 8.x 版本:

大概意思如下 sql :

select * from A where id in (
  select max(id) as id from A 
   where task_id in(1,2,3) 
    group by task_id
);

這個 A 表中是有 task_id 這個索引的。

一眼看去這個查詢沒啥問題,子查詢裏會利用 task_id 這個索引,然後外面的這個查詢會上 id 這個主鍵索引。

而事實是:

一看 explain ,果然走了全表掃描:

可以看到子查詢用上了 taskId 這個索引,而外部的查詢走的是全表掃描,大概掃描了 890W 的數據,所以查詢花了 1 分 30 秒。

那麼問題來了:明明有主鍵索引不用,mysql 爲什麼要選擇全表?

我也不知道。

MySQL 有個優化器,它會決定最終以怎麼樣的形式、選擇那個索引來生成最終的執行計劃。

對將要執行的 SQL 而言,優化器會有成本模型,它會根據當前表的一些估算值結合當前的 SQL 語句進行打分,比如如果用了索引 A 需要多少 I/O 成本、CPU 成本,如果用索引 B 要多少 I/O、CPU 成本。

總而言之,它有自己的一套規則,會根據估計值預算成本,根據成本最終生成執行計劃。

篇幅有限具體不多介紹,有興趣的話看下官網:https://dev.mysql.com/doc/refman/8.0/en/cost-model.html

既然是預算,那就有可能不準,所以有時候就會產生該走索引卻全表掃描的情況(因爲算出的成本走全表掃描反而更低)。

所以針對上面這種情況,我讓他加了強制用主鍵的操作 force index(PRIMARY),但是並沒有生效,還是走了全表。

可以看到掃描了大約 996w 行數據(表數據有新增,所以從 800w 多變成了 900 多 w)。

我自己估計:大致就是因爲子查詢的結果不確定,MySQL 不知道 in 的值到底有多大,所以保守型的選擇了全表(我猜的,具體的邏輯估計得看源碼,不過最終表現的事實確實如此)。

問題就在 in 的值大小不確定。

通過子查詢,我們自己其實也無法保證 in 的值到底是多少。

所以最後的方案是拋棄 in 的方式,採用 inner join 的方式來改造 sql  實現。

改造 SQL 如下:

select * from A a inner join (
  select max(id) as id from A 
   where task_id in(1,2,3) group by task_id
) b 
on a.id = b.id;

這樣改造以後,進行了一波 explain,結果如下:

查詢結果秒出,可以看下第二行,用上了主鍵索引,全部掃描的行數加起來也就 600 多。

所以我們換了一種方式來使得 MySQL 正常的用上索引且所要的結果是一致的。

這裏也建議,如果 sql 是包含子查詢的 in 查詢,那麼最好換成 join 的方式,因爲我們不能保證 in 的查詢一定會用上索引,萬一來個全表掃描,在表數據量比較大的場景,很容易產生阻塞,多來幾個這樣的阻塞,數據庫連接不可用,服務可能就掛了。

在測試環境看起來沒問題,一到生產就 GG。

所以能不用 in + 子查詢,就不要用,可以用 join 來替代實現。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vUpFB6OTKfZOy6wxAdd79w