一、項目實例問題
1、問題背景
某個需求做了之后,注意到有個接口返回數據特別慢,特別是使用下面的 3 個字段排序時就直接卡死,肯定是 sql 性能寫法問題,所以決定研究一下查看究竟。
其實需求挺簡單,有幾個字段排序,前端需要展示那些字段,然后之前的后端寫的 sql 如下,僅提取主要問題點,其實就是需要拿到 starCount、commentCount、totalReward 用來前端展示,而這三個字段呢,又需要從另外三個表里去分別計數,所以不考慮 sql 性能優化的話,就很容易想到了這種錯誤的寫法。
k.tags, v.views, (select coalesce(count(rid),0) from table1 where aa = 'kl' and rid = k.id) starCount, (select coalesce(count(id),0) from table2 where aa = 'kl' and rid = k.id::varchar) commentCount, (select coalesce(count(id),0) from table3 where aa = 'kl' and rid = k.id::varchar) totalReward from table4 k left join table5 v on k.id = v.kl_id
2、優化方案
主要優化后的 sql 如下:使用 left join 替代標量子查詢
k.tags, v.views, coalesce (s.count,0) starCount, coalesce (m.count,0) commentCount, coalesce (p.count,0) totalReward from table4 k left join table5 v on k.id = v.kl_id left join (select rid,count(rid) from table1 where aa = 'kl' group by rid) s on k.id = s.rid left join (select rid,count(rid) from table2 where aa = 'kl' group by rid) m on m.rid = k.id::varchar
left join (select rid,count(rid) from table3 where aa = 'kl' group by rid) p on p.rid = k.id::varchar
order by totalReward desc
優化前比如我有10萬篇文章,那就要執行10萬次(select coalesce(count(rid),0) from table1 where aa = 'kl' and rid = k.id) starCount。
優化后,僅需一次兩表之間的匹配,即使是全表也是1次匹配,分組后也是1次匹配,數據量少是會提高效率但是頂多0.00幾的提高,關鍵是left join。提高了n倍之前order by直接執行失敗time out,優化之后是0.4s左右。
3、分析原因 - 為什么會想到錯誤的寫法
以前我確實很少看到第一種那種標量子查詢的寫法,所以很納悶為什么會這樣寫。一般不都是用 left join 嗎?后來了解到可能情況不一樣:
(1)平常我們使用多表關聯都會想到 left join,因為我們會用到關聯表的多個字段或某個字段,需要將其查出來,所以很容易想到 left join。
(2)而這種情況只需要使用其他表的一個計數的值,沒有使用表里的任何字段,沒學過 sql 優化的,很難想到用 left join。
而很多人使用標量子查詢而不自知執行效率差,往往是因為數據量比較小,並沒有發現不妥,一旦數據量大了之后,就會越來越慢。只有經過大數據量的考驗,才能寫出來優質的 sql。
墨天輪平台有個標量子查詢的優化案例可以看下:Oracle 標量子查詢優化案例 — https://www.modb.pro/db/41963
二、標量子查詢的問題
標量子查詢、聚合標量子查詢、行轉列標量子查詢、帶top的標量子查詢如何轉成left join。
之所以要轉換,主要是因為標量子查詢雖然寫法上比較直觀,容易理解,不用想就知道怎么寫,但是存在:代碼重復、多次訪問同一個表 問題,所以效率比較低。
1、標量子查詢的模板
按標量子查詢方式,寫出來的sql,都類似下面的代碼:
select tb.col1, tb.col2, --下面的代碼是重復的,表和連接條件都類似,只是最后顯示的字段不同
(select x1 from t where t.id = tb.id) as x1, (select x2 from t where t.id = tb.id) as x2, (select x3 from t where t.id = tb.id) as x3, (select x4 from t where t.id = tb.id) as x4, ... from tb
可以看到,其中x1、x2、x3、x4等列,大部分代碼都是重復的。當然,代碼重復本身並沒有太大的問題,最多就是復制粘貼,拷貝多次,然后把字段名改改,就行了。
2、標量子查詢的執行過程
上面的sql經過sql server的優化,生成執行計划,執行過程類似如下的過程:
(1)從tb表中取一條數據,用其中的id值,第1次和t表中的id值進行比較,如果相等,就返回t表的x1字段的值。
(2)從tb表中取一條數據,用其中的id值,第2次和t表中的id值進行比較,如果相等,就返回t表的x2字段的值。
(3)從tb表中取一條數據,用其中的id值,第3次和t表中的id值進行比較,如果相等,就返回t表的x3字段的值。
(4)從tb表中取一條數據,用其中的id值,第4次和t表中的id值進行比較,如果相等,就返回t表的x4字段的值。
(5)按照上述過程遍歷整個tb表的每一條數據。
從上面的過程可以看出,一共訪問了t表4次,做了很多無用功。
如果改成left join的方式,只需要訪問1次t表,少訪問3次,效率提高不少。
所以,要盡量少用標量子查詢的寫法。