這兩天實習項目遇到一個網頁加載巨慢的問題(10多秒),然后定位到是一個MySQL查詢特別慢的語句引起的:
SELECT *
FROM (
SELECT DISTINCT t.vc_date, t.c_bankno, t.vc_bankacco, t.vc_moneytype, t.en_totalbala
, t.en_usablebala, t1.vc_nameinbank, date_format(t.D_IMPORTTIME, '%Y-%m-%d %H:%i:%S') AS D_IMPORTTIME
, t.vc_fundcode, t.c_datamode, t.vc_taskid, t.id, t.vc_projectname
, t.vc_projectcode, t1.c_accotype
, (
SELECT IF(vc_occurtime IS NULL, DATE_FORMAT(vc_occurdate, '%Y-%m-%d'), DATE_FORMAT(CONCAT(vc_occurdate, vc_occurtime), '%Y-%m-%d %H:%i:%S')) AS tradeTime
FROM tbanktradedetail_view
WHERE vc_bankacco = t.vc_bankacco
) AS d_tradetime, t3.vc_entry_caption AS C_ACCOTYPE_STR, t5.vc_entry_caption AS VC_BANKNAME, t4.vc_entry_caption AS VC_MONEYTYPE_STR
FROM tbankaccobala t
INNER JOIN tbankaccoinfo t1 ON t.vc_bankacco = t1.vc_bankacco
INNER JOIN (
SELECT vc_entry_value, vc_entry_caption
FROM ot_dic_tdictionaryentry
WHERE vc_entry_no = '5087'
) t3
ON t1.c_accotype = t3.vc_entry_value
INNER JOIN (
SELECT vc_entry_value, vc_entry_caption
FROM ot_dic_tdictionaryentry
WHERE vc_entry_no = '1004'
) t4
ON t1.VC_MONEYTYPE = t4.vc_entry_value
INNER JOIN (
SELECT vc_entry_value, vc_entry_caption
FROM ot_dic_tdictionaryentry
WHERE vc_entry_no = '1014'
) t5
ON t.c_bankno = t5.vc_entry_value
WHERE 1 = 1
AND t.id IN (
-- this query will take 4.6s:
-- SELECT SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY d_importtime DESC), ',', 1)
-- FROM tbankaccobala
-- GROUP BY vc_bankacco
-- but the following query only takes 1.1s:
SELECT hhhh from(
SELECT SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY d_importtime DESC), ',', 1) as hhhh
FROM tbankaccobala
GROUP BY vc_bankacco
) as sbstr
-- 對IN的子查詢做二次查詢
)
) t
WHERE 1 = 1
ORDER BY t.D_IMPORTTIME DESC
抽出查詢慢關鍵部分:
SELECT *
FROM (
SELECT DISTINCT t.vc_date, t.c_bankno, t.vc_bankacco, t.vc_moneytype, t.en_totalbala
-- 此處省略選擇多個列語句
FROM tbankaccobala t
-- 此處省略多張表連表查詢語句
WHERE 1 = 1
AND t.id IN (
-- 這個查詢需要3s:
SELECT SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY d_importtime DESC), ',', 1)
FROM tbankaccobala
GROUP BY vc_bankacco
)
) t
這個語句導致前端頁面10多秒才有響應(但MySQL執行顯示要4.6秒,phpMyAdmin也是10秒左右響應,為何?)
IN子查詢語句優化
把IN語句里面的內容改成下面這樣,只在外層再加一個select,就把3s的查詢縮短為0.006s:
SELECT hhhh from(
SELECT SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY d_importtime DESC), ',', 1) as hhhh
FROM tbankaccobala
GROUP BY vc_bankacco
) as sbstr
-- 對IN的子查詢做二次select,或者把IN改為JOIN都可以解決速度奇慢的問題
原語句空行處省略了一系列的其他表和 INNER JOIN
語句。一開始懷疑是多表的JOIN操作導致速度變慢,但刪去JOIN變成上面這段注釋掉的語句之后,速度依然非常慢,顯示要3s,於是猜測 IN 才是導致速度變慢的主要因素,改后只要0.006s,嘖…
EXPLAIN 未優化的語句:
(相關子查詢是使用外部查詢中的值的子查詢)
EXPLAIN 優化的語句:
我的理解:優化前,子查詢是相關子查詢,對於外部產生的每個值,都要執行一次子查詢;優化后,子查詢不再是相關子查詢,只需要執行一次子查詢並緩存中間結果,外部查到的每個值去緩存的中間結果里比對一下就行了。
(有人說是能不能用索引的原因——這么說應該是不對的)
完整查詢的后端響應速度對比:
前:
后:
索引優化
對於這么小的數據規模,時間還是太長了… 看前面explain執行計划的截圖,嗯,沒有索引…
給t1的vc_bankacco加上索引之后
解釋執行計划:
查詢和網頁響應用時大幅縮短:
再看sql里還有三個join:
用的都是ot_dic_tdictionaryentry
這張表的t4.vc_entry_value
字段,那么試着給這個字段也加上索引吧,然后用時如下:
是的,時間反而變長了!
explain執行計划:
所以變慢原因是:
沒加vc_entry_value的索引時,會先用vc_entry_no選出一個數量很小的表,再和t1做join,
而加了vc_entry_value的索引之后,MySQL就把這個索引用了起來,join語句被優化為先FirstMatch(ot_dic_tdictionaryentry)
,這產生了一個1713*1713=2934369行的中間結果(笛卡爾乘積),然后才使用vc_entry_no進行where過濾。
所以索引不能亂加啊,加錯了反而會導致性能下降!這個示例里的查詢要加索引只能在vc_entry_no上索引,而不能在vc_entry_value上!
這個示例中主要提升是IN子查詢語句的優化。在使用索引的情況下,對IN子查詢做優化前后的查詢時間分別是3.1s和0.16s