在前面的文章中我们写了一些会导致索引失效的场景,比如《MySQL中,IS NULL和IS NOT NULL不会走索引?错!》一文。
今天这篇文章再介绍另外一种非常隐蔽,但又很容易导致索引失效的场景:隐式类型转换。隐式类型转换使用不当,轻则导致索引失效,性能急剧下降,重则会导致SQL语句未查询到预期的结果。
什么是隐式类型转换?
MySQL中的隐式类型转换规则是在查询或操作(如比较、函数调用等)中,涉及到不同数据类型时发生的自动转换行为。
如果参与操作的表达式或列的数据类型不匹配,MySQL会根据数据类型的上下文自动进行数据类型转换以适配预期的数据类型。这种行为对性能和结果有时会有较大的影响,比如索引可能失效或比较结果出现意外。
隐式类型转换典型案例
在进一步介绍隐式类型转换的详细规则之前,我们先来看两个比较典型的案例,这里采用的MySQL 8.0.37版本。
场景一:未获得预期数据,且索引失效
创建&初始化示例表
表结构及数据如下:
sql
-- tb_type_change
CREATE TABLE `tb_type_change` (
`col1` varchar(255) NOT NULL DEFAULT '',
`col2` int NOT NULL DEFAULT '0',
KEY `idx_c1` (`col1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入一些数据
insert into tb_type_change values('1234567890123456789',1);
insert into tb_type_change values('123456789012345678',2);
insert into tb_type_change values('123456789012345677',3);
insert into tb_type_change values('12345678901234567',4);
insert into tb_type_change values('12345678901234568',5);
insert into tb_type_change values('123456789012345',6);
查询所有数据:
sql
mysql> select * from tb_type_change;
+---------------------+------+
| col1 | col2 |
+---------------------+------+
| 1234567890123456789 | 1 |
| 123456789012345678 | 2 |
| 123456789012345677 | 3 |
| 12345678901234567 | 4 |
| 12345678901234568 | 5 |
| 123456789012345 | 6 |
+---------------------+------+
未获得预期数据示例
执行如下SQL语句:
sql
mysql> select * from tb_type_change where col1 = 123456789012345678;
+--------------------+------+
| col1 | col2 |
+--------------------+------+
| 123456789012345678 | 2 |
| 123456789012345677 | 3 |
+--------------------+------+
通过上面的查询可以看到,SQL语句正常执行,但查询的结果并不是预期的结果。查询条件是123456789012345678
,但结构中竟然包含了"123456789012345677"
。这就是因为隐式类型转换导致未获得预期数据。
原因分析
在 MySQL 中,当查询条件中的数据类型与列的数据类型不匹配时,会发生隐式类型转换。隐式类型转换通常会将查询条件类型转换为列的类型或反之。如果转换过程中出现精度丢失或者未精确匹配,就可能导致查询结果不符合预期。
在上述场景中,表 tb_type_change
中列 col1
的类型是 VARCHAR
,但查询条件 123456789012345678
是一个数字(BIGINT
型)。MySQL 会尝试将 VARCHAR
列的数据转换为 BIGINT
类型以进行比较。
BIGINT
的范围是-9223372036854775808
到9223372036854775807
,但在比较时,MySQL 会将col1
的值从字符串转换为数字。- 转换过程中,如果
col1
的字符串值超过BIGINT
的最大精度范围,MySQL 会截断或丢失部分精度,使得原始字符串被转换为近似的BIGINT
值。
在上述示例中,关于类型的转换:
'123456789012345677'
转换为123456789012345678
(数字,伴随一定的舍入)。'123456789012345678'
转换为123456789012345678
(数字)。
因此,上述SQL语句查询出了两个结果。
场景二:隐式类型转换,索引失效
创建&初始化示例表
新创建一个表以及插入一些数据:
sql
CREATE TABLE test (
col1 VARCHAR(255) NOT NULL DEFAULT '',
col2 INT NOT NULL,
KEY idx_col1 (col1), -- 对字符串列 col1 建立了索引
KEY idx_col2 (col2) -- 对整数列 col2 建立了索引
);
INSERT INTO test (col1, col2) VALUES
('123', 123),
('456', 456),
('789', 789),
('abc', 111),
('xyz', 222);
未走索引,全表扫描示例
执行如下SQL语句:
yaml
mysql> EXPLAIN SELECT * FROM test WHERE col1 = 123 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ALL
possible_keys: idx_col1
key: NULL
key_len: NULL
ref: NULL
rows: 5
filtered: 20.00
Extra: Using where
会发现上面的SQL语句,并没有走idx_col1
索引,而是进行了全表扫描。
正常使用索引示例
再执行另外一个SQL语句:
yaml
mysql> EXPLAIN SELECT * FROM test WHERE col2 = '123' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: test
partitions: NULL
type: ref
possible_keys: idx_col2
key: idx_col2
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
此时,针对col2
列的查询,正确使用了idx_col2
索引。
原因分析
在上面的示例中:
- 当字段类型为字符串类型,参数为整型时,会导致索引失效;
- 而字段类型为整型,传入的参数为字符串类型时,不会导致索引失效;
这是因为在字符串与数字进行比较时,MySQL会将字符串类型转换为数字进行比较,因此当字段类型为字符串时,会在字段上加函数,而导致索引失效。
MySQL隐式类型转换规则
下面我们整理一些在MySQL当中常见的隐式类型转换规则。
字符串与数字
当一个字符串与一个数字进行比较时,字符串会被转换为一个数字。转换是基于字符串的数值前缀。如果字符串没有数值前缀,则转换结果为 0。上面的案例中,便是字符串转数字的场景之一。
示例:
ini
SELECT '6' = 6; -- 返回 1(TRUE),因为字符串 '6' 被转换为数字 6。
SELECT '6a' = 6; -- 返回 1(TRUE),因为字符串 '6a' 在转换时被认定为数字 6。
不同类型的数值
不同类型的数值(例如 INT 和 DOUBLE)在比较时会转换为精度更高的数值类型。
示例:
ini
SELECT 5 = 5.0; -- 返回 1(TRUE),整型 5 转换为浮点数 5.0 进行比较。
数值与日期比较
日期格式的数据和整型比较时会将整型转化为日期格式,但是日期格式的字符串和整型比较会将日期字符串转化为整型。
sql
SELECT CAST('20230101' as date)=20230101; -- 返回 1(TRUE)
SELECT DATE'2023-01-01' =20230101; -- 返回 1(TRUE)
SELECT '2023-01-01'=20230101; -- 返回 0(FALSE)
SELECT '2023-01-01'=2023; -- 返回 1(TRUE)
其他规则
- 两个参数至少有一个是
NULL
时,比较的结果也是NULL
,例外是使用<=>
对两个NULL
做比较时会返回 1,这两种情况都不需要做类型转换; - 两个参数都是字符串,会按照字符串来比较,不做类型转换;
- 两个参数都是整数,按照整数来比较,不做类型转换;
- 十六进制的值和非数字做比较时,会被当做二进制串;
- 有一个参数是
TIMESTAMP
或DATETIME
,并且另外一个参数是常量,常量会被转换为TIMESTAMP
; - 有一个参数是
decimal
类型,如果另外一个参数是decimal
或者整数,会将整数转换为decimal
后进行比较,如果另外一个参数是浮点数,则会把decimal
转换为浮点数进行比较; - 同一类型内部的转换。例如,比较
TINYINT
和BIGINT
时,TINYINT
会被转换为BIGINT
。 - 使用
BLOB
或TEXT
类型时,应尽量避免使用不同类型的字面值,因为这可能导致意外的类型转换或比较结果。
小结
隐式转换的类型主要有字段类型不一致、IN
参数包含多个类型、字符集类型或校对规则不一致等。数据库在进行隐式转换时,如果转换无法正常进行或产生了错误的结果,可能会影响查询的准确性和性能。因此,在设计数据库和编写 SQL 查询时,需仔细甄别,最好显式指定所需的数据类型,以避免潜在的问题。