翼度科技»论坛 编程开发 mysql 查看内容

MySQL bit类型增加索引后查询结果不正确案例浅析

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
昨天同事遇到的一个案例,这里简单描述一下:一个表里面有一个bit类型的字段,同事在优化相关SQL的过程中,给这个表的bit类型的字段新增了一个索引,然后测试验证时,居然发现SQL语句执行结果跟不加索引不一样。加了索引后,SQL语句没有查询出一条记录,删除索引后,SQL语句就能查询出几十条记录。下面我们构造一个简单的例子,重现一下这个案例
我们先创建表student_attend,初始化一些数据。这篇文章的测试环境为MySQL 8.0.35社区版。
  1. CREATE TABLE `student_attend` (
  2.   `id` int NOT NULL AUTO_INCREMENT COMMENT '自增编号',
  3.   `std_id` int DEFAULT NULL COMMENT '学号',
  4.   `class_id` int DEFAULT NULL COMMENT '课程编号',
  5.   `is_attend` bit(1) DEFAULT b'1' COMMENT '是否缺陷考勤',
  6.   PRIMARY KEY (`id`)
  7. ) ENGINE=InnoDB;


  8. insert into student_attend(std_id, class_id, is_attend)
  9. select 1001, 1, 1 from dual union all
  10. select 1001, 2, 0 from dual union all
  11. select 1001, 3, 1 from dual union all
  12. select 1001, 4, 1 from dual union all
  13. select 1001, 5, 1 from dual union all
  14. select 1001, 6, 0 from dual union all
  15. select 1002, 1, 1 from dual union all
  16. select 1002, 2, 1 from dual union all
  17. select 1003, 1, 0 from dual union all
  18. select 1003, 2, 0 from dual;
复制代码
如下所示,假设我们要查询is_attend=1的所有学生信息,那么可以有下面三种写法
  1. mysql> select * from student_attend where is_attend=1;
  2. +----+--------+----------+----------------------+
  3. | id | std_id | class_id | is_attend            |
  4. +----+--------+----------+----------------------+
  5. |  1 |   1001 |        1 | 0x01                 |
  6. |  3 |   1001 |        3 | 0x01                 |
  7. |  4 |   1001 |        4 | 0x01                 |
  8. |  5 |   1001 |        5 | 0x01                 |
  9. |  7 |   1002 |        1 | 0x01                 |
  10. |  8 |   1002 |        2 | 0x01                 |
  11. +----+--------+----------+----------------------+
  12. 6 rows in set (0.00 sec)

  13. mysql> select * from student_attend where is_attend=b'1';
  14. +----+--------+----------+----------------------+
  15. | id | std_id | class_id | is_attend            |
  16. +----+--------+----------+----------------------+
  17. |  1 |   1001 |        1 | 0x01                 |
  18. |  3 |   1001 |        3 | 0x01                 |
  19. |  4 |   1001 |        4 | 0x01                 |
  20. |  5 |   1001 |        5 | 0x01                 |
  21. |  7 |   1002 |        1 | 0x01                 |
  22. |  8 |   1002 |        2 | 0x01                 |
  23. +----+--------+----------+----------------------+
  24. 6 rows in set (0.00 sec)

  25. #遇到问题的SQL写法
  26. mysql> select * from student_attend where is_attend='1';
  27. +----+--------+----------+----------------------+
  28. | id | std_id | class_id | is_attend            |
  29. +----+--------+----------+----------------------+
  30. |  1 |   1001 |        1 | 0x01                 |
  31. |  3 |   1001 |        3 | 0x01                 |
  32. |  4 |   1001 |        4 | 0x01                 |
  33. |  5 |   1001 |        5 | 0x01                 |
  34. |  7 |   1002 |        1 | 0x01                 |
  35. |  8 |   1002 |        2 | 0x01                 |
  36. +----+--------+----------+----------------------+
  37. 6 rows in set (0.00 sec)

  38. mysql> 
复制代码
接下来,我们在字段is_attend上创建索引ix_student_attend_n1,如下所示
  1. create index ix_student_attend_n1 on student_attend(is_attend);
复制代码
然后我们继续测试验证,就能出现我前文所说的情况,如需所示,最后一个SQL,它的返回记录数为0.
  1. mysql> select * from student_attend where is_attend=1;
  2. +----+--------+----------+----------------------+
  3. | id | std_id | class_id | is_attend            |
  4. +----+--------+----------+----------------------+
  5. |  1 |   1001 |        1 | 0x01                 |
  6. |  3 |   1001 |        3 | 0x01                 |
  7. |  4 |   1001 |        4 | 0x01                 |
  8. |  5 |   1001 |        5 | 0x01                 |
  9. |  7 |   1002 |        1 | 0x01                 |
  10. |  8 |   1002 |        2 | 0x01                 |
  11. +----+--------+----------+----------------------+
  12. 6 rows in set (0.00 sec)

  13. mysql> select * from student_attend where is_attend=b'1';
  14. +----+--------+----------+----------------------+
  15. | id | std_id | class_id | is_attend            |
  16. +----+--------+----------+----------------------+
  17. |  1 |   1001 |        1 | 0x01                 |
  18. |  3 |   1001 |        3 | 0x01                 |
  19. |  4 |   1001 |        4 | 0x01                 |
  20. |  5 |   1001 |        5 | 0x01                 |
  21. |  7 |   1002 |        1 | 0x01                 |
  22. |  8 |   1002 |        2 | 0x01                 |
  23. +----+--------+----------+----------------------+
  24. 6 rows in set (0.00 sec)

  25. mysql> select * from student_attend where is_attend='1';
  26. Empty set (0.00 sec)

  27. mysql> 
复制代码
其实第一次见到这种情况的时候,我还是有点震惊的,因为在我的观念中,索引只会影响执行计划,不会影响查询结果,但是现在的情况是索引的存在影响了SQL的查询结果。那么为什么会出现这种情况呢?
首先看了一下执行计划,如下所示,从执行计划看,它既没有走全表扫描也没有走索引,仅仅有"message": "no matching row in const table"提示,如果仅仅分析执行计划,我们得不到更多的有用信息
  1. mysql> explain
  2.     -> select * from student_attend where is_attend='1';
  3. +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
  4. | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                          |
  5. +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
  6. |  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL |     NULL | no matching row in const table |
  7. +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------+
  8. 1 row in set, 1 warning (0.00 sec)

  9. mysql> 
  10. mysql> explain format=json
  11.     -> select * from student_attend where is_attend='1'\G
  12. *************************** 1. row ***************************
  13. EXPLAIN: {
  14.   "query_block": {
  15.     "select_id": 1,
  16.     "message": "no matching row in const table"
  17.   } /* query_block */
  18. }
  19. 1 row in set, 1 warning (0.00 sec)

  20. mysql> show warnings\G
  21. *************************** 1. row ***************************
  22.   Level: Note
  23.    Code: 1003
  24. Message: /* select#1 */ select `kerry`.`student_attend`.`id` AS `id`,`kerry`.`student_attend`.`std_id` AS `std_id`,`kerry`.`student_attend`.`class_id` AS `class_id`,`kerry`.`student_attend`.`is_attend` AS `is_attend` from `kerry`.`student_attend` where (`kerry`.`student_attend`.`is_attend` = '1')
  25. 1 row in set (0.00 sec)

  26. mysql> 
复制代码
那么我们使用trace跟踪分析一下优化器如何选择执行计划。看看其详细执行过程,如下所示
  1. mysql> SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;
  2. Query OK, 0 rows affected (0.00 sec)

  3. mysql> SET OPTIMIZER_TRACE_MAX_MEM_SIZE=1000000;
  4. Query OK, 0 rows affected (0.00 sec)

  5. mysql> select * from student_attend where is_attend='1';
  6. Empty set (0.00 sec)

  7. mysql> SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE \G;
  8. *************************** 1. row ***************************
  9.                             QUERY: select * from student_attend where is_attend='1'
  10.                             TRACE: {
  11.   "steps": [
  12.     {
  13.       "join_preparation": {
  14.         "select#": 1,
  15.         "steps": [
  16.           {
  17.             "expanded_query": "/* select#1 */ select `student_attend`.`id` AS `id`,`student_attend`.`std_id` AS `std_id`,`student_attend`.`class_id` AS `class_id`,`student_attend`.`is_attend` AS `is_attend` from `student_attend` where (`student_attend`.`is_attend` = '1')"
  18.           }
  19.         ] /* steps */
  20.       } /* join_preparation */
  21.     },
  22.     {
  23.       "join_optimization": {
  24.         "select#": 1,
  25.         "steps": [
  26.           {
  27.             "condition_processing": {
  28.               "condition": "WHERE",
  29.               "original_condition": "(`student_attend`.`is_attend` = '1')",
  30.               "steps": [
  31.                 {
  32.                   "transformation": "equality_propagation",
  33.                   "resulting_condition": "(`student_attend`.`is_attend` = '1')"
  34.                 },
  35.                 {
  36.                   "transformation": "constant_propagation",
  37.                   "resulting_condition": "(`student_attend`.`is_attend` = '1')"
  38.                 },
  39.                 {
  40.                   "transformation": "trivial_condition_removal",
  41.                   "resulting_condition": "(`student_attend`.`is_attend` = '1')"
  42.                 }
  43.               ] /* steps */
  44.             } /* condition_processing */
  45.           },
  46.           {
  47.             "substitute_generated_columns": {
  48.             } /* substitute_generated_columns */
  49.           },
  50.           {
  51.             "table_dependencies": [
  52.               {
  53.                 "table": "`student_attend`",
  54.                 "row_may_be_null": false,
  55.                 "map_bit": 0,
  56.                 "depends_on_map_bits": [
  57.                 ] /* depends_on_map_bits */
  58.               }
  59.             ] /* table_dependencies */
  60.           },
  61.           {
  62.             "ref_optimizer_key_uses": [
  63.               {
  64.                 "table": "`student_attend`",
  65.                 "field": "is_attend",
  66.                 "equals": "'1'",
  67.                 "null_rejecting": true
  68.               }
  69.             ] /* ref_optimizer_key_uses */
  70.           },
  71.           {
  72.             "rows_estimation": [
  73.               {
  74.                 "table": "`student_attend`",
  75.                 "range_analysis": {
  76.                   "table_scan": {
  77.                     "rows": 10,
  78.                     "cost": 3.35
  79.                   } /* table_scan */,
  80.                   "potential_range_indexes": [
  81.                     {
  82.                       "index": "PRIMARY",
  83.                       "usable": false,
  84.                       "cause": "not_applicable"
  85.                     },
  86.                     {
  87.                       "index": "ix_student_attend_n1",
  88.                       "usable": true,
  89.                       "key_parts": [
  90.                         "is_attend",
  91.                         "id"
  92.                       ] /* key_parts */
  93.                     }
  94.                   ] /* potential_range_indexes */,
  95.                   "setup_range_conditions": [
  96.                     {
  97.                       "impossible_condition": {
  98.                         "cause": "value_out_of_range"
  99.                       } /* impossible_condition */
  100.                     }
  101.                   ] /* setup_range_conditions */,
  102.                   "impossible_range": true
  103.                 } /* range_analysis */,
  104.                 "rows": 0,
  105.                 "cause": "impossible_where_condition"
  106.               }
  107.             ] /* rows_estimation */
  108.           }
  109.         ] /* steps */,
  110.         "empty_result": {
  111.           "cause": "no matching row in const table"
  112.         } /* empty_result */
  113.       } /* join_optimization */
  114.     },
  115.     {
  116.       "join_execution": {
  117.         "select#": 1,
  118.         "steps": [
  119.         ] /* steps */
  120.       } /* join_execution */
  121.     }
  122.   ] /* steps */
  123. }
  124. MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
  125.           INSUFFICIENT_PRIVILEGES: 0
  126. 1 row in set (0.00 sec)

  127. ERROR: 
  128. No query specified

  129. mysql> 
  130. mysql> SET optimizer_trace="enabled=off";
  131. Query OK, 0 rows affected (0.01 sec)
复制代码
从trace的详细信息看,这个过程中发生了隐式转换:下面这个过程就是发生了类型转换
由于发生类型转换过程中(字符串转换为bit类型)遇到了数据截断错误(从value_out_of_range等信息就可以看出),如下截图所示
而优化器应该是根据一定的逻辑判断,得到这个值不存在索引中,从而就判断没有匹配的记录,直接返回空的结果集了,根本不去走扫描全表或走索引查找等操作。
  1.         "empty_result": {
  2.           "cause": "no matching row in const table"
  3.         } /* empty_result */
复制代码
当然这里仅仅是根据trace的信息做的一个判断,如有错误或不谨慎的地方,敬请谅解。毕竟没有深入分析过源码。
那么为什么没有索引的话,SQL语句的结果就是正确的呢? 难道没有发生类型转换吗? 难度没有发生数据截断错误吗?那么我们就继续trace跟踪分析看看,如下所示
  1. mysql> drop index ix_student_attend_n1 on student_attend;
  2. Query OK, 0 rows affected (0.03 sec)
  3. Records: 0  Duplicates: 0  Warnings: 0

  4. mysql> SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;
  5. Query OK, 0 rows affected (0.00 sec)

  6. mysql> SET OPTIMIZER_TRACE_MAX_MEM_SIZE=1000000;
  7. Query OK, 0 rows affected (0.00 sec)

  8. mysql> select * from student_attend where is_attend='1';
  9. +----+--------+----------+----------------------+
  10. | id | std_id | class_id | is_attend            |
  11. +----+--------+----------+----------------------+
  12. |  1 |   1001 |        1 | 0x01                 |
  13. |  3 |   1001 |        3 | 0x01                 |
  14. |  4 |   1001 |        4 | 0x01                 |
  15. |  5 |   1001 |        5 | 0x01                 |
  16. |  7 |   1002 |        1 | 0x01                 |
  17. |  8 |   1002 |        2 | 0x01                 |
  18. +----+--------+----------+----------------------+
  19. 6 rows in set (0.00 sec)

  20. mysql> SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE \G;
  21. *************************** 1. row ***************************
  22.                             QUERY: select * from student_attend where is_attend='1'
  23.                             TRACE: {
  24.   "steps": [
  25.     {
  26.       "join_preparation": {
  27.         "select#": 1,
  28.         "steps": [
  29.           {
  30.             "expanded_query": "/* select#1 */ select `student_attend`.`id` AS `id`,`student_attend`.`std_id` AS `std_id`,`student_attend`.`class_id` AS `class_id`,`student_attend`.`is_attend` AS `is_attend` from `student_attend` where (`student_attend`.`is_attend` = '1')"
  31.           }
  32.         ] /* steps */
  33.       } /* join_preparation */
  34.     },
  35.     {
  36.       "join_optimization": {
  37.         "select#": 1,
  38.         "steps": [
  39.           {
  40.             "condition_processing": {
  41.               "condition": "WHERE",
  42.               "original_condition": "(`student_attend`.`is_attend` = '1')",
  43.               "steps": [
  44.                 {
  45.                   "transformation": "equality_propagation",
  46.                   "resulting_condition": "(`student_attend`.`is_attend` = '1')"
  47.                 },
  48.                 {
  49.                   "transformation": "constant_propagation",
  50.                   "resulting_condition": "(`student_attend`.`is_attend` = '1')"
  51.                 },
  52.                 {
  53.                   "transformation": "trivial_condition_removal",
  54.                   "resulting_condition": "(`student_attend`.`is_attend` = '1')"
  55.                 }
  56.               ] /* steps */
  57.             } /* condition_processing */
  58.           },
  59.           {
  60.             "substitute_generated_columns": {
  61.             } /* substitute_generated_columns */
  62.           },
  63.           {
  64.             "table_dependencies": [
  65.               {
  66.                 "table": "`student_attend`",
  67.                 "row_may_be_null": false,
  68.                 "map_bit": 0,
  69.                 "depends_on_map_bits": [
  70.                 ] /* depends_on_map_bits */
  71.               }
  72.             ] /* table_dependencies */
  73.           },
  74.           {
  75.             "ref_optimizer_key_uses": [
  76.             ] /* ref_optimizer_key_uses */
  77.           },
  78.           {
  79.             "rows_estimation": [
  80.               {
  81.                 "table": "`student_attend`",
  82.                 "table_scan": {
  83.                   "rows": 10,
  84.                   "cost": 0.25
  85.                 } /* table_scan */
  86.               }
  87.             ] /* rows_estimation */
  88.           },
  89.           {
  90.             "considered_execution_plans": [
  91.               {
  92.                 "plan_prefix": [
  93.                 ] /* plan_prefix */,
  94.                 "table": "`student_attend`",
  95.                 "best_access_path": {
  96.                   "considered_access_paths": [
  97.                     {
  98.                       "rows_to_scan": 10,
  99.                       "access_type": "scan",
  100.                       "resulting_rows": 10,
  101.                       "cost": 1.25,
  102.                       "chosen": true
  103.                     }
  104.                   ] /* considered_access_paths */
  105.                 } /* best_access_path */,
  106.                 "condition_filtering_pct": 100,
  107.                 "rows_for_plan": 10,
  108.                 "cost_for_plan": 1.25,
  109.                 "chosen": true
  110.               }
  111.             ] /* considered_execution_plans */
  112.           },
  113.           {
  114.             "attaching_conditions_to_tables": {
  115.               "original_condition": "(`student_attend`.`is_attend` = '1')",
  116.               "attached_conditions_computation": [
  117.               ] /* attached_conditions_computation */,
  118.               "attached_conditions_summary": [
  119.                 {
  120.                   "table": "`student_attend`",
  121.                   "attached": "(`student_attend`.`is_attend` = '1')"
  122.                 }
  123.               ] /* attached_conditions_summary */
  124.             } /* attaching_conditions_to_tables */
  125.           },
  126.           {
  127.             "finalizing_table_conditions": [
  128.               {
  129.                 "table": "`student_attend`",
  130.                 "original_table_condition": "(`student_attend`.`is_attend` = '1')",
  131.                 "final_table_condition   ": "(`student_attend`.`is_attend` = '1')"
  132.               }
  133.             ] /* finalizing_table_conditions */
  134.           },
  135.           {
  136.             "refine_plan": [
  137.               {
  138.                 "table": "`student_attend`"
  139.               }
  140.             ] /* refine_plan */
  141.           }
  142.         ] /* steps */
  143.       } /* join_optimization */
  144.     },
  145.     {
  146.       "join_execution": {
  147.         "select#": 1,
  148.         "steps": [
  149.         ] /* steps */
  150.       } /* join_execution */
  151.     }
  152.   ] /* steps */
  153. }
  154. MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
  155.           INSUFFICIENT_PRIVILEGES: 0
  156. 1 row in set (0.00 sec)

  157. ERROR: 
  158. No query specified

  159. mysql> SET optimizer_trace="enabled=off";
  160. Query OK, 0 rows affected (0.00 sec)
复制代码
从上面trace信息来看,似乎执行计划先进行全表扫描,然后过滤记录,输出信息里面没有value_out_of_range这类信息,似乎没有发生数据截断。具体步骤跟之前的trace信息有很大不同。具体只看到了下面这些信息,但是更多信息我也看不出来。不清楚底层到底做了啥。
小结

关于bit类型的字段,我们写SQL的时候,不要使用字符串,避免发生隐式类型转换。正确的写法应该是下面这种方式
  1. select * from student_attend where is_attend=b'1';

  2. select * from student_attend where is_attend=1;
复制代码
DBA在给bit类型创建索引的时候也必须谨慎处理,跟开发和Support人员多协商沟通,告知他们可能出现这种情况,因为你可能没法控制开发人员写出这样的SQL。
扫描上面二维码关注我如果你真心觉得文章写得不错,而且对你有所帮助,那就不妨帮忙“推荐"一下,您的“推荐”和”打赏“将是我最大的写作动力!本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接.
来源:https://www.cnblogs.com/kerrycode/p/18260767
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具