where 中使用 case when 无法使用索引

Posted 渔夫数据库笔记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了where 中使用 case when 无法使用索引相关的知识,希望对你有一定的参考价值。

1.版本

1)操作系统

#cat /proc/version
Linux version 4.14.76-1.el7.rx.x86_64 (root@8dfc8db5e7914181ad0cf0484d9be73a) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC)) #1 SMP Thu Oct 18 09:52:06 +06 2018

2)mysql数据库版本

> select version();
+-----------+
| version() |
+-----------+
| 8.0.19    |
+-----------+
1 row in set (0.00 sec)

 

2.问题描述

    一个朋友的数据库经常出现cpu使用率彪高的现象(正常时他的数据库cpu使用率在20%左右,异常时上升到 50%-60%),通过排查发现一类可疑的sql(在 where 条件中使用了 case when),他的原始sql我就不发了。我在测试环境写了一个更能看出问题的sql如下

select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end;

1. 表结构及统计信息如下:
[root@127.0.0.1][test_shao]> show create table test_user;
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table     | Create Table                                                                                                                                                                                                                                                                                                                                                                                                           |
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| test_user | CREATE TABLE `test_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_name` varchar(80) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `email` varchar(200) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `tel` varchar(30) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci |
+-----------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

[root@127.0.0.1][test_shao]> show table status like 'test_user';
+-----------+--------+---------+------------+----------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+--------------------+----------+----------------+---------+
| Name      | Engine | Version | Row_format | Rows     | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time         | Update_time | Check_time | Collation          | Checksum | Create_options | Comment |
+-----------+--------+---------+------------+----------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+--------------------+----------+----------------+---------+
| test_user | InnoDB |      10 | Dynamic    | 99363553 |             99 |  9852420096 |               0 |            0 |   7340032 |      100000001 | 2021-02-02 15:53:23 | NULL        | NULL       | utf8mb4_general_ci |     NULL |                |         |
+-----------+--------+---------+------------+----------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+--------------------+----------+----------------+---------+
1 row in set (0.07 sec)


2.第一类 select 测试
[root@127.0.0.1][test_shao]> explain select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end;
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | test_user | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 99363553 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
#我们看到查询并不会走 id 列的主键索引,走的是全表扫描

[root@127.0.0.1][test_shao]> select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end;
+----+-------------+--------------------------+---------------------+-------------+
| id | user_name   | email                    | create_time         | tel         |
+----+-------------+--------------------------+---------------------+-------------+
|  1 | user_test_1 | user_test_1xxxxxx@qq.com | 2021-02-02 15:54:56 | 15955827468 |
|  2 | user_test_2 | user_test_2xxxxxx@qq.com | 2021-02-02 15:54:56 | 15957465968 |
|  3 | user_test_3 | user_test_3xxxxxx@qq.com | 2021-02-02 15:54:56 | 15919847440 |
|  4 | user_test_4 | user_test_4xxxxxx@qq.com | 2021-02-02 15:54:56 | 15926839297 |
|  5 | user_test_5 | user_test_5xxxxxx@qq.com | 2021-02-02 15:54:56 | 15974654170 |
|  6 | user_test_6 | user_test_6xxxxxx@qq.com | 2021-02-02 15:54:56 | 15992752962 |
|  7 | user_test_7 | user_test_7xxxxxx@qq.com | 2021-02-02 15:54:56 | 15939802303 |
|  8 | user_test_8 | user_test_8xxxxxx@qq.com | 2021-02-02 15:54:56 | 15920752632 |
|  9 | user_test_9 | user_test_9xxxxxx@qq.com | 2021-02-02 15:54:56 | 15984356256 |
+----+-------------+--------------------------+---------------------+-------------+
9 rows in set (1 min 58.28 sec)

该查询的慢日志如下:
# User@Host: root[root] @  [127.0.0.1]  Id:    17
# Query_time: 118.227921  Lock_time: 0.001371 Rows_sent: 9  Rows_examined: 100000000
SET timestamp=1618278221;
select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end;

#从慢日志中我们可以看出来该sql扫描了表中所有的记录(Rows_examined: 100000000),最终返回9条记录(id<10)



3.第二类 select 测试 
[root@127.0.0.1][test_shao]> explain select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end limit 20;
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | test_user | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 99363553 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
#该sql依然是全表扫描

[root@127.0.0.1][test_shao]> select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end limit 20;
+----+-------------+--------------------------+---------------------+-------------+
| id | user_name   | email                    | create_time         | tel         |
+----+-------------+--------------------------+---------------------+-------------+
|  1 | user_test_1 | user_test_1xxxxxx@qq.com | 2021-02-02 15:54:56 | 15955827468 |
|  2 | user_test_2 | user_test_2xxxxxx@qq.com | 2021-02-02 15:54:56 | 15957465968 |
|  3 | user_test_3 | user_test_3xxxxxx@qq.com | 2021-02-02 15:54:56 | 15919847440 |
|  4 | user_test_4 | user_test_4xxxxxx@qq.com | 2021-02-02 15:54:56 | 15926839297 |
|  5 | user_test_5 | user_test_5xxxxxx@qq.com | 2021-02-02 15:54:56 | 15974654170 |
|  6 | user_test_6 | user_test_6xxxxxx@qq.com | 2021-02-02 15:54:56 | 15992752962 |
|  7 | user_test_7 | user_test_7xxxxxx@qq.com | 2021-02-02 15:54:56 | 15939802303 |
|  8 | user_test_8 | user_test_8xxxxxx@qq.com | 2021-02-02 15:54:56 | 15920752632 |
|  9 | user_test_9 | user_test_9xxxxxx@qq.com | 2021-02-02 15:54:56 | 15984356256 |
+----+-------------+--------------------------+---------------------+-------------+
9 rows in set (1 min 58.02 sec)
#执行时间同未加 limit 限制条件的sql 也差不多

该sql的慢日志如下:
# User@Host: root[root] @  [127.0.0.1]  Id:    19
# Query_time: 118.015081  Lock_time: 0.000325 Rows_sent: 9  Rows_examined: 100000000
SET timestamp=1618280076;

#从慢日志中我们可以看出,该sql也是扫描了整张表中的记录,最后返回了9条记录


3.第三类 select 测试(修改 limit限制的值)
[root@127.0.0.1][test_shao]> explain select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end limit 9;
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | test_user | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 99363553 |   100.00 | Using where |
+----+-------------+-----------+------------+------+---------------+------+---------+------+----------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
#执行计划我们看到同上面两个sql一致

[root@127.0.0.1][test_shao]> select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end limit 9;
+----+-------------+--------------------------+---------------------+-------------+
| id | user_name   | email                    | create_time         | tel         |
+----+-------------+--------------------------+---------------------+-------------+
|  1 | user_test_1 | user_test_1xxxxxx@qq.com | 2021-02-02 15:54:56 | 15955827468 |
|  2 | user_test_2 | user_test_2xxxxxx@qq.com | 2021-02-02 15:54:56 | 15957465968 |
|  3 | user_test_3 | user_test_3xxxxxx@qq.com | 2021-02-02 15:54:56 | 15919847440 |
|  4 | user_test_4 | user_test_4xxxxxx@qq.com | 2021-02-02 15:54:56 | 15926839297 |
|  5 | user_test_5 | user_test_5xxxxxx@qq.com | 2021-02-02 15:54:56 | 15974654170 |
|  6 | user_test_6 | user_test_6xxxxxx@qq.com | 2021-02-02 15:54:56 | 15992752962 |
|  7 | user_test_7 | user_test_7xxxxxx@qq.com | 2021-02-02 15:54:56 | 15939802303 |
|  8 | user_test_8 | user_test_8xxxxxx@qq.com | 2021-02-02 15:54:56 | 15920752632 |
|  9 | user_test_9 | user_test_9xxxxxx@qq.com | 2021-02-02 15:54:56 | 15984356256 |
+----+-------------+--------------------------+---------------------+-------------+
9 rows in set (0.00 sec)
#但是sql执行起来却很快

该sql的慢查询日志如下:
# User@Host: root[root] @  [127.0.0.1]  Id:    20
# Query_time: 0.000758  Lock_time: 0.000294 Rows_sent: 9  Rows_examined: 9
SET timestamp=1618281582;
select * from test_user where case when '01' between '00' and '11' then id<10 else id<5 end limit 9;

#从该sql的慢查日志中我们看到该sql最终只扫描了9条记录(limit 9)

NOTE1:从上面的例子中我们看到,对于上面的sql 不加limit 限制,或者加的limit限制 大于9时,sql都是扫描整张表的数据。为什么没有使用主键索引索引?因为我们对索引列使用了函数,所以导致索引列失效(case when 函数)

NOTE2:全表扫描走索引链表的最小端往最大端扫描时扫描到id>=10之后还在继续往后扫描,并未终止扫描(上面测试中未加limit以及limit 大于10的情况),这是为什么?确实在一直往下扫描,没搞清楚为什么

NOTE3:这时使用 use index() 或者 force index() 都无法让如下sql走主键索引

 

以上是关于where 中使用 case when 无法使用索引的主要内容,如果未能解决你的问题,请参考以下文章

在 WHERE 中使用“CASE WHEN”语句进行查询会导致 QuerySyntaxException:意外 AST

在where条件中使用CASE WHEN 语句

case when 使用

sql语句借助case when实现自动拼装where条件

跨表使用group by,case when的问题

sql中where后面用case的问题