暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

小心 MySQL 的隐式类型转换陷阱

SOHU-DBA 2016-07-27
478


小心 MySQL 的隐式类型转换陷阱



1. 隐式类型转换实例


今天生产库上突然出现 MySQL 线程数告警,IOPS 很高,实例会话里面出现许多类似下面的 sql:(修改了相关字段和值)


1
2
复制
SELECT f_col3_id,f_qq1_id FROM d_dbname.t_tb1 WHERE f_col1_id=1226391 and f_col2_id=1244378 and 
f_qq1_id in (12345,23456,34567,45678,56789,67890,78901,89012,90123,901231,901232,901233)
复制



用 explain 看了下扫描行数和索引选择情况:


1
2
3
4
5
6
7
8
复制
mysql>explain SELECT f_col3_id,f_qq1_id FROM d_dbname.t_tb1 WHERE f_col1_id=1226391 
and f_col2_id=1244378 and f_qq1_id in (12345,23456,34567,45678,56789,67890,78901,89012,90123,901231,901232,901233);
+------+---------------+---------+--------+--------------------------------+---------------+------------+--------+--------+------------------------------------+

| id   | select_type   | table   | type   | possible_keys                  | key           | key_len    | ref    | rows   | Extra                              |
+------+---------------+---------+--------+--------------------------------+---------------+------------+--------+--------+------------------------------------+

| 1    | SIMPLE        | t_tb1   | ref    | uid_type_frid,idx_corpid_qq1id | uid_type_frid | 8          | const  | 1386   | Using index condition; Using where |
+------+---------------+---------+--------+--------------------------------+---------------+------------+--------+--------+------------------------------------+

共返回 1 行记录,花费 11.52 ms.
复制



t_tb1 表上有个索引uid_type_frid(f_col2_id,f_type)
idx_corp_id_qq1id(f_col1_id,f_qq1_id)
,而且如果选择后者时,f_qq1_id 的过滤效果应该很佳,但却选择了前者。当使用 hint use index(idx_corp_id_qq1id)
时:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制
mysql>explain extended SELECT f_col3_id,f_qq1_id FROM d_dbname.t_tb1  use index(idx_corpid_qq1id) WHERE f_col1_id=1226391 and f_col2_id=1244378 and f_qq1_id in (12345,23456,34567,45678,56789,67890,78901,89012,90123,901231,901232,901233);
+------+---------------+--------+--------+---------------------+------------------+------------+----------+-------------+------------------------------------+
| id   | select_type   | table  | type   | possible_keys       | key              | key_len    | ref      | rows        | Extra                              |
+------+---------------+--------+--------+---------------------+------------------+------------+----------+-------------+------------------------------------+
| 1    | SIMPLE        | t_tb1  | ref    | idx_corpid_qq1id    | idx_corpid_qq1id | 8          | const    | 2375752     | Using index condition; Using where |
+---- -+---------------+--------+--------+---------------------+------------------+------------+----------+-------------+------------------------------------+
共返回 1 行记录,花费 17.48 ms.

mysql>show warnings;
+-----------------+----------------+-----------------------------------------------------------------------------------------------------------------------+
| Level           | Code           | Message                                                                                                               |
+-----------------+----------------+-----------------------------------------------------------------------------------------------------------------------+
| Warning         |           1739 | Cannot use range access on index 'idx_corpid_qq1id' due to type or collation conversion on field 'f_qq1_id'           |
| Note            |           1003 | * select#1 */ select `d_dbname`.`t_tb1`.`f_col3_id` AS `f_col3_id`,`d_dbname`.`t_tb1`.`f_qq1_id` AS `f_qq1_id` from `d_dbname`.`t_tb1` USE INDEX (`idx_corpid_qq1id`) where |
|                 |                |  ((`d_dbname`.`t_tb1`.`f_col2_id` = 1244378) and (`d_dbname`.`t_tb1`.`f_col1_id` = 1226391) and (`d_dbname`.`t_tb1`.`f_qq1_id` in |
|                 |                | (12345,23456,34567,45678,56789,67890,78901,89012,90123,901231,901232,901233)))                                        |
+-----------------+----------------+-----------------------------------------------------------------------------------------------------------------------+
共返回 2 行记录,花费 10.81 ms.
复制



rows 列达到 200w 行,但问题也发现了:select_type 应该是 range 才对,key_len 看出来只用到了idx_corpid_qq1id
索引的第一列。上面 explain 使用了 extended
,所以show warnings;
可以很明确的看到 f_qq1_id 出现了隐式类型转换:f_qq1_id 是 varchar,而后面的比较值是整型。


解决该问题就是避免出现隐式类型转换 (implicit type conversion) 带来的不可控:把 f_qq1_id in 的内容写成字符串:


1
2
3
4
5
6
7
8
复制
mysql>explain SELECT f_col3_id,f_qq1_id FROM d_dbname.t_tb1 WHERE f_col1_id=1226391 and f_col2_id=1244378 and 
f_qq1_id in ('12345','23456','34567','45678','56789','67890','78901','89012','90123','901231');
+-------+---------------+--------+---------+--------------------------------+------------------+-------------+---------+---------+------------------------------------+

| id    | select_type   | table  | type    | possible_keys                  | key              | key_len     | ref     | rows    | Extra                              |
+-------+---------------+--------+---------+--------------------------------+------------------+-------------+---------+---------+------------------------------------+

| 1     | SIMPLE        | t_tb1  | range   | uid_type_frid,idx_corpid_qq1id | idx_corpid_qq1id | 70          |         | 40      | Using index condition; Using where |
+-------+---------------+--------+---------+--------------------------------+------------------+-------------+---------+---------+------------------------------------+

共返回 1 行记录,花费 12.41 ms.
复制


扫描行数从 1386 减少为 40。



类似的还出现过一例:


1
2
3
复制
SELECT count(0)  FROM d_dbname.t_tb2 where f_col1_id= '1931231'  AND f_phone in(098890);

| Warning | 1292 | Truncated incorrect DOUBLE value: '1512-98464356'
复制


优化后直接从扫描 rows 100w 行降为 1。


借这个机会,系统的来看一下 mysql 中的隐式类型转换。




2. mysql 隐式转换规则


2.1 规则


下面来分析一下隐式转换的规则

a. 两个参数至少有一个是 NULL 时,比较的结果也是 NULL,例外是使用 <=> 对两个 NULL 做比较时会返回 1,这两种情况都不需要做类型转换
b. 两个参数都是字符串,会按照字符串来比较,不做类型转换
c. 两个参数都是整数,按照整数来比较,不做类型转换
d. 十六进制的值和非数字做比较时,会被当做二进制串
e. 有一个参数是 TIMESTAMP 或 DATETIME,并且另外一个参数是常量,常量会被转换为 timestamp
f. 有一个参数是 decimal 类型,如果另外一个参数是 decimal 或者整数,会将整数转换为 decimal 后进行比较,如果另外一个参数是浮点数,则会把 decimal 转换为浮点数进行比较
g. 所有其他情况下,两个参数都会被转换为浮点数再进行比较


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制
mysql> select 11 + '11', 11 + 'aa', 'a1' + 'bb', 11 + '0.01a';  
+-----------+-----------+-------------+--------------+

| 11 + '11' | 11 + 'aa' | 'a1' + 'bb' | 11 + '0.01a' |
+-----------+-----------+-------------+--------------+

|        22 |        11 |           0 |        11.01 |
+-----------+-----------+-------------+--------------+

1 row in set, 4 warnings (0.00 sec)

mysql> show warnings;
+---------+------+-------------------------------------------+

| Level   | Code | Message                                   |
+---------+------+-------------------------------------------+

| Warning | 1292 | Truncated incorrect DOUBLE value: 'aa'    |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'a1'    |
| Warning | 1292 | Truncated incorrect DOUBLE value: 'bb'    |
| Warning | 1292 | Truncated incorrect DOUBLE value: '0.01a' |
+---------+------+-------------------------------------------+

4 rows in set (0.00 sec)


mysql> select '11a' = 11, '11.0' = 11, '11.0' = '11', NULL = 1;
+------------+-------------+---------------+----------+

| '11a' = 11 | '11.0' = 11 | '11.0' = '11' | NULL = 1 |
+------------+-------------+---------------+----------+

|          1 |           1 |             0 |     NULL |
+------------+-------------+---------------+----------+

1 row in set, 1 warning (0.01 sec)
复制



上面可以看出11 + 'aa'
,由于操作符两边的类型不一样且符合第 g 条,aa
要被转换成浮点型小数,然而转换失败(字母被截断),可以认为转成了 0,整数11
被转成浮点型还是它自己,所以11 + 'aa' = 11


0.01a
转成 double 型也是被截断成0.01
,所以11 + '0.01a' = 11.01


等式比较也说明了这一点,'11a'
'11.0'
转换后都等于 11
,这也正是文章开头实例为什么没走索引的原因: varchar 型的 f_qq1_id,转换成浮点型比较时,等于 12345 的情况有无数种如 12345a、12345.b 等待,MySQL 优化器无法确定索引是否更有效,所以选择了其它方案。


但并不是只要出现隐式类型转换,就会引起上面类似的性能问题,最终是要看转换后能否有效选择索引。像f_id = '654321'
f_mtime between '2016-05-01 00:00:00' and '2016-05-04 23:59:59'
就不会影响索引选择,因为前者 f_id 是整型,即使与后面的字符串型数字转换成 double 比较,依然能根据 double 确定 f_id 的值,索引依然有效。后者是因为符合第 e 条,只是右边的常量做了转换。


开发人员可能都只要存在这么一个隐式类型转换的坑,但却又经常不注意,所以干脆无需记住那么多规则,该什么类型就与什么类型比较。




2.2 隐式类型转换的安全问题


implicit type conversion 不仅可能引起性能问题,还有可能产生安全问题。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制
mysql> desc t_account;
+-----------+-------------+------+-----+---------+----------------+
| Field     | Type        | Null | Key | Default | Extra          |
+-----------+-------------+------+-----+---------+----------------+
| fid       | int(11)     | NO   | PRI | NULL    | auto_increment |
| fname     | varchar(20) | YES  |     | NULL    |                |
| fpassword | varchar(50) | YES  |     | NULL    |                |
+-----------+-------------+------+-----+---------+----------------+

mysql> select * from t_account;
+-----+-----------+-------------+
| fid | fname     | fpassword   |
+-----+-----------+-------------+
|   1 | xiaoming  | p_xiaoming  |
|   2 | xiaoming1 | p_xiaoming1 |
+-----+-----------+-------------+

假如应用前端没有WAF防护,那么下面的sql很容易注入:
mysql> select * from t_account where fname='A' ;

fname传入  A' OR 1='1  

mysql> select * from t_account where fname='A' OR 1='1';
复制



攻击者更聪明一点: fname 传入 A'+'B
 ,fpassword 传入 ccc'+0
 :


1
2
3
4
5
6
7
8
复制
mysql> select * from t_account where fname='A'+'B' and fpassword='ccc'+0;
+-----+-----------+-------------+

| fid | fname     | fpassword   |
+-----+-----------+-------------+

|   1 | xiaoming  | p_xiaoming  |
|   2 | xiaoming1 | p_xiaoming1 |

+-----+-----------+-------------+
2 rows in set, 7 warnings (0.00 sec)
复制





参考


原文链接地址:http://seanlook.com/2016/05/05/mysql-type-conversion/


http://seanlook.com/2016/05/05/mysql-type-conversion/index.html









sohu-dba


文章转载自SOHU-DBA,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论