乐趣区

关于mysql:MySQL为什么varchar字段用数字查无法命中索引而int字段用字符串查却能命中

字符串字段误应用数字进行查问,会导致隐式类型转换,无奈命中索引的坑我置信大多数小伙伴都踩过。
特地是当字段中存的大多数数据都是数字时,很容易先入为主地认为字段是 int 类型,谬误地应用相似 where file_id=123456789 执行了查问。好一点的可能当时通过 Explain 命令查看语句的执行打算,发现居然没用命中索引,从而纠正错误;杯具一点的代码公布上线后呈现大量慢查问,数据库服务器的 CPU 使用率和磁盘 IO 飙升,酿成生产事变。

而仔细的小伙伴肯定会发现,尽管 varchar 字段用数字查无奈命中索引,而 int 字段用字符串查却通常能很快查出后果。这是为什么呢?

上面咱们通过理论测试来阐明呈现这种景象的起因。
测试用 MySQL 版本为 5.7.18,数据表 file 构造如下,存储引擎为 InnoDB,表数据条数为 5 百万 +。

mysql> SELECT VERSION();
+---------------------+
| VERSION()           |
+---------------------+
| 5.7.18-20170830-log |
+---------------------+

mysql> DESC `file`;
+----------+---------------------+------+-----+---------+----------------+
| Field    | Type                | Null | Key | Default | Extra          |
+----------+---------------------+------+-----+---------+----------------+
| id       | int(11)             | NO   | PRI | NULL    | auto_increment |
| fs_id    | varchar(20)         | NO   | MUL | NULL    |                |
| filename | varchar(255)        | NO   |     | NULL    |                |
| shareid  | bigint(20) unsigned | NO   | MUL | NULL    |                |
| uk       | bigint(20) unsigned | NO   |     | NULL    |                |
| pid      | varchar(32)         | NO   |     | NULL    |                |
+----------+---------------------+------+-----+---------+----------------+

mysql> SELECT COUNT(*) FROM `file`;
+----------+
| COUNT(*) |
+----------+
|  5416697 |
+----------+

varchar 字段用数字进行查问

数据表 file 中的 fs_id 字段是 varchar 类型,并且建设了一般索引 idx_fs_id
当应用字符串进行查问时,耗时 0.07 秒。
通过 EXPLAIN 命令查看执行打算,结果表明查问时应用了 fs_id 字段的索引。

mysql> SELECT * FROM `file` WHERE `fs_id`='635341798980956';
+---------+-----------------+-------------+------------+------------+---------+
| id      | fs_id           | filename    | shareid    | uk         | pid     |
+---------+-----------------+-------------+------------+------------+---------+
| 1043170 | 635341798980956 | ⑮MySQL 高级 | 3181065465 | 3959617630 | o6RlSp0 |
+---------+-----------------+-------------+------------+------------+---------+
1 row in set (0.07 sec)

mysql> EXPLAIN SELECT * FROM `file` WHERE `fs_id`='635341798980956';
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key       | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | file  | NULL       | ref  | idx_fs_id     | idx_fs_id | 62      | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+-----------+---------+-------+------+----------+-------+

然而,当应用数字进行查问时,耗时 7.04 秒。
通过 EXPLAIN 命令查看执行打算,发现查问时进行了全表扫描,并未应用到索引。

mysql> SELECT * FROM `file` WHERE `fs_id`=635341798980956;
+---------+-----------------+-------------+------------+------------+---------+
| id      | fs_id           | filename    | shareid    | uk         | pid     |
+---------+-----------------+-------------+------------+------------+---------+
| 1043170 | 635341798980956 | ⑮MySQL 高级 | 3181065465 | 3959617630 | o6RlSp0 |
+---------+-----------------+-------------+------------+------------+---------+
1 row in set (7.04 sec)

mysql> EXPLAIN SELECT * FROM `file` WHERE `fs_id`=635341798980956;
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | file  | NULL       | ALL  | idx_fs_id     | NULL | NULL    | NULL | 4878670 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+

fs_id 字段明明建设了索引,然而应用数字进行查问时,还是须要全表扫描。
之所以呈现这种状况,置信大多数小伙伴都晓得,是因为 fs_id 字段是字符串类型,而输出参数却是整数类型,所以触发了 隐式类型转换

int 字段用字符串查进行查问

数据表 file 中的 shareid 字段是 bigint 类型,并且建设了一般索引 idx_shareid
当应用数字和字符串进行查问时,耗时都是 0.04 秒。
通过 EXPLAIN 命令查看执行打算,结果表明无论数字两边加不加引号,查问时应用了 idx_shareid 字段的索引。

mysql> SELECT * FROM `file` WHERE `shareid`=3181065465;
+---------+-----------------+-------------+------------+------------+---------+
| id      | fs_id           | filename    | shareid    | uk         | pid     |
+---------+-----------------+-------------+------------+------------+---------+
| 1043170 | 635341798980956 | ⑮MySQL 高级 | 3181065465 | 3959617630 | o6RlSp0 |
+---------+-----------------+-------------+------------+------------+---------+
1 row in set (0.04 sec)

mysql> SELECT * FROM `file` WHERE `shareid`='3181065465';
+---------+-----------------+-------------+------------+------------+---------+
| id      | fs_id           | filename    | shareid    | uk         | pid     |
+---------+-----------------+-------------+------------+------------+---------+
| 1043170 | 635341798980956 | ⑮MySQL 高级 | 3181065465 | 3959617630 | o6RlSp0 |
+---------+-----------------+-------------+------------+------------+---------+
1 row in set (0.04 sec)

mysql> EXPLAIN SELECT * FROM `file` WHERE `shareid`=3181065465;
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key         | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | file  | NULL       | ref  | idx_shareid   | idx_shareid | 8       | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-------+

mysql> EXPLAIN SELECT * FROM `file` WHERE `shareid`='3181065465';
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key         | key_len | ref   | rows | filtered | Extra                 |
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+
|  1 | SIMPLE      | file  | NULL       | ref  | idx_shareid   | idx_shareid | 8       | const |    1 |   100.00 | Using index condition |
+----+-------------+-------+------------+------+---------------+-------------+---------+-------+------+----------+-----------------------+

对于这个后果,当初有三个疑难:

  1. 隐式类型转换的规定是什么?
  2. 为什么触发隐式类型转换,查问数据时就须要全表扫描?
  3. 为什么 int 字段用字符串查就能命中索引?

1. 隐式类型转换的规定是什么?

有一个非常简单的办法,能够验证隐式类型转换的规定,就是看 SELECT '10' > 9SELECT 9 > '10' 的后果:

如果规定是 “将字符串转成数字”,那么就是做数字比拟,SELECT '10' > 9 的后果应该是 1,SELECT 9 > '10' 的后果应该是 0;
如果规定是 “将数字转成字符串”,那么就是做字符串比拟,SELECT '10' > 9 的后果应该是 0,SELECT 9 > '10' 的后果应该是 1。

mysql> SELECT '10' > 9;
+----------+
| '10' > 9 |
+----------+
|        1 |
+----------+
mysql> SELECT 9 > '10';
+----------+
| 9 > '10' |
+----------+
|        0 |
+----------+

可见,SELECT '10' > 9 的后果是 1,SELECT 9 > '10' 的后果是 0,所以能够确认 MySQL 类型转换规定是:当字符串和数字做比拟时,会将字符串转换成数字

因而,当咱们应用上面语句进行查问时:

SELECT * FROM `file` WHERE `fs_id`=635341798980956;

对于 MySQL 优化器来说,这个语句相当于将 fs_id 转换成 int 类型再与输出的值进行比拟:

SELECT * FROM `file` WHERE CAST(`fs_id` AS signed INT)=635341798980956;

家喻户晓,如果查问时对索引字段进行函数操作,查问过程将无奈应用索引

2. 为什么触发隐式类型转换,查问数据时就须要全表扫描?

对于 InnoDB 的 B+ 树索引构造,置信大多数小伙伴都有肯定的理解。

示例有上面一组数据:

1, 2, 3, 4, 6, 6, 7, 11, 13, 21, 23, 39, 42, 61, 71, 
101, 201, 220, 303, 345, 411, 601, 620, 701, 1402, 3333

当作为数值类型建设索引时,B+ 树索引构造如下:

当作为字符串类型建设索引时,数据程序和 B + 树索引构造如下:

1, 101, 11, 13, 1402, 2, 201, 21, 220, 23, 3, 303, 3333, 
345, 39, 4, 411, 42, 6, 6, 601, 61, 620, 7, 701, 71

实际上,B+ 树索引的疾速定位能力,来源于同一层兄弟节点的有序性 对索引字段做函数操作,可能会毁坏索引值的有序性
当字符串和数字做比拟时,会将字符串转换成数字

因而,当字符串类型的字段 fs_id 接管到数值类型的输出参数时,fs_id 会被转换成数值类型,字符串类型建设的索引对于数值类型来说是乱序的,因而无奈应用 fs_id 字段的索引,只能通过全表扫描进行查找。

3. 为什么 int 字段用字符串查就能命中索引?

是因为 数值类型字段用字符串查问 不会触发隐式类型转换吗?并不是。

通过下面验证,咱们已知:当字符串和数字做比拟时,会将字符串转换成数字

因而,当咱们应用字符串类型作为输出参数对数值型字段进行查问时:

SELECT * FROM `file` WHERE `shareid`='3181065465'

对于 MySQL 优化器来说,这个语句相当于将输出参数 '3181065465' 转换成 int 类型再进行查问:

SELECT * FROM `file` WHERE `shareid`=CAST('3181065465' AS signed INT);

而对等号前面的输出参数进行函数操作,是不影响字段 shareid 的索引应用的。因而,尽管数值类型字段用字符串查问也会产生隐式类型转换,然而并不影响字段索引的应用。

总结

MySQL 查问中,当字符串和数字做比拟时,会触发隐式类型转换,转换规则是将字符串转换成数字。

当索引字段是字符串类型,输出参数是数值类型时,会将字段转换成数值类型再进行查找,也就是对索引字段做了函数操作,毁坏了索引的有序性,因而无奈应用索引。
当索引字段是数值类型,输参数是字符串类型时,会将输出参数转换成数值类型再进行查找,对等号前面的输出参数进行函数操作,并不影响索引字段的有序性,因而能够应用索引。

退出移动版