为什么varchar上的索引必须要指定索引长度?

前言

看过阿里Java开发手册的同学,应该有注意到在MySQL索引规约中,有这么一条强制性规范:

在这里插入图片描述
本文就来分析分析为什么在varchar上的索引必须要指定索引长度?

索引结构

要了解原因,必须先对索引结构有一定认识,一般我们都会使用InnoDB存储引擎,其对应的索引结构就是B+TREE。

B+TREE和其他二叉树的最大区别就在于,节点的度可以设置的很大,MySQL中就定义了16KB为一页,一页就是树的一个节点。

下图,树的根节点中,7、13对应的就是保存的索引字段值。
在这里插入图片描述

那么,既然一页的大小是恒定的16KB,那也就意味着索引字段值占用的空间越小,一页能保存的数量也就越多,最终就体现在减少磁盘IO的次数上。

这里我们不妨举个例子:

假设我们索引字段是int类型的,且单行数据量大小为1KB,那么如果树的高度是3层,则大约可以存储数据行的计算方式如下:

int类型占用:4byte
索引指针占用:6byte
第三层的叶子节点:16kb/1kb = 16条
第一层和第二层节点,每个节点可以保存:16kb/(4byte+6byte) ≈ 1600条

1600*1600*16 ≈ 4千万条

如果索引字段现在改成bigint类型,那么大约可以存储的数据量是:

1170*1170*16 ≈ 2千万条

可以看出,假设在树的高度不变的情况下,索引字段所占用空间的大小会直接影响数据的存储量。

如果确定索引长度

按照上面分析,难道索引字段的长度越小越好?当然不是了!

还是举个例子:

假设有这么两条数据,A的字段值为:20200101000000,B的字段值为:2020010100001

可以看出两个数据的前12位都是一样的,这时候如果只索引前12位是就会索引到两条一模一样的数据,最后又不得在从这两条数据中进行过滤。

需要注意的是,如果数据未匹配,也无法使用覆盖索引的特性,必须回表到聚集索引中过滤。

指定索引长度后不会走覆盖索引案例演示:

drop table t_demo;

CREATE TABLE t_demo (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增id',
name VARCHAR(20) DEFAULT NULL COMMENT '姓名',
phone char(11) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

alter table t_demo add index idx_phone(phone(3));


INSERT into t_demo (name,phone) VALUES ('zz','''13900000000');
INSERT into t_demo (name,phone) VALUES ('ls','13700000000');
INSERT into t_demo (name,phone) VALUES ('ls2','13800000000');

INSERT into t_demo (name,phone) VALUES ('zz','''13900000001');
INSERT into t_demo (name,phone) VALUES ('ls','13700000002');
INSERT into t_demo (name,phone) VALUES ('ls2','13800000003');

explain select phone from t_demo where phone = '13900000001';

未使用覆盖索引
在这里插入图片描述

drop table t_demo;

CREATE TABLE t_demo (
id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增id',
name VARCHAR(20) DEFAULT NULL COMMENT '姓名',
phone char(11) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 索引全字段值
alter table t_demo add index idx_phone(phone(11));


INSERT into t_demo (name,phone) VALUES ('zz','''13900000000');
INSERT into t_demo (name,phone) VALUES ('ls','13700000000');
INSERT into t_demo (name,phone) VALUES ('ls2','13800000000');

INSERT into t_demo (name,phone) VALUES ('zz','''13900000001');
INSERT into t_demo (name,phone) VALUES ('ls','13700000002');
INSERT into t_demo (name,phone) VALUES ('ls2','13800000003');


explain select phone from t_demo where phone = '13900000001';

使用覆盖索引
在这里插入图片描述

所以并不是索引字段越小越好,而是要根据索引区分度的计算来进行评估。

查索引字段区分度的方式:

SELECT count(DISTINCT LEFT(order_no, 20)) / count(*) AS '20', count(DISTINCT LEFT(order_no, 22)) / count(*) AS '22', count(DISTINCT LEFT(order_no, 24)) / count(*) AS '24', count(DISTINCT LEFT(order_no, 26)) / count(*) AS '26', count(DISTINCT LEFT(order_no, 28)) / count(*) AS '28', count(DISTINCT LEFT(order_no, 30)) / count(*) AS '30', count(DISTINCT LEFT(order_no, 32)) / count(*) AS '32' FROM test;

在这里插入图片描述
order_no字段长度是32,可以看出从获取长度为26开始,区分度已经接近1,再增加长度性价比已经不高了。

索引对于order_no字段设置索引长度为26比较合适。

区分度小技巧

有些字段的特征在于,前面大多数部分都是相同的,只是后面几位不同,对于这样的字段我们保留前几位作为索引字段时不行的,那实际上可以通过倒序存储的方式来满足。

除了倒序存储的方式,还可以使用Hash的方式,但是要注意存在Hash冲突的问题

总结

虽然在阿里的Java开发手册中,是强制要求varchar字段上建立索引必须要指定索引长度,但通过我们的分析也能看出,并不是索引的字段都适用的,比如区分度不高的、或者可以大量运用覆盖索引的实现的,我认为,开发手册强调的普适场景,具体如何运用应该是我们通过了解原理之后,自行笃定。

猜你喜欢

转载自blog.csdn.net/CSDN_WYL2016/article/details/120127934