記一次 MySQL 的慢查優化

2021-09-19 10:58:26 字數 3708 閱讀 1685

最近遇見乙個 mysql 的慢查問題,於是排查了下,這裡把相關的過程做個總結。

我首先檢視了 mysql 的慢查詢日誌,發現有這樣一條 query 耗時非常長(大概在 1 秒多),而且掃瞄的行數很大(10 多萬條資料,差不多是全表了):

select * from tgdemand_demand t1

where

( t1.id in

(select t2.demand_id

from tgdemand_job t2

where (t2.state = 'working' and t2.wangwang = 'abc')

)and

not (t1.state = 'needconfirm')

)order by t1.create_date desc

這個查詢不是很複雜,首先執行乙個子查詢,取到任務的狀態(state)是 'working' 並且任務的關聯人 (wangwang)是'abc'的所有需求 id(這個設計師進行中的任務對應的需求 id),然後再到主表tgdemand_demand中帶入剛才的 id 集合,查詢出需求狀態(state)不是 'needconfirm' 的所有需求,最後進行乙個排序。

按道理子查詢篩選出 id 後到主表過濾是直接使用到主鍵,應該是很快的啊。而且,我檢查了子查詢的 tgdemand_job 表的索引,where 中用到的查詢條件都已經增加了索引。怎麼會這樣呢?

於是,我對這個 query 執行了乙個 explain(輸出 sql 語句的執行計畫),看看 mysql 的執行計畫是怎樣的。輸出如下:

我們看到,第一行是 t1 表,type 是 all(全表掃瞄),rows(影響行數)是 157089,沒有用到任何索引;第二行是 t2 表,用到了索引。和我之前理解的執行順序完全不一樣!

為什麼 mysql 不是先執行子查詢,而是對 t1 表進行了全表掃瞄呢?我們仔細看第二行的 select_type,發現它的值是 dependent_subquery,意思是這個子查詢的查詢方式依賴外層的查詢。這是什麼意思?

實際上,mysql 對於這種子查詢會進行改寫,上面的 sql 會被改寫成下面的形式:

select * from tgdemand_demand t1 where exists (

select * from tgdemand_job t2 where t1.id = t2.demand_id and (t2.state = 'working' and t2.wangwang = 'abc')

) and not (t1.state = 'needconfirm')

order by t1.create_date desc;

這表示,sql 會去掃瞄 tgdemand_demand 表的所有資料,每條資料再傳入到子查詢中與表 tgdemand_job 進行關聯,執行子查詢,子查詢根本不會先執行,而且子查詢會執行 157089 次(外層表的記錄數量)。還好我們的子查詢加了必要的索引,不然結果會更加慘不忍睹。

這個結果真是太坑爹,而且十分違反直覺。對於慢查詢,千萬不要想當然,還是多多 explain,看看資料庫實際上是怎麼去執行的。

既然子查詢會被改寫,那最簡單的解決方案就是不用子查詢,將內層獲取需求 id 的 sql 單獨拿出來執行,取到結果後再執行一條 sql 去獲取實際的資料。大概像這樣(下面的語句是不合法的,只是示意):

ids = select t2.demand_id

from tgdemand_job t2

where (t2.state = 'working' and t2.wangwang = 'abc');

select * from tgdemand_demand t1

where

( t1.id in ids

andnot (t1.state = 'needconfirm')

)order by t1.create_date desc;

說幹咱就幹,我找到了下面的**(是 python 語言寫的):

demand_ids = job.objects.filter(wangwang=user['wangwang'], state='working').values_list("demand_id", flat=true)

demands = demand.objects.filter(id__in=demand_ids).exclude(state__in=['needconfirm']).order_by('-create_date')

咦!這不是和我想得是一樣的嘛?先查出需求 id(**第一行),然後用 id 集合再去執行實際的查詢(**第二行)。為什麼經過 orm 框架的處理後產出的 sql 就不一樣了呢?

帶著這個問題我搜尋了一番。原來 django 自帶的 orm 框架生成的 queryset 是懶執行的(lazy evaluated),我們可以將這種 queryset 到處傳,直到需要時才會實際的執行 sql。

比如,我們**裡面的job.objects.filter(wangwang=user['wangwang'], state='working').values_list("demand_id", flat=true)這個 queryset 實際上並沒有執行,就被作為引數傳遞給了id__in,當demand.objects.filter(id__in=demand_ids).exclude(state__in=['needconfirm']).order_by('-create_date')這個 queryset 執行時,剛才未執行的 queryset 才開始作為 sql 執行,於是生成了最開始的 sql 語句。

既然如此,我們的目的要讓 queryset 提前執行,獲得結果集。根據文件,對 queryset 進行迴圈、slice、取 len、list 轉換的時候被執行。於是我將**更改為了下面的樣子:

demand_ids = list(job.objects.filter(wangwang=user['wangwang'], state='working').values_list("demand_id", flat=true))

demands = demand.objects.filter(id__in=demand_ids).exclude(state__in=['needconfirm']).order_by('-create_date')

終於,頁面開啟速度恢復正常了。

實際上,我們也可以對 sql 進行改寫來解決問題:

select * from tgdemand_demand t1, (select t.demand_id from tgdemand_job t where t.state = 'working' and t.wangwang = 'abc') t2

where t1.id=t2.demand_id and not (t1.state = 'needconfirm')

order by t1.create_date desc

思路是去掉子查詢,換用 2 個表進行 join 的方式來取得資料。這裡就不展開了。

框架可以提高生產率的前提是對背後的原理足夠了解,不然應用很可能就會在某個時間暴露出一些隱蔽的要命問題(這些問題在小規模階段可能根本都發現不了......)。保證應用的健壯真是個大學問,還有很多東西值得我們去探索。

記一次MySQL索引優化

兩張表是主 check drawings 從 check drawings img 關係。check drawings,主表資料 3591條。select count from check drawings 3591 check drawings img,從表資料107203條,資料量並不大,從表通...

記一次SQL優化

問題發生在關聯主表a 4w資料量 和副表b 4w資料量 關聯欄位都是openid 當時用的是 left join 直接跑sql,卡死 伺服器也是差 優化1 改left join 為join,兩者區別就是left join查詢時已主表為依據,該是幾條就幾條 就算副表沒有關聯的資料 join如果副表沒有...

記一次慢查詢引發的事故

首先,測試環境上線新版本,並且通過黑盒測試以及功能測試。然後,我們就上線了新的版本。但是在執行3天後,整個伺服器大部分介面都失效了,基本上都是timeout。檢查伺服器情況 cpu基本上佔滿了。接著查了資料庫狀態,通過mysql命令show processlist 存在大量的waiting for ...