0
点赞
收藏
分享

微信扫一扫

MySQL(三)|《千万级大数据查询优化》第一篇:创建高性能的索引

MySQL优化一般是需要索引优化、查询优化、库表结构优化三驾马车齐头并进。
可以说,索引优化是对查询性能优化最有效的手段,索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好几个数量级。创建一个真正“最优”的索引经常需要重写查询,所以索引优化和查询优化的关系很紧密。
本文是《千万级大数据查询优化》系列第一篇:创建高性能的索引。
我们先从一个面试题开始。

看完四位求职者的回答,你的答案是什么呢?

一、分析四位求职者的答案

上面的面试题涉及到的知识是多列索引的创建和选择合适的索引列顺序,我们先创建一个表进行测试。

# 创建数据表
create table tb_test_1(
   id smallint unsigned auto_increment primary key,
   AAA varchar(100) not null,
   BBB varchar(100) not null,
   CCC varchar(100) not null,
   DDD varchar(100) not null
);

# 插入数据,执行几十次
INSERT INTO tb_test_1 VALUES 
(null, CONCAT('aaa', ROUND(RAND()*1)), CONCAT('bbb', ROUND(RAND()*1)), CONCAT('ccc', ROUND(RAND()*1)), CONCAT('ddd', ROUND(RAND()*1))),
(null, CONCAT('aaa', ROUND(RAND()*10)), CONCAT('bbb', ROUND(RAND()*10)), CONCAT('ccc', ROUND(RAND()*10)), CONCAT('ddd', ROUND(RAND()*10))),
(null, CONCAT('aaa', ROUND(RAND()*100)), CONCAT('bbb', ROUND(RAND()*100)), CONCAT('ccc', ROUND(RAND()*100)), CONCAT('ddd', ROUND(RAND()*100))),
(null, CONCAT('aaa', ROUND(RAND()*1000)), CONCAT('bbb', ROUND(RAND()*1000)), CONCAT('ccc', ROUND(RAND()*1000)), CONCAT('ddd', ROUND(RAND()*1000)));

再按照四位面试者的回答一一进行测试。
先按照第一个求职者回答到:需要创建3个组合索引(a, b, c)、(a, c)、(b, c):

ALTER TABLE tb_test_1 ADD INDEX idx_abc (AAA, BBB, CCC);
ALTER TABLE tb_test_1 ADD INDEX idx_ac (AAA, CCC);
ALTER TABLE tb_test_1 ADD INDEX idx_bc (BBB, CCC);

再把四个查询分别执行下,通过执行计划检查命中索引的情况如何,在分析之前先把EXPLAIN字段的含义进行一个说明,如下所示:

分别执行如下四个查询:

EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' AND BBB='bbb1' AND CCC='ccc1';
EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' and BBB='bbb1';
EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' and CCC='ccc1';
EXPLAIN SELECT * FROM tb_test_1 WHERE BBB='bbb1' and CCC='ccc1';

执行计划如下:

通过执行计划得知,前面两个查询使用了idx_abc组合索引,后面两个查询分别使用了idx_ac和idx_bc两个组合索引。

再按照第二个求职者回答:也是需要3个组合索引(a, b)、(b, c)、(a, c)。为了排除干扰,先把之前的索引全部删除。

# 删除索引
DROP INDEX idx_abc ON tb_test_1;
DROP INDEX idx_ac ON tb_test_1;
DROP INDEX idx_bc ON tb_test_1;

# 创建3个组合索引(a, b)、(b, c)、(a, c)
ALTER TABLE tb_test_1 ADD INDEX idx_ab (AAA, BBB);
ALTER TABLE tb_test_1 ADD INDEX idx_ac (AAA, CCC);
ALTER TABLE tb_test_1 ADD INDEX idx_bc (BBB, CCC);

再次执行

EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' AND BBB='bbb1' AND CCC='ccc1';
EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' and BBB='bbb1';
EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' and CCC='ccc1';
EXPLAIN SELECT * FROM tb_test_1 WHERE BBB='bbb1' and CCC='ccc1';

再来看看第三个求职者回答(第四个的回答在普通情况下一致):只需要2个组合索引(a, b, c)、(b, c)。

# 删除索引
DROP INDEX idx_ab ON tb_test_1;
DROP INDEX idx_ac ON tb_test_1;
DROP INDEX idx_bc ON tb_test_1;

# 创建2个组合索引(a, b, c)、(b, c)
ALTER TABLE tb_test_1 ADD INDEX idx_abc (AAA, BBB, CCC);
ALTER TABLE tb_test_1 ADD INDEX idx_bc (BBB, CCC);

执行计划如下:

EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' AND BBB='bbb1' AND CCC='ccc1';
EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' and BBB='bbb1';
EXPLAIN SELECT * FROM tb_test_1 WHERE AAA='aaa1' and CCC='ccc1';
EXPLAIN SELECT * FROM tb_test_1 WHERE BBB='bbb1' and CCC='ccc1';

最后,我们从key和rows的值来对比这三种情况的结果如何。
第一种:创建3个组合索引(a, b, c)(a, c)(b, c)
key:idx_abc;rows:2
key:idx_abc;rows:7
key:idx_ac;rows:8
key:idx_bc;rows:5
第二种:创建3个组合索引(a, b)(b, c)(a, c)
key:idx_bc,idx_ab;rows:1
key:idx_ab;rows:7
key:idx_ac;rows:8
key:idx_bc;rows:5
第三种:创建2个组合索引(a, b, c)(b, c)
key:idx_abc;rows:2
key:idx_abc;rows:7
key:idx_abc;rows:21
key:idx_bc;rows:5

从索引数量和遍历的行数两个指标来评价,第一、二种的效果是一样的,都需要3个组合索引,第三种的组合索引数量是2个,但是在WHERE AAA='aaa1' and CCC='ccc1'查询时遍历的行数为21,比前面两种的8要大。
综合来说,在普通情况下,四位求职者的回答都是正确的。但是作为面试官来说,虽然前面三位都回答正确了,但是肯定都得不到录用!
第四位求职者说到“索引的区分度”是什么意思呢?我们以此为契机来分析如何创建一个高性能的索引。

二、创建高性能的索引

理解了后面的内容,第四位求职者的答案是否正确读者自己去判断。

2.1、组合索引:将选择性最高的列放到索引最前列

在创建组合索引时,需要选择合适的索引列顺序。合适的索引列顺序有一个经验法则:将选择性最高的列放到索引最前列(注意:这个法则也是在不需要考虑排序和分组的通常情况下有用)。
比如我们要查询WHERE AAA='aaa1' and BBB='bbb1',组合索引是应该idx_ab还是idx_ba?参考经验法则,先来看看这两个值在这个表中的分布情况,确定哪个列的选择性更高。如下查看AAA和BBB两个列的选择性值“

# 查看选择性值
SELECT COUNT(DISTINCT AAA)/COUNT(*) AS aaa_selectivity, COUNT(DISTINCT BBB)/COUNT(*) AS bbb_selectivity, COUNT(*) FROM tb_test_1;

执行结果如下:

从结果中的值来看,AAA的选择性高于BBB,那么从这个方面来考虑组合索引应该为idx_ab。

2.2、索引长度和区分度的取舍

首先介绍下索引长度和区分度的概念。索引长度很好理解,就是这个索引的长度。我们在上面提到的:

这里提到的精确性也就是稍微的区分度。通常情况下索引长度和区分度是相互矛盾的。我们举例说明,向tb_test_1表中插入如下数据。

INSERT INTO tb_test_1 VALUES 
(null, 'aaaaaaaaaaaaaaaaaaaaaaaa1111', 'b', 'c', 'd'),
(null, 'aaaaaaaaaaaaaaaaaaaaaaaa1112', 'b', 'c', 'd'),
(null, 'aaaaaaaaaaaaaaaaaaaaaaaa1122', 'b', 'c', 'd'),
(null, 'aaaaaaaaaaaaaaaaaaaaaaaa1222', 'b', 'c', 'd');

在创建索引之前我们需要找出“索引长度和区分度”之间的平衡值,这个很有必要。因为当索引很长时,这会让索引变得大且很慢。诀窍就是选择足够长的索引长度以保证较高的区分度,同时又不能太长(以便节约空间),也就是前缀索引应该足够长,以使得前缀索引的选择性接近于整个列。我们先找出整个列的选择性:

SELECT COUNT(DISTINCT AAA)/COUNT(*) AS aaa_selectivity FROM tb_test_1;

得出的选择性值为0.5625,如下图。

进行找出最接近整个列的选择性值的最小索引长度。

SELECT COUNT(DISTINCT LEFT(AAA,4))/COUNT(*) AS aaa_selectivity_4,
COUNT(DISTINCT LEFT(AAA,5))/COUNT(*) AS aaa_selectivity_5,
COUNT(DISTINCT LEFT(AAA,6))/COUNT(*) AS aaa_selectivity_6,
COUNT(DISTINCT LEFT(AAA,7))/COUNT(*) AS aaa_selectivity_7,
COUNT(DISTINCT LEFT(AAA,8))/COUNT(*) AS aaa_selectivity_8,
COUNT(DISTINCT LEFT(AAA,9))/COUNT(*) AS aaa_selectivity_9,
COUNT(DISTINCT LEFT(AAA,26))/COUNT(*) AS aaa_selectivity_26,
COUNT(DISTINCT LEFT(AAA,27))/COUNT(*) AS aaa_selectivity_27,
COUNT(DISTINCT LEFT(AAA,28))/COUNT(*) AS aaa_selectivity_28,
COUNT(DISTINCT LEFT(AAA,29))/COUNT(*) AS aaa_selectivity_29
FROM tb_test_1;

执行结果如下:

从结果中我们得知,当索引长度为28时,区分度和整个列是一致的,当索引长度为6之后,区分度也已经很高了,为0.5391,比整个列的0.5625差不了多少。当然因为长度为28也不是很大,我们把索引长度定位28,在实际应用中,当索引再长的话就不得不的损失一些精确性。
ALTER TABLE tb_test_1 ADD INDEX idx_a (AAA(28));

2.3、网上关于索引的一些传说

最后贴上网上关于索引的一些传说,读者可以先判断下是否正确。我也会在后面的文章一一验证。


嗨,能够看到这的读者都不容易,为了验证有多少人完整地看完,请看完的读者在评论一句“爆炸”。
PS:本文写了三个小时,写完之后脑子要爆炸。请点个赞吧~

举报

相关推荐

0 条评论