程序员们记住这8种常被忽视的SQL错误用法

    作者:课课家教育更新于: 2019-04-16 10:01:25

        其实,今天你会看到每个人——从新手到专家——在使用SQL时犯的各种常见错误。你不能永远避免犯任何错误,但是熟悉广泛的错误将帮助你在尽可能短的时间内解决这些错误。但是我们有了前人的经验,我们就可以避免更多的数据库错误了,那么我们来看一下我们需要记住哪些错误?

    sql语句的执行顺序:

    1. FROM  
    2.  
    3.  
    4. ON  
    5.  
    6.  
    7.  
    8.  JOIN  
    9.  
    10.  
    11. WHERE  
    12.  
    13.  
    14. GROUP BY  
    15.  
    16.  
    17. HAVING  
    18.  
    19.  
    20. SELECT 
    21.  
    22. DISTINCT  
    23.  
    24.  
    25. ORDER BY  
    26.  
    27.  
    28. LIMIT  
    29.  

    1、LIMIT 语句

    分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般 DBA 想到的办法是在 type, name, create_time 字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。

    1. SELECT *  
    2. FROM   operation  
    3. WHERE  type = 'SQLStats'  
    4.        AND name = 'SlowLog'  
    5. ORDER  BY create_time  
    6. LIMIT  1000, 10;  

    好吧,可能90%以上的 DBA 解决该问题就到此为止。但当 LIMIT 子句变成 “LIMIT 1000000,10” 时,程序员仍然会抱怨:我只取10条记录为什么还是慢?

    要知道数据库也并不知道第1000000条记录从什么地方开始,即使有索引也需要从头计算一次。出现这种性能问题,多数情形下是程序员偷懒了。

    在前端数据浏览翻页,或者大数据分批导出等场景下,是可以将上一页的最大值当成参数作为查询条件的。SQL 重新设计如下:

    1. SELECT   *  
    2. FROM     operation  
    3. WHERE    type = 'SQLStats'  
    4. AND      name = 'SlowLog'  
    5. AND      create_time > '2017-03-16 14:00:00'  
    6. ORDER BY create_time limit 10; 

    在新设计下查询时间基本固定,不会随着数据量的增长而发生变化。

    2、隐式转换

    SQL语句中查询变量和字段定义类型不匹配是另一个常见的错误。比如下面的语句:

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

    其中字段 bpn 的定义为 varchar(20),MySQL 的策略是将字符串转换为数字之后再比较。函数作用于表字段,索引失效。

    上述情况可能是应用程序框架自动填入的参数,而不是程序员的原意。现在应用框架很多很繁杂,使用方便的同时也小心它可能给自己挖坑。

    3、关联更新、删除

    虽然 MySQL5.6 引入了物化特性,但需要特别注意它目前仅仅针对查询语句的优化。对于更新或删除需要手工重写成 JOIN。

    比如下面 UPDATE 语句,MySQL 实际执行的是循环/嵌套子查询(DEPENDENT SUBQUERY),其执行时间可想而知。

    1. UPDATE operation o  
    2. SET    status = 'applying'  
    3. WHERE  o.id IN (SELECT id  
    4.                 FROM   (SELECT o.id,  
    5.                                o.status  
    6.                         FROM   operation o  
    7.                         WHERE  o.group = 123  
    8.                                AND o.status NOT IN ( 'done' )  
    9.                         ORDER  BY o.parent,  
    10.                                   o.id  
    11.                         LIMIT  1) t);  

    执行计划:

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

    重写为 JOIN 之后,子查询的选择模式从 DEPENDENT SUBQUERY 变成 DERIVED,执行速度大大加快,从7秒降低到2毫秒。

    1. UPDATE operation o  
    2.        JOIN  (SELECT o.id,  
    3.                             o.status  
    4.                      FROM   operation o  
    5.                      WHERE  o.group = 123  
    6.                             AND o.status NOT IN ( 'done' )  
    7.                      ORDER  BY o.parent,  
    8.                                o.id  
    9.                      LIMIT  1) t 
    10.          ON o.id = t.id  
    11. SET    status = 'applying'  

    执行计划简化为:

    1. +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+ 
    2. | id | select_type | table | type | possible_keys | key   | key_len | ref   | rows | Extra                                               | 
    3. +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+ 
    4. | 1  | PRIMARY     |       |      |               |       |         |       |      | Impossible WHERE noticed after reading const tables | 
    5. | 2  | DERIVED     | o     | ref  | idx_2,idx_5   | idx_5 | 8       | const | 1    | Using where; Using filesort                         | 
    6. +----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+ 
    7. 4、混合排序 

    4、混合排序

    MySQL 不能利用索引进行混合排序。但在某些场景,还是有机会使用特殊方法提升性能的。

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

    执行计划显示为全表扫描:

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

    由于 is_reply 只有0和1两种状态,我们按照下面的方法重写后,执行时间从1.58秒降低到2毫秒。

    1. SELECT *  
    2. FROM   ((SELECT * 
    3.          FROM   my_order o  
    4.                 INNER JOIN my_appraise a  
    5.                         ON a.orderid = o.id  
    6.                            AND is_reply = 0  
    7.          ORDER  BY appraise_time DESC  
    8.          LIMIT  0, 20)  
    9.         UNION ALL  
    10.         (SELECT * 
    11.          FROM   my_order o  
    12.                 INNER JOIN my_appraise a  
    13.                         ON a.orderid = o.id  
    14.                            AND is_reply = 1  
    15.          ORDER  BY appraise_time DESC  
    16.          LIMIT  0, 20)) t  
    17. ORDER  BY  is_reply ASC,  
    18.           appraisetime DESC  
    19. LIMIT  20;  

    5、EXISTS语句

    MySQL 对待 EXISTS 子句时,仍然采用嵌套子查询的执行方式。如下面的 SQL 语句:

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

    执行计划为:

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

    去掉 exists 更改为 join,能够避免嵌套子查询,将执行时间从1.93秒降低为1毫秒。

    1. SELECT * 
    2. FROM   my_neighbor n  
    3.        INNER JOIN message_info m  
    4.                ON n.id = m.neighbor_id  
    5.                   AND m.inuser = 'xxx'  
    6.        LEFT JOIN my_neighbor_apply sra  
    7.               ON n.id = sra.neighbor_id  
    8.                  AND sra.user_id = 'xxx'  
    9. WHERE  n.topic_status < 4  
    10.        AND n.topic_type <> 5  

    新的执行计划:

    1. +----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+ 
    2. | id | select_type | table | type   | possible_keys     | key       | key_len | ref   | rows | Extra                 | 
    3. +----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+ 
    4. |  1 | SIMPLE      | m     | ref    | | idx_message_info   | 122     | const    |    1 | Using index condition | 
    5. |  1 | SIMPLE      | n     | eq_ref | | PRIMARY   | 122     | ighbor_id |    1 | Using where      | 
    6. |  1 | SIMPLE      | sra   | ref    | | idx_user_id | 123     | const     |    1 | Using where           | 
    7. +----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+ 
    8. 6、条件下推 

    6、条件下推

    外部查询条件不能够下推到复杂的视图或子查询的情况有:

    1.聚合子查询; 2.含有 LIMIT 的子查询; 3.UNION 或 UNION ALL 子查询; 4.输出字段中的子查询;

    如下面的语句,从执行计划可以看出其条件作用于聚合子查询之后:

    1. SELECT *  
    2. FROM   (SELECT target,  
    3.                Count(*)  
    4.         FROM   operation  
    5.         GROUP  BY target) t  
    6. WHERE  target = 'rm-xxxx'  

    1. +----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+ 
    2. | id | select_type | table      | type  | possible_keys | key         | key_len | ref   | rows | Extra       | 
    3. +----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+ 
    4. |  1 | PRIMARY     |  | ref   |    |  | 514     | const |    2 | Using where | 
    5. |  2 | DERIVED     | operation  | index | idx_4         | idx_4       | 519     | NULL  |   20 | Using index | 
    6. +----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+ 

    确定从语义上查询条件可以直接下推后,重写如下:

    1. SELECT target,  
    2.        Count(*)  
    3. FROM   operation  
    4. WHERE  target = 'rm-xxxx'  
    5. GROUP  BY target 

    执行计划变为:

    1. +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ 
    2. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 
    3. +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ 
    4. | 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index | 
    5. +----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ 

    关于 MySQL 外部条件不能下推的详细解释说明请参考以前文章:MySQL · 性能优化 · 条件下推到物化表 http://mysql.taobao.org/monthly/2016/07/08

    7、提前缩小范围**

    先上初始 SQL 语句:

    1. SELECT *  
    2. FROM   my_order o  
    3.        LEFT JOIN my_userinfo u  
    4.               ON o.uid = u.uid 
    5.        LEFT JOIN my_productinfo p  
    6.               ON o.pid = p.pid  
    7. WHERE  ( o.display = 0 )  
    8.        AND ( o.ostaus = 1 )  
    9. ORDER  BY o.selltime DESC  
    10. LIMIT  0, 15  

    该SQL语句原意是:先做一系列的左连接,然后排序取前15条记录。从执行计划也可以看出,最后一步估算排序记录数为90万,时间消耗为12秒。

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

    由于最后 WHERE 条件以及排序均针对最左主表,因此可以先对 my_order 排序提前缩小数据量再做左连接。SQL 重写后如下,执行时间缩小为1毫秒左右。

    1. SELECT *  
    2. FROM ( 
    3. SELECT *  
    4. FROM   my_order o  
    5. WHERE  ( o.display = 0 )  
    6.        AND ( o.ostaus = 1 )  
    7. ORDER  BY o.selltime DESC  
    8. LIMIT  0, 15 
    9. ) o  
    10.      LEFT JOIN my_userinfo u  
    11.               ON o.uid = u.uid  
    12.      LEFT JOIN my_productinfo p  
    13.               ON o.pid = p.pid  
    14. ORDER BY  o.selltime DESC 
    15. limit 0, 15 

    再检查执行计划:子查询物化后(select_type=DERIVED)参与 JOIN。虽然估算行扫描仍然为90万,但是利用了索引以及 LIMIT 子句后,实际执行时间变得很小。

     

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

    8、中间结果集下推

    再来看下面这个已经初步优化过的例子(左连接中的主表优先作用查询条件):

    1. SELECT    a.*,  
    2.           c.allocated  
    3. FROM      (  
    4.               SELECT   resourceid  
    5.               FROM     my_distribute d  
    6.                    WHERE    isdelete = 0  
    7.                    AND      cusmanagercode = '1234567'  
    8.                    ORDER BY salecode limit 20) a  
    9. LEFT JOIN  
    10.           (  
    11.               SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated  
    12.               FROM     my_resources  
    13.                    GROUP BY resourcesid) c  
    14. ON        a.resourceid = c.resourcesid 

    那么该语句还存在其它问题吗?不难看出子查询 c 是全表聚合查询,在表数量特别大的情况下会导致整个语句的性能下降。

    其实对于子查询 c,左连接最后结果集只关心能和主表 resourceid 能匹配的数据。因此我们可以重写语句如下,执行时间从原来的2秒下降到2毫秒。

    1. SELECT    a.*,  
    2.           c.allocated  
    3. FROM      (  
    4.                    SELECT   resourceid  
    5.                    FROM     my_distribute d  
    6.                    WHERE    isdelete = 0  
    7.                    AND      cusmanagercode = '1234567'  
    8.                    ORDER BY salecode limit 20) a  
    9. LEFT JOIN  
    10.           (  
    11.                    SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated  
    12.                    FROM     my_resources r,  
    13.                             (  
    14.                                      SELECT   resourceid  
    15.                                      FROM     my_distribute d  
    16.                                      WHERE    isdelete = 0  
    17.                                      AND      cusmanagercode = '1234567'  
    18.                                      ORDER BY salecode limit 20) a  
    19.                    WHERE    r.resourcesid = a.resourcesid  
    20.                    GROUP BY resourcesid) c  
    21. ON        a.resourceid = c.resourcesid 

    但是子查询 a 在我们的SQL语句中出现了多次。这种写法不仅存在额外的开销,还使得整个语句显的繁杂。使用 WITH 语句再次重写:

    1. WITH a AS  
    2. (  
    3.          SELECT   resourceid  
    4.          FROM     my_distribute d  
    5.          WHERE    isdelete = 0  
    6.          AND      cusmanagercode = '1234567'  
    7.          ORDER BY salecode limit 20) 
    8. SELECT    a.*,  
    9.           c.allocated  
    10. FROM      a  
    11. LEFT JOIN  
    12.           (  
    13.                    SELECT   resourcesid, sum(ifnull(allocation, 0) * 12345) allocated  
    14.                    FROM     my_resources r,  
    15.                             a  
    16.                    WHERE    r.resourcesid = a.resourcesid  
    17.                    GROUP BY resourcesid) c  
    18. ON        a.resourceid = c.resourcesid 
    19. 总结 

    小编结语:

    数据库编译器产生执行计划,决定着SQL的实际执行方式。但是编译器只是尽力服务,所有数据库的编译器都不是尽善尽美的。上述提到的多数场景,在其它数据库中也存在性能问题。了解数据库编译器的特性,才能避规其短处,写出高性能的SQL语句。程序员在设计数据模型以及编写SQL语句时,要把算法的思想或意识带进来。编写复杂SQL语句要养成使用 WITH 语句的习惯。简洁且思路清晰的SQL语句也能减小数据库的负担 。

    其实,针对于数据库的错误,我们更需要记住,数据库的错误,我们不可忽视,因为一个小小的错误会造成很大的错误的,而且数据也对我们非常重要,许多不同类型的错误——上百个——会干扰你和你的数据。幸运的是,大多数错误都不是灾难性的,并很好解决。然而,有一些错误如果发生了就非常严重。无论你什么时候试着纠正错误,你都要非常谨慎,因为如果你没有找到问题的根源那这个问题很可能会加倍。当你确实产生了错误,当然这是肯定会的,将它们当作你的学习经历。

课课家教育

未登录