當心MySQL的隱式類型轉換陷阱

1. 隱式類型轉換實例

今天生產庫上忽然出現MySQL線程數告警,IOPS很高,實例會話裏面出現許多相似下面的sql:(修改了相關字段和值)html

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 看了下掃描行數和索引選擇狀況:前端

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)時:mysql

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,然後面的比較值是整型。sql

解決該問題就是避免出現隱式類型轉換(implicit type conversion)帶來的不可控:把f_qq1_id in的內容寫成字符串:安全

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。性能

相似的還出現過一例:優化

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。ui

借這個機會,系統的來看一下mysql中的隱式類型轉換。spa

2. mysql隱式轉換規則

2.1 規則

下面來分析一下隱式轉換的規則.net

  1. 兩個參數至少有一個是 NULL 時,比較的結果也是 NULL,例外是使用 <=> 對兩個 NULL 作比較時會返回 1,這兩種狀況都不須要作類型轉換

  1. 兩個參數都是字符串,會按照字符串來比較,不作類型轉換

  2. 兩個參數都是整數,按照整數來比較,不作類型轉換

  3. 十六進制的值和非數字作比較時,會被當作二進制串

  4. 有一個參數是 TIMESTAMP 或 DATETIME,而且另一個參數是常量,常量會被轉換爲 timestamp

  5. 有一個參數是 decimal 類型,若是另一個參數是 decimal 或者整數,會將整數轉換爲 decimal 後進行比較,若是另一個參數是浮點數,則會把 decimal 轉換爲浮點數進行比較

  6. 全部其餘狀況下,兩個參數都會被轉換爲浮點數再進行比較

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 不只可能引發性能問題,還有可能產生安全問題。

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

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/

相關文章
相關標籤/搜索