1. 版本
1)操作系統版本
cat /proc/version
Linux version 3.10.0-957.5.1.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC) ) #1
2)數據庫版本
mysql --version
mysql Ver 14.14 Distrib 5.7.22, for linux-glibc2.12 (x86_64) using EditLine wrapper
2. 問題描述
2.1 問題發現
這是一個朋友跟我諮詢的問題,幫他分析解決這個問題中,我發先這個問題也正好有效的印證了我們常說的mysql 數據類型優化原則,既選擇更小的數據類型(在滿足業務使用的情況下)。在此拿出來跟大家分享一下。他的問題如下
他在兩張表上進行關聯查詢,如果兩張表都是utf8 字符集那麼查詢會很快。如果兩張表是utf8mb4字符集那麼查詢就比較慢。其實如果朋友是dba 的話,他一看執行計劃就能看出來,是因爲兩種情況下執行計劃不一樣所以導致執行效率不同
下面建兩張測試表,來重現朋友當時的現象
1. 創建兩張字符集爲 utf8 的表
create table test_join_1(id int,name varchar(250)) default character set utf8;
create table test_join_2(id int,name varchar(250)) default character set utf8;
2. 查看sql 的執行計劃
#這裏該sql寫的是否有優化空間,不在我們本次討論範圍
explain select a.* ,b.id from `test_join_1` a left join
(SELECT name, min(id) id from `test_join_2` b group by name) b on a.name=b.name;
explain select a.* ,b.id from `test_join_1` a left join
-> (SELECT name, min(id) id from `test_join_2` b group by name) b on a.name=b.name;
+----+-------------+------------+------------+------+---------------+-------------+---------+------------------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+-------------+---------+------------------+------+----------+---------------------------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 753 | test_chen.a.name | 2 | 100.00 | NULL |
| 2 | DERIVED | b | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using temporary; Using filesort |
+----+-------------+------------+------------+------+---------------+-------------+---------+------------------+------+----------+---------------------------------+
3 rows in set, 1 warning (0.00 sec)
3. 轉換表的字符集爲 utf8mb4
#注意 utf8 字符集轉換成 utf8mb4不會有問題,當時 utf8mb4 向 utf8 轉換不能保證沒有問題
ALTER TABLE test_join_1 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE test_join_2 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
4. 轉換字符集後再次查看sql 的執行計劃
explain select a.* ,b.id from `test_join_1` a left join
-> (SELECT name, min(id) id from `test_join_2` b group by name) b on a.name=b.name;
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | Using where; Using join buffer (Block Nested Loop) |
| 2 | DERIVED | b | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using temporary; Using filesort |
+----+-------------+------------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
3 rows in set, 1 warning (0.00 sec)
##在使用 utf8 字符集時,mysql 爲派生表創建了一個索引(auto_key0),這樣a表和派生表之間使用 index Nested-Loop Join(NLJ) 方式關聯。在使用 utf8mb4 字符集時 Mysql 使用 Block Nested-Loop Join(BNL) 方式進行關聯。NLJ 比 BNL 訪問方式更高效。
通過分析執行計劃,相同數據的情況下,第一次執行會比第二次執行更高效,這也符合朋友生產中產生的現象。但是問題是爲什麼使用 utf8字符集時,會給派生表創建索引,使用utf8mb4時就不會呢?其實原因很簡單,代價,mysql 會計算給派生表name列索引的長度(派生表name列長度使用b表中name列長度),b表name列 爲 varchar(250),在utf8和utf8mb4 字符集下計算出的索引長度分別爲 753(utf8 字符集最多使用3個字節表示一個字符,同時 name 列允許爲 null,並且是可變長度類型,所以key_len=250*3+1+2=753) 和 1003(utf8mb4 字符集最多使用4個字節表示一個字符,同時name列允許爲null,並且是可變長度類型,所以key_len=250*4+1+2=1003)
對於 key_len 超過 1000 的列mysql不會爲派生表創建 <auto_key0>,這也告訴我們,不要隨意設置列的長度,在滿足業務的情況下,列的長度是越小越好。
2.2 問題原因
通過上面的例子,我們知道問題的原因是,在不同的字符集下,mysql 對同一個列創建創建索引的長度是不一樣的,當mysql計算在該列上創建索引的長度超過1000的話,就不會對派生表創建索引。
關於索引長度具體怎麼計算可以參考:
https://blog.csdn.net/shaochenshuo/article/details/105210931
2.3 問題處理
按照規範設計和使用數據庫。
MySQL 應該使用更小的數據類型(在滿足業務的情況下),理由如下:
1)更小的數據類型可能佔用的磁盤空間更小,相同的內存空間可以緩存更多的記錄,減少IO操作,減少磁盤空間佔用
2)更小的數據類型意味着CPU可以更快的進行計算。
3)最主要的是,在數據庫執行過程中MySQL有時會建立一些臨時表進行連接排序或去重操作,在這些臨時表中列的長度跟源表中列的長度一致,列的長度越大,性能也就越不好。所以列的類型儘可能的小。