在mysql查詢中,當查詢條件左右兩側類型不匹配的時候會發生隱式轉換,可能導致查詢無法使用索引。下面分析兩種隱式轉換的情況。
第一種情況:索引字段是varchar類型
select * from user where index_filed=2;
因為等號兩側類型不一致,因此會發生隱式轉換,cast(index_filed as signed),然后和2進行比較。因為'2',' 2','2a'都會轉化成2,故MySQL無法使用索引只能進行全表掃描,造成了慢查詢的產生。
第二種情況:索引字段是int類型
select * from user where index_filed='2';
這次等號右側是'2',注意帶單引號喲,左側的索引字段是int類型,因此也會發生隱式轉換,但因為int類型的數字只有2能轉化為'2',是唯一確定的。所以雖然需要隱式轉換,但不影響使用索引,不會導致慢查詢。
問題如下下所示
問題描述
where 條件語句里,字段屬性和賦給的條件,當數據類型不一樣,這時候是沒法直接比較的,需要進行一致轉換
默認轉換規則是:
①:不同類型全都轉換為浮點型(下文都說成整型了,一個意思)
②:如果字段是字符,條件是整型,那么會把表中字段全都轉換為整型(也就是上面圖中的問題,下面有詳細解釋)
轉換總結:
1:字符轉整型
①:字符開頭的一律為0
②:數字開頭的,直接截取到第一個不是字符的位置
2:時間類型轉換
①:date 轉 datetime 或者 timestamp 追加 00:00:00
②:date 轉 time 無意義,直接為 00:00:00
③:datetime 或者 timestamp 轉 date 直接截取date字段
④:datetime 或者 timestamp 轉 time 直接截取time字段
⑤:time 轉 datetime 或者 timestamp 按照字符串進行截取 23:12:13 -> 2023-12-13(這個后文有討論)
⑥:cast函數只能轉datetime,不能轉timestamp
如果按照timestamp來理解,因為timestamp是有范圍的('1970-01-01 00:00:01.000000' to'2038-01-19 03:14:07.999999'),所以只能是2023年,而不能是1923年
對於不符合的時間值,如10:12:32等,會變為 0000-00-00 或為 空
⑦:time和datetime轉換為數字時,會變為雙精度,加上ms(版本不同不一樣)
案例分析
表結構,name字段有索引
-- 注意name字段是有索引的
CREATE TABLE `t3` ( `id` int(11) NOT NULL, `c1` int(11) NOT NULL, `name` varchar(100) NOT NULL DEFAULT 'fajlfjalfka', KEY `name` (`name`), KEY `id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 1 row in set (0.00 sec)
-- 模擬線上一個隱式轉換帶來的全表掃面慢查詢
-- 發生隱式轉換
xxxx.test> select * from t3 where name = 0; +----+----+-------------+
| id | c1 | name |
+----+----+-------------+
| 1 | 2 | fajlfjalfka |
| 2 | 0 | fajlfjalfka |
| 1 | 2 | fajlfjalfka |
| 2 | 0 | fajlfjalfka |
+----+----+-------------+
4 rows in set, 4 warnings (0.00 sec) -- 上述SQL執行計划是全表掃描,掃描后,字符轉整型,都是0,匹配上了條件,全部返回
xxxx.test> desc select * from t3 where name = 0; +----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | t3 | ALL | name | NULL | NULL | NULL | 4 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec) -- 加上單引號后,是走name索引的,非全表掃描
xxxx.test> desc select * from t3 where name = '0'; +----+-------------+-------+------+---------------+------+---------+-------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+-------+------+-----------------------+
| 1 | SIMPLE | t3 | ref | name | name | 102 | const | 1 | Using index condition |
+----+-------------+-------+------+---------------+------+---------+-------+------+-----------------------+
1 row in set (0.00 sec) -- 走索引,沒返回
xxxx.test> select * from t3 where name = '1'; Empty set (0.00 sec)
解釋:
如果條件寫0或者1,會進行全表掃面,需要把所有的name字段由字符全都轉換為整型,再和0或者1去比較。由於都是字母開頭的字符,會全都轉為為0,返回的結果就是所有行。
那有人問了,為什么不把條件里的 0 自動改成 '0' ?見下文。
轉換舉例:
-- 字符開頭,直接是0
xxxx.test> select cast('a1' as unsigned int) as test ; +------+
| test |
+------+
| 0 |
+------+
1 row in set, 1 warning (0.00 sec) xxxx.test> show warnings; +---------+------+-----------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------+
| Warning | 1292 | Truncated incorrect INTEGER value: 'a1' |
+---------+------+-----------------------------------------+
1 row in set (0.00 sec) -- 開頭不是字符,一直截取到第一個不是字符的位置
xxxx.test> select cast('1a1' as unsigned int) as test ; +------+
| test |
+------+
| 1 |
+------+
1 row in set, 1 warning (0.00 sec) xxxx.test> select cast('123a1' as unsigned int) as test ; +------+
| test |
+------+
| 123 |
+------+
1 row in set, 1 warning (0.00 sec) -- 直接按照字符截取,補上了20(不能補19)
xxxx.test> select cast('23:12:13' as datetime) as test ; +---------------------+
| test |
+---------------------+
| 2023-12-13 00:00:00 |
+---------------------+
1 row in set (0.00 sec) -- 為什么不能轉換為timestamp,沒搞清楚,官方文檔給的轉換類型里沒有timestamp。如果是這樣的話,上面的datetime就不好解釋為什不是1923了。難道是檢測了當前的系統時間?
xxxx.test> select cast('23:12:13' as timestamp) as test ; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'timestamp) as test' at line 1
-- 這個時間無法轉換成datetime
xxxx.test> select cast('10:12:32' as datetime) as test ; +------+
| test |
+------+
| NULL |
+------+
1 row in set, 1 warning (0.00 sec) xxxx.test> show warnings ; +---------+------+--------------------------------------+
| Level | Code | Message |
+---------+------+--------------------------------------+
| Warning | 1292 | Incorrect datetime value: '10:12:32' |
+---------+------+--------------------------------------+
1 row in set (0.00 sec) -- 5.5版本下,時間轉字符,會增加ms
xxxx.(none)> select version(); +------------+
| version() |
+------------+
| 5.5.31-log |
+------------+
1 row in set (0.00 sec) xxxx.(none)> select CURTIME(), CURTIME()+0, NOW(), NOW()+0 ; +-----------+---------------+---------------------+-----------------------+
| CURTIME() | CURTIME()+0 | NOW() | NOW()+0 |
+-----------+---------------+---------------------+-----------------------+
| 15:40:01 | 154001.000000 | 2016-05-06 15:40:01 | 20160506154001.000000 |
+-----------+---------------+---------------------+-----------------------+
1 row in set (0.00 sec) -- 5.6 不會
xxxx.test> select version(); +------------+
| version() |
+------------+
| 5.6.24-log |
+------------+
1 row in set (0.00 sec) xxxx.test> select CURTIME(), CURTIME()+0, NOW(), NOW()+0 ; +-----------+-------------+---------------------+----------------+
| CURTIME() | CURTIME()+0 | NOW() | NOW()+0 |
+-----------+-------------+---------------------+----------------+
| 15:40:55 | 154055 | 2016-05-06 15:40:55 | 20160506154055 |
+-----------+-------------+---------------------+----------------+
1 row in set (0.00 sec)
為什么不把 where name = 0 中的 0 轉換為 '0' 答案如下:
如果是數字往字符去轉換,如 0 轉'0',這樣查詢出來的結果只能是字段等於 '0',而實際上,表里的數據,如'a0','00',這其實都是用戶想要的0,畢竟是用戶指定了數字0,所以MySQL還是以用戶發出的需求為准,否則,'00'這些都不會返回給用戶。
-- 上面遺留的問題,跟系統時間並沒有關系。懷疑雖然指定的是datetime,但是內部還是按照timestamp去做的。
mysql> select now(); +---------------------+
| now() |
+---------------------+
| 1999-08-03 14:16:50 |
+---------------------+
1 row in set (0.00 sec) mysql> select cast('23:12:13' as datetime) as test ; +---------------------+
| test |
+---------------------+
| 2023-12-13 00:00:00 |
+---------------------+
1 row in set (0.00 sec)
查詢示例如下所示:
CREATE TABLE `t_user` ( `id` bigint(100) NOT NULL AUTO_INCREMENT, `password` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL, `phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `age` int(11) DEFAULT NULL, `nick_name` varchar(255) DEFAULT NULL, `sex` int(255) DEFAULT NULL, `birthday` datetime DEFAULT NULL, `class_id` int(11) DEFAULT NULL COMMENT '班級id', PRIMARY KEY (`id`), KEY `index_phone` (`phone`) USING BTREE, KEY `index_birthday` (`birthday`) USING BTREE, KEY `index_name` (`name`,`age`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1588138958133 DEFAULT CHARSET=utf8;
explain select age from t_user where age = '32'; --- 走索引
explain select age from t_user where age = 32; --- 走索引
explain select * from t_user where name = 32; --- 不走索引
explain select * from t_user where name = '32'; --- 走索引
explain select * from t_user where birthday = '2020-09-09'; --- 走索引
explain select * from t_user where DATE_FORMAT(birthday,'yyyy-MM-dd') = '2020-09-09'; ---不走索引
隱式轉換如上所示:不要再列上進行函數計算,比如使用date_format函數等,int類型的數值可以是整型或者是字符串類型都是可以的,但是字符串類型的數據的數值必須是字符串,否則會進行全表掃描,不走索引。