给不固定内容字符串类型的字段加索引, 通常有这么几种做法:
- 前缀索引
- 倒序索引:
- 加hash字段做为索引列
可以参考 丁奇 老师的专栏文章: 怎么给字符串字段加索引?
这里我们只讨论hash方式.
- 使用
md5 short
, 栏位设置为char(16)
, 这样一个索引列占用空间是 16*3+1=49bytes
. - 使用
crc32
, 栏位设置为bigint
, 这样一个索引列占用空间是 8+ 1= 9bytes
.
表 charset=utf8mb3. 如果是utf8mb4, 则md5 short索引长度为 16*4+1=69 bytes
crc32的值超过了4 bytes的int, 必须使用big int
生成模拟数据
我们以用户email
+电话tel
两个栏位进行数据模拟. email前面部分长度为6-20
建表
USE `test`;
DROP TABLE IF EXISTS `hash_index`;
CREATE TABLE `hash_index` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`email` varchar(50) DEFAULT NULL,
`email_md5` char(16) DEFAULT NULL COMMENT 'email的短md5',
`email_hash` bigint DEFAULT NULL COMMENT 'email的crc32',-- 注意: 必须用bigint, 4字节的int存不下
`tel` varchar(50) DEFAULT NULL,
`tel_md5` char(16) DEFAULT NULL COMMENT 'tel的短md5',
`tel_hash` bigint DEFAULT NULL COMMENT 'tel的crc32',-- 注意: 必须用bigint, 4字节的int存不下
PRIMARY KEY (`id`),
KEY `md5` (`email_md5`,`tel_md5`),
KEY `hashs` (`email_hash`,`tel_hash`)
) ENGINE=InnoDB CHARSET=utf8mb3;
php生成sql文件
function genRandString($length){
$arr_source = array_merge(range('a', 'z'), range(0,9));
$super = count($arr_source) - 1;
$arr = [];
for($i = 0; $i < $length; $i++){
$ix = rand(0, $super);
$arr[] = $arr_source[$ix];
}
return implode('', $arr);
}
function genRandDomain(){
$arr_domain = [
'gmail.com',
'qq.com',
'163.com',
'126.com',
'gmail.com',
];
$rt = $arr_domain[rand(0, count($arr_domain) - 1)];
return $rt;
}
/**
* 生成一个电话号码(非国内)
*
* @return string
*/
function genRandTel(){
$length = 9;
$arr = [];
for($i = 0; $i < $length; $i++){
$arr[] = rand(0, 9);
}
$first = rand(1, 9);
return $first. implode('', $arr);
}
/**
* 生成一个email地址
* @return string
*/
function genRandEmail(){
return genRandString(rand(6,20)) . '@' . genRandDomain();
}
/**
* 生成短md5
* @param string $str
* @return string
*/
function md5_short($str=''){
return substr(md5($str),8,16);
}
/**
* 生成sql
*
* @param int $length
* @param bool $auto_commit 是否自动提交
* @param bool $batch_insert 是否使用批量插入
*/
function genSQL($length, $auto_commit=true, $batch_insert=false) {
// region 数据插入优化设置
$step = 500;
$batch_insert_size = 50;
// endregion
$file_name = 'hash_index_'.$length.'.sql';
$table_name = 'hash_index';
//数据太长, 容易超出内存限制, 故分批次写入
file_put_contents($file_name, 'USE `test`;' . PHP_EOL);//重置文件
for ($i = 0, $n = ceil($length / $step); $i < $n; $i++) {
$lot_data = [];//分批数据, 元素直接是values的字符串
for($j=0; $j < $step; $j++){
$email = genRandEmail();
$email_md5 = md5_short($email);
$email_hash = crc32($email);//注意: 这里不同于hash('crc32', $str) !!!!
$tel = genRandTel();
$tel_md5 = md5_short($tel);
$tel_hash = crc32($tel);//注意: 这里不同于hash('crc32', $str) !!!!
$lot_data[] = '(\''.$email.'\', \''.$email_md5.'\', '.$email_hash.', \''.$tel.'\', \''.$tel_md5.'\', '.$tel_hash.')';
}
$arr_sql = [];
if (!$auto_commit) {
$arr_sql[] = 'SET autocommit = 0;';
}
if ($batch_insert) {
//这里再分成50个一份
for ($j = 0, $n2 = ceil(count($lot_data) / $batch_insert_size); $j < $n2; $j++) {
$arr_v_sliced = array_slice($lot_data, $batch_insert_size * $j, $batch_insert_size);
$arr_sql[] = 'INSERT INTO `' . $table_name . '`(`email`,`email_md5`,`email_hash`,`tel`,`tel_md5`,`tel_hash`) VALUES ' . implode(',', $arr_v_sliced) . ';';
}
} else {
foreach ($lot_data AS $row) {
$arr_sql[] = 'INSERT INTO `' . $table_name . '`(`email`,`email_md5`,`email_hash`,`tel`,`tel_md5`,`tel_hash`) VALUES '.$row.';';
}
}
if (!$auto_commit) {
$arr_sql[] = 'commit;';
}
$sql_all = implode(PHP_EOL, $arr_sql) . ($i < $n - 1 ? PHP_EOL : '');
file_put_contents($file_name, $sql_all, FILE_APPEND);
}
}
用上面的代码, 我们来生成50w笔数据:
genSQL(500000, false, true);
数据查询
先看以下使用两种hash索引的执行计划:
- md5
EXPLAIN SELECT * FROM `hash_index`
WHERE email_md5='a2dce0b8e5f97c82' AND tel_md5='60890e62c19aefe2'
AND email='wxnpjbf752blke3z@163.com' AND tel='8326347684';
- crc32
EXPLAIN SELECT * FROM `hash_index`
WHERE email_hash=1168974173 AND tel_hash=775416052
AND email='wxnpjbf752blke3z@163.com' AND tel='8326347684';
注意上面的sql中, 除了使用索引, 还加了原栏位的值判断, 这是避免md5或crc32值重复带来的问题.
注意看explain结果中的key_len, 这个表示使用到的索引的长度.
crc32的重复概率是比md5要高很多的, 到底有多高呢?
SELECT email_hash, COUNT(email_hash) AS e
FROM `hash_index`
GROUP BY email_hash HAVING COUNT(email_hash) >1
ORDER BY e DESC;
SELECT tel_hash, COUNT(tel_hash) AS e
FROM `hash_index`
GROUP BY tel_hash HAVING COUNT(tel_hash) >1
ORDER BY e DESC;
从目前来看, 50w随机数据的重复概率还是很低的.
其实本栗子中的tel都是数值, 直接使用bigint存储即可, 不需要再做额外的索引, 只是为了和之前的查询业务做比较而添加的.
就目前windows下单个请求测试, 两种hash的查询性能看不出来差异, 但是理论上是crc32更快的.
关于索引空间大小的计算问题
索引使用的硬盘空间大小, 与explain计算出来的差异比较大. 按上面计算的, md5 short
占用的索引空间是98 bytes, crc32
的是9bytes.
我们导入一个5w的数据作为测试, 建2个单独索引的表做比较.
DROP TABLE IF EXISTS `hash_index__md5`;
CREATE TABLE `hash_index__md5` (
`id` int unsigned NOT NULL, -- 这里不要加PK
`email` varchar(50) DEFAULT NULL,
`email_md5` char(16) DEFAULT NULL COMMENT 'email的短md5',
`email_hash` bigint DEFAULT NULL COMMENT 'email的crc32',-- 注意: 必须用bigint, 4字节的int存不下
`tel` varchar(50) DEFAULT NULL,
`tel_md5` char(16) DEFAULT NULL COMMENT 'tel的短md5',
`tel_hash` bigint DEFAULT NULL COMMENT 'tel的crc32',-- 注意: 必须用bigint, 4字节的int存不下
KEY `md5` (`email_md5`,`tel_md5`)
-- KEY `hashs` (`email_hash`,`tel_hash`)
) ENGINE=InnoDB CHARSET=utf8mb3;
DROP TABLE IF EXISTS `hash_index__crc`;
CREATE TABLE `hash_index__crc` (
`id` int unsigned NOT NULL,
`email` varchar(50) DEFAULT NULL,
`email_md5` char(16) DEFAULT NULL COMMENT 'email的短md5',
`email_hash` bigint DEFAULT NULL COMMENT 'email的crc32',-- 注意: 必须用bigint, 4字节的int存不下
`tel` varchar(50) DEFAULT NULL,
`tel_md5` char(16) DEFAULT NULL COMMENT 'tel的短md5',
`tel_hash` bigint DEFAULT NULL COMMENT 'tel的crc32',-- 注意: 必须用bigint, 4字节的int存不下
-- KEY `md5` (`email_md5`,`tel_md5`),
KEY `hashs` (`email_hash`,`tel_hash`)
) ENGINE=InnoDB CHARSET=utf8mb3;
我们从表hash_index
导入数据到这两个单独索引的表:
INSERT INTO `test`.`hash_index__md5`(`id`,`email`,`email_md5`,`email_hash`,`tel`,`tel_md5`,`tel_hash`)
SELECT * FROM `test`.`hash_index`;
INSERT INTO `test`.`hash_index__crc`(`id`,`email`,`email_md5`,`email_hash`,`tel`,`tel_md5`,`tel_hash`)
SELECT * FROM `test`.`hash_index`;
然后看着三个表的空间使用:
-- 查看表空间
SELECT `TABLE_NAME`, `ROW_FORMAT`, `TABLE_ROWS`, `AVG_ROW_LENGTH`
, CONCAT(ROUND(`DATA_LENGTH`/1024/1024, 4), ' MB') AS `DATA_LENGTH`
, CONCAT(ROUND(`INDEX_LENGTH`/1024/1024, 4), ' MB') AS `INDEX_LENGTH`
, CONCAT(ROUND(`DATA_FREE`/1024/1024, 4), ' MB') AS `DATA_FREE`
, `TABLE_COMMENT`
FROM `information_schema`.`tables`
WHERE table_schema='test' AND table_name IN('hash_index', 'hash_index__md5', 'hash_index__crc');
crc32索引占用的硬盘空间大约是md5 short的一半.
但是索引文件加载到内存后呢? 在内存表中, 所需的空间是根据table的定义. maybe这就是差异所在吧. 丁老师能给解释一下就好了.....
我们可以使用performance_schema
https://dev.mysql.com/doc/refman/8.0/en/performance-schema-query-profiling.html
profile 自 MySQL 5.6.7 开始不推荐使用
todo