暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL 查询避坑指南

云筑网技术团队 2022-04-21
238

    MySQL查询中,你可能会不经意的掉入某些坑中,甚至某些坑可能让你不死也掉半条命。下面罗列一下常见的坑。

一、 多表操作时不写别名

掉坑现场重现:

    mysql> select * from t1;
    +------+
    | Id |
    +------+
    | 1 |
    | 2 |
    | 3 |
    +------+
    3 rows in set (0.00 sec)


    mysql> select * from t2;
    Empty set (0.00 sec)


    mysql> select * from t3;
    +------+
    | ld |
    +------+
    | 10 |
    +------+
    1 row in set (0.00 sec)
    mysql> delete from t1 where Id in(select Id from t2);
    Query OK, 0 rows affected (0.00 sec)


    mysql> delete from t1 where Id in(select Id from t3);
    Query OK, 3 rows affected (0.03 sec)
    复制


    t1表有3条记录(记录值不重复),t2表没有记录,t3表有1条记录。所以第一个DELETE语句不会删除任何记录,这是符合预期的。

    可是第2个DELETE语句为什么删除了3条记录呢?t3表只有一条记录,t1的记录值也是不重复的,所以是不是应该最多只删除一条记录呢?

    眼神好的同学可能发现了,t3表压根就没有Id列,t3表的列名是ld。没错,问题就在这里了。在子查询中,列可以是父表的列,也可以是子表的列,当你不写列名所属表的时候,MySQL根据表结构来确定列所属的表,悲剧就发生在这里,由于没有写列所属的表,所以实际上的条件是t1.Id=t1.Id,这个自然就是把t1表的数据都删除了。如果这个情况发生的生产环境,其后果可想而知。

    我们在DB规范中,明确要求多表查询语句中,一定要写清楚列所属的表,它不单纯是让阅读清晰,更重要的是防止掉坑。

    注意,这个坑可不是专属于MySQL

    二、 不支持FULL JOIN

    DBA一直跟我说,MySQL不支持FULL JOIN,可是我觉得不太可能嘛,好歹MySQL也是一个发展了这么多年,用户群也非常大的成熟产品,不至于连这个都不支持。于是我自己写了个FULL JOIN的查询,看看下面的执行现场,妥妥的执行了嘛。

      mysql> select * from t1 full join t3 on Id=ld;
      Empty set (0.00 sec)
      复制

                  找DBA理论去,然后DBA让我自己看下执行计划(只截取了部分):

        +----+-------------+-------
        | id | select_type | table
        +----+-------------+-------
        | 1 | SIMPLE | t3
        | 1 | SIMPLE | full
        复制

        其他部分都不用看,只需要看table列中中显示的那个full,大家就能明白是怎么回事了吧。因为full joinMySQL中不支持,所以full不是保留的关键字,被识别成表的别名了,查询执行当然就不会出错,但实际上是一个inner join操作。

        当然,这是一个多表查询,如果按照DB规范,把列所属的表写上,那么查询会直接报错找不到表,那样会更早发现问题。

        三、 REPLACE INTO的副作用

        REPLACE INTO通常用于处理:存在更新、不存在插入这种业务逻辑。在表中存在多个唯一时,它导致的结果可能不是我们想要的,参考如下:

          mysql> create table t1(a int, b int, c int,unique(a),unique(b),unique(c));
          mysql> insert into t1 values(1,1,1),(2,2,2),(3,3,3);
          Query OK, 3 rows affected (0.02 sec)
          Records: 3 Duplicates: 0 Warnings: 0


          mysql> replace into t1 values(1,2,3);
          Query OK, 4 rows affected (0.02 sec)


          mysql> select * from t1;
          +------+------+------+
          | a | b | c |
          +------+------+------+
          | 1 | 2 | 3 |
          +------+------+------+
          1 row in set (0.00 sec)
          复制

          虽然我们只插入了一条记录,但是由于a、b、c都是要保证唯一的,所以我们插入的这条记录,凡是会导致违反唯一约束的记录都会删除,这就导致删除了3条记录。

          可以酌情考虑使用INSERT INTO ON DUPLICATE KEY UPDATE语句,它只会选择一个唯一约束作为记录是否存在的判断依据,并且如果UPDATE导致违反了其他唯一约束,会给出Duplicate的错误信息。不过,在表中有多个唯一约束时,你无法控制ON DUPLICATE KEY选择使用哪个唯一约束来确定记录是否存在。

          四、 INSERT INTO ON DUPLICATE KEY UPDATE加自增列的诡异现象

          先看看下面的演示脚本:

            mysql> select * from t1 order by id desc;
            +-----+------+
            | id | c1 |
            +-----+------+
            | 127 | 1 |
            | 126 | 2 |
            +-----+------+
            2 rows in set (0.00 sec)


            mysql> insert into t1(c1) values(2) as n on duplicate key update c1=n.c1;
            ERROR 1062 (23000): Duplicate entry '2' for key 't1.c1'
            复制

            在这个演示脚本中,t1.id是自增列+主键,c1上有唯一约束。出错的insert 语句没有指定自增列,因此这个列自动生成最新的,不会成为ON DUPLICATE KEY的判断依据,而通过c1来确定记录是否存在的话,要么存在更新,要么不存在插入一条记录,但这里居然报duplicate的错误,这是不是很诡异呢?

            MySQL不支持将自增值设置为一个比表中最大值还小的值,所以应该可以排除将当前自增值调小,使新记录自增值变成127,导致以自增值作为记录是否存在的依据,使更新发生在id=127的记录,从而产生duplicate错误。为了验证问题,我们仍然通过SHOW CREATE TABLE t1来验证一下:

              Create Table: CREATE TABLE `t1` (
              `id` tinyint NOT NULL AUTO_INCREMENT,
              `c1` int DEFAULT NULL,
              PRIMARY KEY (`id`),
              UNIQUE KEY `c1` (`c1`)
              ) ENGINE=InnoDB AUTO_INCREMENT=127 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
              复制

              结果显示:AUTO_INCREMENT=127,所以下一个值为128,不可能回到127上。但再仔细看,你会发现id的数据类型是tinyint,它的上限就是127,也就是下一个值是128,这是不是应该会出现Out of range的错误?

              事实上不会,当显式往表中INSERT一个超过数据类型范围的值时,确实会报Out of range的错误;但如果让MySQL自己来生成这个值时,它发现产生的新值超过范围时,会使用最大值,所以它不会出现Out of range的错误。在这个示例中,生成的自增值超出范围了,所以回退到最大值127,这导致更新操作发生在id=127的记录上,并因此产生了诡异的Duplicate错误。

              五、 数据类型不一致

              在一个查询表达式中,涉及多个字段、变量、常量时,类型不一致是非常常见的,但这种不统一有时候会带来非常严重的影响。

              这是最近遇到的一次掉坑,查询语句非常简单,看不出什么毛病;满足条件的数据量只有200条,但执行时长超过30秒。

                SELECT A.BNo, B.SId, A.Status, COUNT(0) AS Total 
                FROM A
                INNER JOIN B ON B.ONo = A.SNo
                INNER JOIN C ON B.SId = C.SId AND A.BNo = C.BNo
                GROUP BY A.BNo, B.SId, A.Status
                复制

                查看执行计划,驱动表为C(数据量最小的一个表),依次JOIN B、A得到最终结果,它们的Rows评估为100、948、1,有合适的索引,看起来应该没有什么问题。

                进行实际的数据验证,C JOIN B的结果有近800万(检查数据,发现数据的分布很不均衡,所以评估的数据量与实际数据量有很大差异的原因明确了);C JOIN A的结果不足300条。正常来说,应该先JOIN小表,再去JOIN大表,目前的这个执行计划显然不太合理。最后检查表结构,发现C JOIN A的字段类型不一致,类型不一致导致需要先做类似转换,这通常会导致索引无效或低效。将字段类型统一之后,执行计划正常了,并且执行时间由原来的30秒+缩短到毫秒。

                除了数据类型,字符集的差异也会造成类似的影响,只是我们一般都是使用统一字符集,所以这种情况较少遇见。

                六、 不确定性函数

                从一个表中,随机抽取10条记录,SQL语句怎么写?似乎很容易:ORDER BY RAND() LIMIT 10

                没毛病,也确实得到了想要的结果,但试试用这个语句从大表中取10条数据,你会发现很慢,每次都很慢,为什么呢?

                MySQL函数在内部被标记为确定性或非确定性。如果函数使用相同的参数值调用,返回的值可能不相同,则函数是确定性的,RANDUUID都是非确定性函数。

                在查询中使用非确定性函数,意味着对于每一行,函数都会被调用,所以对于ORDER BY RAND() LIMIT 10而言,由于第一行都会调用RAND,它的值是不定的,那么要ORDER BY,所有的行都要得到RAND的结果,也就是做了表扫描,慢就可以理解了。

                七、 容错查询

                区别于那些严格要求,一有异常就抛出错的数据库系统,MySQL查询讲究尽量给出它理解的结果,通过Warning展示对查询的质疑。这种体系有时候会导致一些混乱。

                比如下面的两个查询:

                  mysql> select id from t1 where id='1a' union all select id from t2 where id='1a';
                  +------+
                  | id |
                  +------+
                  | 1 |
                  | 1a |
                  +------+
                  2 rows in set, 1 warning (0.00 sec)


                  mysql> select id from( select id from t1 union all select id from t2 )d where id='1a';
                  +------+
                  | id |
                  +------+
                  | 1a |
                  +------+
                  1 row in set (0.00 sec)
                  复制

                  在数据不变的情况下,从查询逻辑来说,上述两个查询应该返回相同的结果,但这两个查询返回了不同的查询结果,并且我们注意到第一个查询的输出中包含了1 warning

                  重新执行查询1,并且通过SHOW WARNINGS,我们得到了这样的信息:

                    +---------+------+----------------------------------------+
                    | Level | Code | Message |
                    +---------+------+----------------------------------------+
                    | Warning | 1292 | Truncated incorrect DOUBLE value: '1a' |
                    +---------+------+----------------------------------------+
                    复制

                    这表明查询1中出来了一个数据类型转换,将字符串1a转成了DOUBLE,在这个示例中,t1.id是int,t2.id是varchar,熟悉数据库的同学大致都知道,当varchar和int中比较的时候,int的类型优先级高于varchar,也就是varchar会转为int之后再做比较,所以对于查询1,t1.id=’1a’这个操作导致1a转成数字,但1a显然不是一个数字,MySQL在这里做了和谐处理,将1a中的前置数字提取出来作为数字了,但这个理解可能是不正确的,所以给了一个warning

                    如果是按照同样的规则来解析查询2,你会发现仍然解释不通,在UNIO ALL的时候,两个表的数据类型不一致,按照数据类型优先级,也是varcharint,这里应该有一个warning,并且UNION ALL的结果应该是两个值为1的记录,最后经过WHERE条件的时候,会因为数据类型不一致,再报一次warning。但查询2很神奇的是没有warning,并且记录值也不符合预期。原因在于UNION对于数据的转换规则又有所不同,在UNION查询中,它会综合评估列的类型和长度,确定一个合适的结果类型,在这个查询中,评估的结果是使用varchar数据类型,所以查询过程中只有一次intvarchar,而且这个转换是没有歧义的,所以查询不会有任何Warning

                    关于表达式数据类型转换规则和UNION对数据输出类型界定,可以参考MySQL官方文档上的相关说明。

                     

                     

                    作  者:邹建(飞雪)
                    审  稿:吴友强(技巅)
                    编  吴友强(技巅)
                    文章转载自云筑网技术团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论