1. 程式人生 > 實用技巧 >SQL優化案例(1):隱式轉換

SQL優化案例(1):隱式轉換

MySQL是當下最流行的關係型資料庫之一,網際網路高速發展的今天,MySQL資料庫在電商、金融等諸多行業的生產系統中被廣泛使用。

在實際的開發運維過程中,想必大家也常常會碰到慢SQL的困擾。一條效能不好的SQL,往往會帶來過大的效能開銷,進而引起整個作業系統資源的過度使用,甚至造成會話堆積,引發線上故障。
而在SQL調優的場景中,一類比較常見的問題,就是隱式型別轉換。那什麼是隱式轉換呢?

在MySQL中,當操作符與不同型別的運算元一起使用時,會發生型別轉換以使運算元相容,此時則會發生隱式轉換。出現隱式轉換,往往意味著SQL的執行效率將大幅降低。
接下來筆者將結合幾大常見場景,讓大家實際體會什麼是隱式轉換,以及如何去應對出現隱式轉換的情況,請閱讀以下案例。

傳遞資料型別和欄位型別不一致造成隱式轉換

一類比較經典的場景就是傳遞資料型別和欄位型別不一致造成的隱式轉換,這種場景也是我們平時最常遇到的。具體可以看下下面這個例子:

1)待優化場景

SQL及執行計劃如下:

select * from dt_t1 where emp_no = 41680;

該表索引如下:

key idx_empno (`emp_no`)

2)場景解析

從執行計劃中Type部分:ALL,全表掃描,而沒有走idx_empno索引, 一般這種情況可能傳遞的資料型別和實際的欄位型別不一致,那麼我們來看下具體的表結構。

root@localhost mysql.sock 5.7.28-log :[employees] 14:48:10>desc employees;
+------------+---------------+------+-----+---------+-------+
| Field      | Type          | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+-------+
| emp_no     | varchar(14)   | NO   | MUL | NULL    |       |
| birth_date | date          | NO   |     | NULL    |       |
| first_name | varchar(14)   | NO   |     | NULL    |       |
| last_name  | varchar(16)   | NO   |     | NULL    |       |
| gender     | enum('M','F') | NO   |     | NULL    |       |
| hire_date  | date          | NO   |     | NULL    |       |
+------------+---------------+------+-----+---------+-------+
6 rows in set (0.00 sec)

表結構中看到該欄位型別為varchar 型別,傳遞欄位為整型,造成隱式轉換不能走索引。

3)場景優化

該SQL可通過簡單改寫來避免出現隱式轉換,如下:

select * from dt_t1 where emp_no='41680';

當傳入資料是與匹配欄位一致的varchar型別時,便可以正常使用到索引了,優化效果如下:

關聯欄位型別不一致造成隱式轉換

除了常量匹配的查詢場景,關聯查詢在關聯欄位不一致的情況下,也會出現隱式轉換。

1)待優化場景

SELECT  count(*) from t1  as a
JOIN  `t2`  b on a.`id` = b.`alipay_order_no` ;

2)場景解析

從執行計劃中可以看出被驅動表 b, Extra:Range checked for each record (index map: 0x8)

一般在當我們看到Range checked for each record (index map: 0x8) 的時候,可能就是發生了隱式轉換,我們來看下官方文件是怎麼解釋的

Range checked for each record (index map: N) (JSON property: message)
MySQL found no good index to use, but found that some of indexes might be used after column values from preceding tables are known. For each row combination in the preceding tables, MySQL checks whether it is possible to use a range or index_merge access method to retrieve rows. This is not very fast, but is faster than performing a join with no index at all. The applicability criteria are as described in Section 8.2.1.2, “Range Optimization”, and Section 8.2.1.3, “Index Merge Optimization”, with the exception that all column values for the preceding table are known and considered to be constants.
Indexes are numbered beginning with 1, in the same order as shown by SHOW INDEX for the table. The index map value N is a bitmask value that indicates which indexes are candidates. For example, a value of 0x19 (binary 11001) means that indexes 1, 4, and 5 will be considered.

查看下錶結構:

CREATE TABLE `t2` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `alipay_order_no` varchar(45) DEFAULT NULL,
 xxxx
 PRIMARY KEY (`id`),
 KEY `idx_alipay_order_no_temp` (`alipay_order_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2539968 DEFAULT CHARSET=utf8
共返回 1 行記錄,花費 5 ms.
 CREATE TABLE `t1` (
 `id` bigint(20) NOT NULL,
 xxxxxx
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
共返回 1 行記錄,花費 5 ms.

我們從表結構上面進行觀察到該關聯欄位資料 一個是int 型別,一個是varchar 型別。

當發生這種場景的時候我們應該如何優化呢?

我們還回來看看下具體的執行計劃,該驅動表為a,被驅動表b; 關聯條件:a.id = b.alipay_order_no ; 當a 表的欄位id 當為常數傳遞給b.alipay_order_no 的時候,發生column_type 不一致,無法使用索引,那麼我們讓a.id 傳遞的 欄位型別和b.alipay_order_no 保持一致,就可以使用索引了?

3)場景優化

我們可以對驅動表的關聯欄位進行顯式的型別轉換,讓其與被驅動表關聯欄位型別一致。改寫後SQL如下:

SELECT  count(*)
from `t1`a
 JOIN `t2` b on CAST( a.`id` AS CHAR ) = b.`alipay_order_no`

進行改寫後就可以正常利用索引進行關聯了,執行計劃如下:

字符集不一致造成隱式轉換

前面的兩種場景都是操作符兩側資料型別不同的情況,事實上,資料型別相同也可能會出現隱式轉換,比如下面這個字符集不一致導致隱式轉換的例子:

1)待優化場景

SQL及執行計劃如下:

SELECT COUNT(*)
FROM `t1`  o
join `t2`  og  ON `o`.`def8`= `og`.`group_id`
WHERE  o.`def1`= 'DG21424956'

2)場景解析

從這個執行計劃中我們可以看出第二列表og 中含有using join buffer (Block Nested Loop) ,TYpe=ALL .

一般這種情況下:using join buffer (Block Nested Loop) ,發生的情況是 a. 關聯欄位沒有索引 b.發生隱式轉換 等

看下具體表結構:

create table t1(
 ..... 
 `group_id` varchar(20) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `group_id` (`group_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8
create table t2(
 ..... 
 `def8` varchar(20) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `idx_tr_def1` (`def8`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

我們從表結構中可以看出關聯欄位都存在索引,但字符集是不一樣的,t1 utf8,t2 utf8mb4.

3)場景優化

SQL改寫思路和上例類似,我們對驅動表的關聯欄位進行字符集轉換,如下:

SELECT COUNT(*)   FROM `t1`  o
left join `t2` og  ON CONVERT(  o.`def8`  USING utf8 ) = `og`.`group_id`
WHERE  o.`def1`= 'DG21424956

轉換成一致的字符集之後,便可以通過索引進行關聯了

校驗規則不一致造成隱式轉換

那麼,只要保證操作符兩側資料型別以及字符集一致,就不會出現隱式轉換嗎?

答案是否定的,因為字符集還有一個很重要的屬性,就是校驗規則,當校驗規則不一致的時候,也是會出現隱式轉換行為的。具體看下面這個例子:

1)待優化場景

SELECT *
FROM `t1`
WHERE `uuid` in (SELECT uuid  FROM t2 WHERE project_create_at!= "0000-00-00 00:00:00")

該SQL執行計劃如下:

2)場景解析

兩張表的表結構如下:

CREATE TABLE `t1` (
 `id` int(11) NOT NULL AUTO_INCREMENT,  `
 uuid` varchar(128) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT 'UUID',
 xxxxxx
 PRIMARY KEY (`id`),
UNIQUE KEY `uuid_idx` (`uuid`)
) ENGINE=InnoDB AUTO_INCREMENT=2343994 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
CREATE TABLE `t2` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `uuid` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '專案uuid',
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=5408 DEFAULT CHARSET=utf8

我們從表結構看出,t1表作為被驅動表uuid是存在唯一索引的,並且關聯欄位資料型別以及字符集也都是一致的,但是校驗規則的不同導致了這個場景無法使用到索引。

3)場景優化

我們可以通過如下改寫,對驅動表關聯欄位的校驗規則進行顯示定義,讓其與被驅動表一致

explain extended
select b.*
from (select  uuid COLLATE utf8_unicode_ci as uuid
from t1 where project_create_at != "0000-00-00 00:00:00") a, t2 b
where a.uuid = b.uuid
+--------------+-----------------------+--------------------+----------------+-----------------------+-------------------+---------------+----------------+-----------------------+
| id           | select_type           | table              | type           | key                   | key_len           | ref           | rows           | Extra                 |
+--------------+-----------------------+--------------------+----------------+-----------------------+-------------------+---------------+----------------+-----------------------+
| 1            | PRIMARY               | <derived2>         | ALL            |                       |                   |               | 51             |                       |
| 1            | PRIMARY               | b                  | eq_ref         | uuid_idx              | 386               | a.uuid        | 1              |                       |
| 2            | DERIVED               | volunteer_patients | range          | idx-project-create-at | 6                 |               | 51             | Using index condition |
+--------------+-----------------------+--------------------+----------------+-----------------------+-------------------+---------------+----------------+-----------------------+
共返回 3 行記錄,花費 4 ms.

可以看到,改寫後的SQL,正常使用到索引進行欄位關聯,這樣就達到了我們預期的效果。

總結

隱式轉換出現的場景主要有欄位型別不一致、關聯欄位型別不一致、字符集型別不一致或校對規則不一致等。當出現隱式轉換帶來的SQL效能問題時,分析相應場景對症下藥即可。

除此之外,隱式轉換還可能會帶來查詢結果集不準,字符集不一致也會造成主從同步報錯等,因此在實際使用時我們應當儘量避免。

更多技術型文章可關注公眾號“雲掣YUNCHE”
也可到官網作進一步瞭解:https://www.dtstack.com/dtsmart/