這些經常被忽視的SQL錯誤用法,你有沒有踩過坑?

章為忠發表於2021-09-01

之前已經講過mysql的效能優化,感興趣的朋友可以看看之前的文章。但是有些問題其實是我們自身的SQL語句有問題導致的。今天就來總結哪些經常被我們忽視的SQL錯誤寫法,看看你都踩過哪些坑?

 

一、LIMIT語句

Limit是分頁查詢是最常用的場景之一,但也通常也是最容易出問題的地方。比如對於下面簡單的語句,一般我們覺得在type, name, create_time欄位上加組合索引。這樣條件排序都能有效的利用到索引,效能迅速提升。

SELECT * 
FROM   operation 
WHERE  type = 'xxx' 
       AND name = 'xxx' 
ORDER  BY create_time 
LIMIT  1000, 10; 

但是當資料量很大的時候,當我們查詢最後幾頁資料時,分頁會越來越慢。這就是我們經常碰到的海量資料的分頁問題。這是為什麼呢?

優化方案

因為資料庫也並不知道第1000000條記錄從什麼地方開始,即使有索引也需要從頭計算一次,即進行全表掃描。出現這種效能問題,主要還是我們沒有考慮到大量資料的情況。

其實在前端資料瀏覽翻頁時,是可以將上一頁的最大值作為查詢條件傳給後臺的。SQL 重新設計如下:

select *
from operation
where id>1000000
AND type = 'xxx'
AND name = 'xxx'
ORDER BY create_time
limit 10

經過這種優化,可以保證系統不會隨著資料量的增長而變慢。                                                                                                                                                                                                  

 

二、隱式轉換

SQL語句中查詢變數和欄位定義型別不匹配是另一個常見的錯誤。比如下面的語句:

explain extended
select * 
from my_balance b
where b.bpn = 14000000123
and b.isverified is null;

欄位 bpn 的定義為 varchar 型別,而查詢條件傳入的卻是int 型別。MySQL 會將字串轉換為數字之後再比較。函式作用於表欄位,導致所以索引失效。如下圖所示:

這個坑我們以前也遇見過,花了好半天才發現是這個問題。 所以程式設計師在開發的過程中,一定要認真仔細,確保查詢變數和欄位型別匹配。

優化方案

保證傳入的引數型別和欄位定義的型別一致。

所以,上面的sql語句改為如下即可:

explain extended
select * 
from my_balance b
where b.bpn = '14000000123'
and b.isverified is null;

 

三、關聯更新、刪除

MySQL5.6之後有個新特性,會自動把SQL語句中的巢狀子查詢優化為關聯查詢(join),所以有些時候你會發現巢狀子查詢的效率和關聯查詢的效率差不多。但是需要特別注意mysql目前僅僅針對查詢語句的優化。對於更新或刪除需要手工重寫成 JOIN。

比如下面 UPDATE 語句,MySQL 實際執行的還是巢狀子查詢(DEPENDENT SUBQUERY),其執行時間可想而知。

explain extended
UPDATE operation o SET status
= 'applying' WHERE o.id IN (SELECT id FROM (SELECT o.id,o.status FROM operation o WHERE o.group = 123 AND o.status NOT IN ('done') ORDER BY o.parent,o.id LIMIT 1) t);

執行計劃:

 

優化方案

改為 JOIN 之後,子查詢的選擇模式從巢狀子查詢(DEPENDENT SUBQUERY) 變成了關聯查詢(DERIVED),執行速度大大加快

UPDATE operation o
   JOIN (SELECT o.id,
                o.status
         FROM operation o 
         WHERE o.group = 123
               AND o.status NOT IN ('done')
               ORDER BY o.parent,o.id
        LIMIT 1) t 
    ON o.id = t.id 
SET status = 'applying1

執行計劃簡化為:

 

 

四、Order by

MySQL中的兩種排序方式:

1、通過有序索引順序掃描直接返回有序資料,因為索引的結構是B+樹,索引中的資料是按照一定順序進行排列的,所以在排序查詢中如果能利用索引,就能避免額外的排序操作。EXPLAIN分析查詢時,Extra顯示為Using index。

2、Filesort排序,對返回的資料進行排序,所有不是通過索引直接返回排序結果的操作都是Filesort排序,也就是說進行了額外的排序操作。EXPLAIN分析查詢時,Extra顯示為Using filesort。

優化方案

一般排序的原則就是:儘量減少額外的排序,通過索引直接返回有序資料。

所以我們需要注意以下這些情況:

1、排序欄位在多個索引中,無法使用索引排序,查詢一次只能使用一個索引:

explain 
select store_id,email,last_name 
from my_user 
order by store_id,email,last_name;

 查詢計劃顯示,沒有走所以直接返回有序資料,額外排序放回結果:

 

2、排序欄位順序與索引列順序不一致,同樣也無法利用索引排序。這個就不舉例了跟where條件是一樣的。

需要注意的是:這些都是細節的東西,經常會在開發過程中忽略。然後SQL就莫名其妙的不走索引了。

 

五、混合排序

索引中的資料是按照一定順序進行排列的,所以在排序查詢中如果能利用索引直接返回資料,就能避免額外的排序操作。但是如果出現這種混合了升序和降序的情況,MySQL 無法利用索引直接返回排序結果的。

SELECT *
FROM my_order o
     INNER JOIN my_appraise a ON a.orderid = o.id 
ORDER BY a.is_reply ASC,
         a.appraise_time DESC 
LIMIT 0, 20

執行計劃顯示為全表掃描:

 

優化方案

此類問題根據實際常見優化,原則就是應該避免這種排序的情況。如果確實有多種排序的需求,最好使用其他的方法提升效能。

 

六、EXISTS語句

MySQL 對待 EXISTS 子句時,會採用巢狀子查詢的執行方式。如下面的 SQL 語句:

explain 
SELECT *
FROM my_order n
     LEFT JOIN my_appraise sra 
            ON n.id = sra.orderid 
            
WHERE 1=1
      AND EXISTS(SELECT 1
                    FROM my_user m
                    WHERE n.user_id = m.id 
                        AND m.usercode = '111' )
      AND n.id <> 5

執行計劃為:

 

優化方案

去掉 exists 更改為 join,能夠避免巢狀子查詢,這樣會大大提高查詢效率。

SELECT *
FROM my_neighbor n
     LEFT JOIN my_neighbor_apply sra 
            ON n.id = sra.neighbored 
            AND sra.user_id = 'xxx' 
     INNER JOIN message_info m
            on n.id = m.neighbor_id
            AND m.inuser = 'xxx'
WHERE n.topic_status < 4 
        AND n.topictype <> 5

新的執行計劃顯示沒有了巢狀子查詢:

 

 

七、條件下推

外部查詢條件不能夠下推到複雜的檢視或子查詢的情況有:

  • 聚合子查詢;

  • 含有 LIMIT 的子查詢;

  • UNION 或 UNION ALL 子查詢;

  • 輸出欄位中的子查詢;

如下面的語句,從執行計劃可以看出其條件作用於聚合子查詢之後

SELECT *
FROM(SELECT target, 
            Count(*)
     FROM operation
     GROUPBY target) t
WHERE target = 'rm-xxxx'

優化方案

確定從語義上查詢條件可以直接下推後,重寫如下:

SELECT target, 
       Count(*)
FROM operation
WHERE target = 'rm-xxxx'
GROUPBY target

執行計劃變為:

 

 

八、提前縮小資料範圍

先上初始 SQL 語句:

SELECT *
FROM my_order o
    LEFT JOIN my_userinfo u 
            ON o.uid = u.uid 
    LEFT JOIN my_productinfo p 
            ON o.pid = p.pid 
WHERE o.display = 0
      AND o.ostaus = 1
ORDER BY o.selltime DESC 
LIMIT 0, 15

數為90萬,時間消耗為12秒。

 

優化方案

由於最後 WHERE 條件以及排序均針對最左主表,因此可以先對 my_order 排序提前縮小資料量再做左連線。SQL 重寫後如下,執行時間縮小為1毫秒左右。

SELECT *
FROM (SELECT *
        FROM my_order o
        WHERE o.display = 0
            AND o.ostaus = 1  
        ORDER BY o.selltime DESC LIMIT 0, 15 )o
LEFT JOIN my_userinfo u
        ON o.uid = u.uid 
LEFT JOIN my_productinfo p
        ON o.pid = p.pid 
ORDER BY o.selltime DESC 
limit 0, 15

再檢查執行計劃:

 

子查詢物化後(select_type=DERIVED)參與 JOIN,雖然估算行掃描仍然為90萬,但是利用了索引以及 LIMIT 子句後,實際執行時間變得很小。

 

九、中間結果集下推

再來看下面這個已經初步優化過的例子(左連線中的主表優先作用查詢條件):

SELECT a.*
        c.allocated
FROM (
        SELECT resourceid 
        FROM my_distribute d 
        WHERE isdelete = 0 
            AND cusmanagercode = '1234567'
            ORDER BY salecode limit 20
      ) a
LEFT JOIN
        (
            SELECT resourcesid, sum(allocation) allocated 
            FROM my_resources
            GROUP BY resourcesid
        ) c
ON a.resourceid = c.resourcesid

那麼該語句還存在其它問題嗎?不難看出子查詢 c 是全表聚合查詢,在表數量特別大的情況下會導致整個語句的效能下降。

其實對於子查詢 c,左連線最後結果集只關心能和主表 resourceid 能匹配的資料。因此我們可以重寫語句如下,執行時間大大降低 。

SELECT a.*,
        c.allocated
FROM (
        SELECT resourceid
        FROM my一distribute d
        WHERE isdelete = 0
        AND cusmanagercode = '1234567*
        ORDER BY salecode limit 20) a
LEFT JOIN
    (
        SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated 
        FROM my_resources r,
        (
            SELECT resourceid
            FROM my_distribute d
            WHERE isdelete = 0
            AND cusmanagercode = '1234567'
            ORDER BY salecode limit 20
        ) a 
        WHERE r.resourcesid = a.resourcesid 
        GROUP BY resourcesid
    ) c
ON a.resourceid = c.resourcesid

 

最後

以上總結了一些sql語句常見的坑。裡面很多都是不仔細導致的。只有仔細認真儘量考慮一些大資料的情況,這樣才能寫出高效能的SQL語句。

同時,程式設計師在設計資料模型以及編寫SQL語句時,要把索引及效能提前考慮進去,這樣才能避免後期遇到的一些坑。

 

相關文章