<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    8種專坑同事的 SQL 寫法,性能降低100倍,不來看看?

    VSole2023-01-05 09:19:46

    LIMIT 語句

    分頁查詢是最常用的場景之一,但也通常也是最容易出問題的地方。比如對于下面簡單的語句,一般 DBA 想到的辦法是在 type, name, create_time 字段上加組合索引。這樣條件排序都能有效的利用到索引,性能迅速提升。

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

    好吧,可能90%以上的 DBA 解決該問題就到此為止。但當 LIMIT 子句變成 “LIMIT 1000000,10” 時,程序員仍然會抱怨:我只取10條記錄為什么還是慢?

    要知道數據庫也并不知道第1000000條記錄從什么地方開始,即使有索引也需要從頭計算一次。出現這種性能問題,多數情形下是程序員偷懶了。

    在前端數據瀏覽翻頁,或者大數據分批導出等場景下,是可以將上一頁的最大值當成參數作為查詢條件的。SQL 重新設計如下:

    SELECT   * 
    FROM     operation 
    WHERE    type = 'SQLStats' 
    AND      name = 'SlowLog' 
    AND      create_time > '2017-03-16 14:00:00' 
    ORDER BY create_time limit 10;
    

    在新設計下查詢時間基本固定,不會隨著數據量的增長而發生變化。

    隱式轉換

    SQL語句中查詢變量和字段定義類型不匹配是另一個常見的錯誤。比如下面的語句:

    mysql> explain extended SELECT * 
         > FROM my_balance b 
         > WHERE b.bpn = 14000000123 
         > AND b.isverified IS NULL ;
    mysql> show warnings;
    | Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn'
    

    其中字段 bpn 的定義為 varchar(20),MySQL 的策略是將字符串轉換為數字之后再比較。函數作用于表字段,索引失效。

    上述情況可能是應用程序框架自動填入的參數,而不是程序員的原意。現在應用框架很多很繁雜,使用方便的同時也小心它可能給自己挖坑。

    關聯更新、刪除

    雖然 MySQL5.6 引入了物化特性,但需要特別注意它目前僅僅針對查詢語句的優化。對于更新或刪除需要手工重寫成 JOIN。

    比如下面 UPDATE 語句,MySQL 實際執行的是循環/嵌套子查詢(DEPENDENT SUBQUERY),其執行時間可想而知。

    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);
    

    執行計劃:

    +----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
    | 1  | PRIMARY | o | index |               | PRIMARY | 8       | | 24   | Using where; Using temporary |
    | 2 | DEPENDENT SUBQUERY | |       | |         | |       | | Impossible WHERE noticed after reading const tables |
    | 3  | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8       | const | 1    | Using where; Using filesort |
    +----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
    

    重寫為 JOIN 之后,子查詢的選擇模式從 DEPENDENT SUBQUERY 變成 DERIVED,執行速度大大加快,從7秒降低到2毫秒

    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 = 'applying'
    

    執行計劃簡化為:

    +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
    | 1  | PRIMARY |       | |               | |         | |      | Impossible WHERE noticed after reading const tables |
    | 2 | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8 | const | 1 | Using where; Using filesort |
    +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
    

    混合排序

    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
    

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

    +----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra 
    +----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
    | 1 | SIMPLE | a | ALL | idx_orderid | NULL | NULL | NULL | 1967647 | Using filesort |
    |  1 | SIMPLE | o | eq_ref | PRIMARY | PRIMARY | 122     | a.orderid |       1 | NULL |
    +----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+
    

    由于 is_reply 只有0和1兩種狀態,我們按照下面的方法重寫后,執行時間從1.58秒降低

    到2毫秒。

    SELECT * 
    FROM   ((SELECT *
             FROM   my_order o 
                    INNER JOIN my_appraise a 
                            ON a.orderid = o.id 
                               AND is_reply = 0 
             ORDER  BY appraise_time DESC 
             LIMIT  0, 20) 
            UNION ALL 
            (SELECT *
             FROM   my_order o 
                    INNER JOIN my_appraise a 
                            ON a.orderid = o.id 
                               AND is_reply = 1 
             ORDER  BY appraise_time DESC 
             LIMIT  0, 20)) t 
    ORDER  BY  is_reply ASC, 
              appraisetime DESC 
    LIMIT  20;
    

    EXISTS語句

    MySQL 對待 EXISTS 子句時,仍然采用嵌套子查詢的執行方式。如下面的 SQL 語句:

    SELECT *
    FROM   my_neighbor n 
           LEFT JOIN my_neighbor_apply sra 
                  ON n.id = sra.neighbor_id 
                     AND sra.user_id = 'xxx' 
    WHERE  n.topic_status < 4 
           AND EXISTS(SELECT 1 
                      FROM   message_info m 
                      WHERE  n.id = m.neighbor_id 
                             AND m.inuser = 'xxx') 
           AND n.topic_type <> 5
    

    執行計劃為:

    +----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
    |  1 | PRIMARY | n | ALL |  | NULL | NULL | NULL | 1086041 | Using where |
    | 1 | PRIMARY | sra | ref | | idx_user_id | 123 | const | 1 | Using where |
    |  2 | DEPENDENT SUBQUERY | m | ref |  | idx_message_info | 122     | const |       1 | Using index condition; Using where |
    +----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+ 
    

    去掉 exists 更改為 join,能夠避免嵌套子查詢,將執行時間從1.93秒降低為1毫秒。

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

    新的執行計劃:

    +----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
    |  1 | SIMPLE | m | ref | | idx_message_info | 122     | const |    1 | Using index condition |
    | 1 | SIMPLE | n | eq_ref | | PRIMARY | 122 | ighbor_id | 1 | Using where |
    |  1 | SIMPLE | sra | ref | | idx_user_id | 123     | const |    1 | Using where |
    +----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
    

    條件下推

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

    • 聚合子查詢;
    • 含有 LIMIT 的子查詢;
    • UNION 或 UNION ALL 子查詢;
    • 輸出字段中的子查詢;

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

    SELECT * 
    FROM   (SELECT target, 
                   Count(*) 
            FROM   operation 
            GROUP  BY target) t 
    WHERE  target = 'rm-xxxx'
    +----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
    |  1 | PRIMARY |  | ref | 0> |  | 514     | const |    2 | Using where |
    | 2 | DERIVED | operation | index | idx_4 | idx_4 | 519 | NULL | 20 | Using index |
    +----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
    

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

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

    執行計劃變為:

    +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
    | 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index |
    +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+復制代碼
    

    提前縮小范圍

    先上初始 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
    

    該SQL語句原意是:先做一系列的左連接,然后排序取前15條記錄。從執行計劃也可以看出,最后一步估算排序記錄數為90萬,時間消耗為12秒。

    +----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
    |  1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 909119 | Using where; Using temporary; Using filesort |
    | 1 | SIMPLE | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL |
    |  1 | SIMPLE | p | ALL | PRIMARY | NULL | NULL | NULL |      6 | Using where; Using join buffer (Block Nested Loop) |
    +----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
    

    由于最后 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 子句后,實際執行時間變得很小。

    +----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
    +----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
    |  1 | PRIMARY |  | ALL | NULL | NULL | NULL | NULL |     15 | Using temporary; Using filesort |
    | 1 | PRIMARY | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL |
    |  1 | PRIMARY | p | ALL | PRIMARY | NULL | NULL | NULL |      6 | Using where; Using join buffer (Block Nested Loop) |
    | 2 | DERIVED | o | index | NULL | idx_1 | 5 | NULL | 909112 | Using where |
    +----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
    

    中間結果集下推

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

    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 
                       GROUP BY resourcesid) c 
    ON        a.resourceid = c.resourcesid
    

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

    其實對于子查詢 c,左連接最后結果集只關心能和主表 resourceid 能匹配的數據。因此我們可以重寫語句如下,執行時間從原來的2秒下降到2毫秒。

    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
    

    但是子查詢 a 在我們的SQL語句中出現了多次。這種寫法不僅存在額外的開銷,還使得整個語句顯的繁雜。使用 WITH 語句再次重寫:

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

    總結

    數據庫編譯器產生執行計劃,決定著SQL的實際執行方式。但是編譯器只是盡力服務,所有數據庫的編譯器都不是盡善盡美的。

    上述提到的多數場景,在其它數據庫中也存在性能問題。了解數據庫編譯器的特性,才能避規其短處,寫出高性能的SQL語句。

    程序員在設計數據模型以及編寫SQL語句時,要把算法的思想或意識帶進來。

    編寫復雜SQL語句要養成使用 WITH 語句的習慣。簡潔且思路清晰的SQL語句也能減小數據庫的負擔 。

    selectsql優化
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    在開始介紹如何優化sql前,先附上mysql內部邏輯圖讓大家有所了解連接器:?優先在緩存中進行查詢,如果查到了則直接返回,如果緩存中查詢不到,在去數據庫中查詢。
    一、前言 在應用開發的早期,數據量少,開發人員開發功能時更重視功能上的實現,隨著生產數據的增長,很多SQL語句開始暴露出性能問題,對生產的影響也越來越大,有時可能這些有問題的SQL就是整個系統性能的瓶頸。 二、SQL優化一般步驟 1、通過慢查日志等定位那些執行效率較低的SQL語句 2、explain 分析SQL的執行計劃
    背景我負責的系統到2021年初完成了功能上的建設,開始進入到推廣階段。隨著推廣的逐步深入,收到了很多好評的同時也收到了很多對性能的吐槽。作為一個優秀的后端程序員,這個數據肯定是不能忍的,我們馬上就進入了漫長的接口優化之路。
    在面對超級復雜SQL語句時,性能提升尤為明顯,推薦分解為小查詢來進行優化,不過在應用設計時,如果一個查詢能解決問題且不會產生性能問題,這是完全沒問題的。MySQL查詢緩存保存查詢返回的完整結果。當查詢命中該緩存,MySQL會like返回結果,跳過了解析、優化和執行截斷。這是提高查詢性能最有效的方法之一,而且這是被MySQL引擎處理的,通常MySQL默認是不開啟查詢緩存的,需要手動開啟。
    如果關閉了autocommit,所有的sql語句都在一個事務中,直到執行了commit或rollback,該事務結束,并且開啟了下一個事務。DML語句等都不會強制提交事務。因此與其說ACID是事務必須滿足的條件,不如說它們是衡量事務的四個維度。undo log屬于邏輯日志,它記錄的是sql執行相關的信息。當發生回滾時,InnoDB會根據undo log做相反的事情,對于每個insert,回滾做delete;對于每個delete,回滾做insert;對于update,回滾會執行一個相反的update,把數據改回去。
    sql注入原理:業務端代碼從客戶端接收到惡意payload之后沒有進行過濾直接進行sql語句拼接并且執行造成sql注入本人正在拜讀一本代碼審計的書感覺非常的棒,剛剛好室友在挑戰自己,就順便整理一下知識點!看了一下也沒問題,繼續往下走,發現室友mybatis里的sql全部是使用$拼接的!
    LIMIT 語句分頁查詢是最常用的場景之一,但也通常也是最容易出問題的地方。比如對于下面簡單的語句,一般 DBA 想到的辦法是在 type, name, create_time 字段上加組合索引。這樣條件排序都能有效的利用到索引,性能迅速提升。好吧,可能90%以上的 DBA 解決該問題就到此為止。出現這種性能問題,多數情形下是程序員偷懶了。在新設計下查詢時間基本固定,不會隨著數據量的增長而發生變化。
    并且在 MySQL 執行完畢之后,還能拿到執行結果,如果執行出錯,也要能拿到 MySQL 拋出的錯誤。而負責上述邏輯的,我們稱之為驅動,Python 里面的 MySQL 驅動最常用的就是 pymysql,這是一個同步驅動,異步驅動的話則是 asyncmy。
    避免網站顯示SQL錯誤信息,比如類型錯誤、字段不匹配等,防止攻擊者利用這些錯誤信息進行一些判斷。
    Web安全常見漏洞修復建議
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类