Mysql 自定義HASH索引帶來的巨大性能提高----[挖坑篇]

有這樣一個業務場景,須要在2個表裏比較存在於A表,不存在於B表的數據。表結構以下:html

T_SETTINGS_BACKUP | CREATE TABLE `T_SETTINGS_BACKUP` (
  `FID` bigint(20) NOT NULL AUTO_INCREMENT,
  `FUSERID` bigint(20) NOT NULL COMMENT '用戶ID',
  `FDEVICE` varchar(64) NOT NULL DEFAULT '' COMMENT '用戶設備號(SN)',
  `FAPPID` varchar(64) NOT NULL DEFAULT '' COMMENT '應用ID',
  `FKEYID` varchar(32) NOT NULL DEFAULT '' COMMENT '設置項ID',
  `FCONTENT` varchar(2000) NOT NULL DEFAULT '' COMMENT '設置項內容',
  `FUPDATETIME` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '修改時間',
  `FCREATETIME` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '建立時間',
  PRIMARY KEY (`FID`),
  UNIQUE KEY `UDX_USERID_DEVICE_APPID_KEYID` (`FUSERID`,`FDEVICE`,`FAPPID`,`FKEYID`)
) ENGINE=InnoDB AUTO_INCREMENT=21934 DEFAULT CHARSET=utf8mb4 

暫定義上表爲A表,記錄數:21933mysql

B表表結構以下,記錄數:4794959算法

CREATE TABLE `meizu_device_tmp_1` (
  `id` int(11) unsigned NOT NULL DEFAULT '0',
  `imei` bigint(20) NOT NULL DEFAULT '0' COMMENT 'imei',
  `sn` varchar(20) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT 'sn',
  UNIQUE KEY `imei` (`imei`),
  UNIQUE KEY `sn` (`sn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

A的FDEVICE和B的SN是關聯字段,如今要求出FDEVICE在A不在B的記錄數。天然想到下面的LEFT JOINsql

mysql> explain select A.fdevice  FROM T_SETTINGS_BACKUP A left JOIN meizu_device_tmp_1 B ON A.FDEVICE=B.sn where B.sn is null;
+----+-------------+-------+-------+---------------+-------------------------------+---------+------+---------+--------------------------------------+
| id | select_type | table | type  | possible_keys | key                           | key_len | ref  | rows    | Extra                                |
+----+-------------+-------+-------+---------------+-------------------------------+---------+------+---------+--------------------------------------+
|  1 | SIMPLE      | A     | index | NULL          | UDX_USERID_DEVICE_APPID_KEYID | 654     | NULL |   22232 | Using index                          |
|  1 | SIMPLE      | B     | index | NULL          | sn                            | 62      | NULL | 4772238 | Using where; Using index; Not exists |
+----+-------------+-------+-------+---------------+-------------------------------+---------+------+---------+--------------------------------------+
2 rows in set (0.00 sec)

執行時間1小時以上,等不出結果直接KILL掉了。函數

分析上面的執行計劃,兩個表都用到了覆蓋索引,每一個表都沒有過濾條件,因此須要掃描所有行,2W乘以470W是個巨大的數字,執行器在不停的作內循環的判斷,直到完成22232*4772238次。除了這個循環次數巨大外,這個執行計劃還有2個須要考量的地方性能

1)type=index  2)key_len 優化

type=index 執行效率僅高於全表掃描,在某些狀況下比所有掃描更差。key_len比較大,說明索引太長。A表的索引是個4字段的組合索引,有用的比較字段只有FDEVICE,爲了覆蓋索引優化器把所有字段都加入判斷了。編碼

對於key_len 有兩個疑問 1)爲何A表的key_len=654? 2)爲何B表的ken_len=62? spa

優化key_len, 考慮到業務特性,FUSERID確定大於0,把SQL改一下,執行計劃看起來好一點了,type=range,key_len=8,實際上對A表只用到了FUSERID字段索引,最左前綴,FDEVICE經過WHERE判斷。code

mysql> desc select A.fdevice  FROM T_SETTINGS_BACKUP A left JOIN meizu_device_tmp_1 B ON A.FDEVICE=B.sn where A.fuserid>0  and B.sn is null;
+----+-------------+-------+-------+-------------------------------+-------------------------------+---------+------+---------+--------------------------------------+
| id | select_type | table | type  | possible_keys                 | key                           | key_len | ref  | rows    | Extra                                |
+----+-------------+-------+-------+-------------------------------+-------------------------------+---------+------+---------+--------------------------------------+
|  1 | SIMPLE      | A     | range | UDX_USERID_DEVICE_APPID_KEYID | UDX_USERID_DEVICE_APPID_KEYID | 8       | NULL |   11116 | Using where; Using index             |
|  1 | SIMPLE      | B     | index | NULL                          | sn                            | 62      | NULL | 4911049 | Using where; Using index; Not exists |
+----+-------------+-------+-------+-------------------------------+-------------------------------+---------+------+---------+--------------------------------------+
2 rows in set (0.01 sec)

執行時間依舊很長,等不了直接KILL了。

優化到這一步,還有什麼別的招數,能夠提升執行性能的?彷佛已經到了盡頭。

回顧下兩表的關聯字段,A.FDEVICE=B.sn,兩個字段都是字符串,在數據類型上考慮,天然想到,是否是能夠把記錄比較字段從字符串的比較,改爲數字的比較?這是個優化的方向。在計算機裏底層數據都是01010這樣,只須要把數字換算成0101就能夠作等值比較了,可是變成字符,須要先去字符編碼表找到字符對應的數字,在把數字換算成0101,這裏多出一步查找操做。另外一方面字符佔用的空間比數字要大不少,一個頁內能存下的item條目比數字的要少,這會致使更多的數據頁讀取。

根據這個方向,嘗試使用自定義HASH索引,常見的HASH函數有MD5,password,crc32,sha1等,只有crc32哈希以後的值的數字型的。

mysql> select md5('sdsafa'),password('sdsafa'),crc32('sdsafa'),SHA1('sdsafa');
+----------------------------------+-------------------------------------------+-----------------+------------------------------------------+
| md5('sdsafa') | password('sdsafa') | crc32('sdsafa') | SHA1('sdsafa') |
+----------------------------------+-------------------------------------------+-----------------+------------------------------------------+
| c5067032ca64a35620fc5c75aa42265c | *45ABB21DBD1E6A5659E05F1EBAF589A3B39EB835 | 1766538443 | b9349f6a0b8138e3e6461745fd257678eefeb9a2 |
+----------------------------------+-------------------------------------------+-----------------+------------------------------------------+
1 row in set (0.00 sec)

在表裏加個字段記錄hash以後的值,並對這個字段加上索引。

mysql> CREATE TABLE `meizu_device_tmp_3` (
    ->   `id` int(11) unsigned NOT NULL DEFAULT '0',
    ->   `imei` bigint(20) NOT NULL DEFAULT '0' COMMENT 'imei',
    ->   `sn` varchar(20) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT 'sn',
    ->   `hash_sn` bigint(20) DEFAULT NULL,
    ->   UNIQUE KEY `imei` (`imei`),
    ->   KEY `hash_sn` (`hash_sn`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;
Query OK, 0 rows affected (0.20 sec)

mysql> insert into meizu_device_tmp_3 select id,imei,sn,crc32(sn) from meizu_device_tmp_1;
Query OK, 4794959 rows affected (1 min 50.31 sec)
Records: 4794959  Duplicates: 0  Warnings: 0

mysql> select * from meizu_device_tmp_3 limit 1;
+----------+---------+--------------------+------------+
| id       | imei    | sn                 | hash_sn    |
+----------+---------+--------------------+------------+
| 23930528 | 1311265 | MX21CA2ALHR2460302 | 2330453935 |
+----------+---------+--------------------+------------+
1 row in set (0.00 sec)

查詢時,關聯字段先crc32計算後,再比較,這樣就變成了數字和數字的比較了,被驅動表比較字段也有索引。

可是crc32算法可能存在hash碰撞,也就是不一樣的值hash出來的結果是同樣的,這就「撞」上了。爲了不碰撞致使的比較結果不許確,在hash比較以後,再作一次原值的比較。

優化以後的查詢語句是這樣的

mysql> desc select A.fdevice  FROM T_SETTINGS_BACKUP A left JOIN meizu_device_tmp_3 B ON crc32(A.FDEVICE)=B.hash_sn and A.fdevice=B.sn where  B.sn is null;
+----+-------------+-------+-------+---------------+-------------------------------+---------+------+-------+-------------------------+
| id | select_type | table | type  | possible_keys | key                           | key_len | ref  | rows  | Extra                   |
+----+-------------+-------+-------+---------------+-------------------------------+---------+------+-------+-------------------------+
|  1 | SIMPLE      | A     | index | NULL          | UDX_USERID_DEVICE_APPID_KEYID | 654     | NULL | 22232 | Using index             |
|  1 | SIMPLE      | B     | ref   | hash_sn       | hash_sn                       | 9       | func |     1 | Using where; Not exists |
+----+-------------+-------+-------+---------------+-------------------------------+---------+------+-------+-------------------------+
2 rows in set (0.00 sec)

巨大的改變,被驅動表的rows=1. SQL執行時間0.38秒。

 

hash索引有這麼大的好處,可是也存在很多缺點

1)hash不能處理範圍比較,只能處理等值比較。

2)hash不能作排序,hash出來的結果是隨機分佈的。

3)hash不支持部分索引,如index a(10)就不支持。

4)hash沒法覆蓋索引

5)hash有碰撞,碰撞得比較厲害時,處理碰撞的代價就比較高。

 

CRC32算法:http://wenku.baidu.com/view/465dca06e87101f69e31951f.html

自定義hash索引:http://www.lizhonghaosc.cn/mysql-chuang-jian-zi-ding-yi-ha-xi-suo-yin/

相關文章
相關標籤/搜索