加了?limit?1?查詢竟然變慢了的原因分析及解決辦法
寫 SQL 的時(shí)候,大家都有個(gè)肌肉記憶:如果只需要一條數(shù)據(jù),一定要加上 LIMIT 1。
這聽起來非常合理,畢竟數(shù)據(jù)庫只要找到一條滿足條件的記錄,就可以收工回家,不用再把剩下的幾百萬行數(shù)據(jù)掃一遍,既省 IO 又省 CPU。
但前兩天排查線上問題時(shí),我遇到了一個(gè)非常有意思的案例:加了 LIMIT 1,查詢反而慢了 50 倍。
這就好比你為了抄近道走了一條小路,結(jié)果發(fā)現(xiàn)這條路堵得水泄不通,比走大路還慢。
1. 還原一下現(xiàn)場
業(yè)務(wù)場景很簡單:我們要查某個(gè)用戶最近的一筆“處理中”的訂單。
訂單表 orders 大概有 500 萬數(shù)據(jù),表里有兩個(gè)關(guān)鍵索引:
- idx_user_status
(user_id, status):用來根據(jù)用戶和狀態(tài)過濾數(shù)據(jù)。 - idx_create_time
(create_time):用來按時(shí)間排序。
代碼里的 SQL 是這么寫的:
SELECT id, order_no, amount FROM orders WHERE user_id = 10086 AND status = 1 ORDER BY create_time DESC LIMIT 1;
這條 SQL 上線后,直接觸發(fā)了慢查詢報(bào)警,耗時(shí)飆到了 2.5 秒。
為了搞清楚原因,我試著把 LIMIT 1 去掉,裸跑了一次:
SELECT id, order_no, amount FROM orders WHERE user_id = 10086 AND status = 1 ORDER BY create_time DESC;
結(jié)果只要 50 毫秒。
加了 LIMIT 也就是想省點(diǎn)事,反而變慢了呢?
2. Explain 分析
遇到這種詭異的事,第一反應(yīng)肯定是看執(zhí)行計(jì)劃。對(duì)比了一下兩條 SQL 的 EXPLAIN 結(jié)果,真相立刻浮出水面:
- 沒加 LIMIT時(shí): MySQL 選擇了
idx_user_status索引。它先精準(zhǔn)地把這個(gè)用戶狀態(tài)為 1 的訂單找出來(只有幾十條),然后在內(nèi)存里排個(gè)序。因?yàn)閿?shù)據(jù)量少,這個(gè)排序幾乎瞬間完成。 - 加了 LIMIT 后: MySQL 居然放棄了精準(zhǔn)過濾,改用了
idx_create_time索引。 它的思路變成了,按時(shí)間倒序掃描全表,一邊掃一邊檢查這是不是該用戶的訂單。
為什么優(yōu)化器會(huì)覺得第二種方案更好?
這里我們得站在 MySQL 優(yōu)化器的角度想一想。它在做決策時(shí),其實(shí)是在做一道算術(shù)題:
- 方案 A 走過濾索引:先把符合條件的數(shù)據(jù)全找出來,再排序。 缺點(diǎn):如果符合條件的數(shù)據(jù)很多,排序成本會(huì)很高。
- 方案 B 走時(shí)間索引 + LIMIT:既然你只要 1 條數(shù)據(jù),而且要求按時(shí)間倒序,那我就順著時(shí)間索引往回找。優(yōu)點(diǎn):天然有序,不用再排序了。
優(yōu)化器其實(shí)在賭,優(yōu)化器覺得,運(yùn)氣只要不是太差,應(yīng)該很快就能碰到一條滿足 user_id 和 status 的記錄。
問題就出在這個(gè)賭注上。
在這個(gè)案例里,用戶 10086 是個(gè)老用戶,他最近的一筆“處理中”的訂單,其實(shí)是一年前下的。
于是,MySQL 順著時(shí)間索引,從今天的數(shù)據(jù)開始往回掃,掃了昨天、上周、上個(gè)月……一直掃了 200 多萬行數(shù)據(jù),才終于在去年的數(shù)據(jù)里找到了那條記錄。
這就是為什么加了 LIMIT 1 反而變成了全表掃描級(jí)別的慢查詢。
3. 怎么解決?
既然知道了是優(yōu)化器選錯(cuò)路了,那我們的思路就是幫它糾正過來。
方法一:簡單粗暴 FORCE INDEX
既然優(yōu)化器甚至不清楚,那我們就直接教它做事。
SELECT ... FROM orders FORCE INDEX (idx_user_status) ...
這就相當(dāng)于在導(dǎo)航里強(qiáng)制選定路線。優(yōu)點(diǎn)是立竿見影,缺點(diǎn)是代碼不夠優(yōu)雅,如果以后索引名改了,這行代碼會(huì)報(bào)錯(cuò)。
方法二:最穩(wěn)妥的聯(lián)合索引**
優(yōu)化器之所以糾結(jié),是因?yàn)楝F(xiàn)有的索引沒法同時(shí)滿足“過濾”和“排序”。
我們可以建一個(gè)聯(lián)合索引:(user_id, status, create_time)。
在這個(gè)索引里,數(shù)據(jù)先按用戶和狀態(tài)聚在一起,內(nèi)部再按時(shí)間排序。MySQL 只要用這個(gè)索引,既能精準(zhǔn)定位,又不用額外排序,這才是最完美的解法。
方法三:子查詢的小技巧
如果你不想改表結(jié)構(gòu),還有一個(gè)巧妙的寫法:
SELECT * FROM (
SELECT ... FROM orders
WHERE user_id = 10086 AND status = 1
ORDER BY create_time DESC
) AS tmp
LIMIT 1;我們用一個(gè)子查詢先把數(shù)據(jù)找出來,這時(shí)候 MySQL 會(huì)乖乖走過濾索引,然后再在外層取 LIMIT 1。這就相當(dāng)于人為地切斷了 LIMIT 對(duì)內(nèi)層索引選擇的干擾。
寫在最后
LIMIT 1 確實(shí)是個(gè)好習(xí)慣,但也要看場景。
在這個(gè)案例里,MySQL 的優(yōu)化器因?yàn)檫^度自信,覺得“很快就能找到這一條”,結(jié)果在數(shù)據(jù)分布不均勻的情況下翻了車。
下次如果再遇到加了限制反而變慢的問題,直接用 EXPLAIN 看看,因?yàn)閮?yōu)化器有時(shí)候也是會(huì)做出抽風(fēng)的事!
到此這篇關(guān)于加了limit 1查詢竟然變慢了的原因分析及解決辦法的文章就介紹到這了,更多相關(guān)limit1查詢變慢解決內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Mysql搭建主從復(fù)制功能的步驟實(shí)現(xiàn)
這篇文章主要介紹了關(guān)于Mysql搭建主從復(fù)制功能的步驟實(shí)現(xiàn),在實(shí)際的生產(chǎn)中,為了解決Mysql的單點(diǎn)故障已經(jīng)提高M(jìn)ySQL的整體服務(wù)性能,一般都會(huì)采用主從復(fù)制,需要的朋友可以參考下2023-05-05
mysql中union和union?all的使用及注意事項(xiàng)
這篇文章主要給大家介紹了關(guān)于mysql中union和union?all的使用及注意事項(xiàng)的相關(guān)資料,需要的朋友可以參考下2022-08-08
windows10下mysql 8.0 下載與安裝配置圖文教程
這篇文章主要介紹了windows10下mysql 8.0 下載與安裝配置圖文教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
SQL Server 完整備份遇到的一個(gè)不常見的錯(cuò)誤及解決方法
這篇文章給大家介紹了SQL Server 完整備份遇到的一個(gè)不常見的錯(cuò)誤及解決方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-05-05
Rsyslog + MySQL 實(shí)現(xiàn)日志集中存儲(chǔ)及常見問題排查
文章介紹了如何使用rsyslog和MySQL實(shí)現(xiàn)Linux系統(tǒng)日志的集中存儲(chǔ)和統(tǒng)一查詢,它詳細(xì)描述了環(huán)境前置要求、MySQL端配置、rsyslog核心配置、服務(wù)重啟與驗(yàn)證、高級(jí)配置以及常見問題排查,感興趣的朋友跟隨小編一起看看吧2025-12-12

