(2022.06.18 Sat)
在索引使用过程中,需要注意诸多问题,这里列举若干。
评价索引设置的优劣,由L&L提出了three-star system,星级越高越好:
- 一星:索引将相关的记录放到一起
- 二星:索引中的数据顺序和查询中的排列顺序一致
- 三星:索引中的列包含了查询中的全部列
不参与运算的列
在WHERE
从句中的索引不能是表达式的一部分或函数的参数。比如下面这个查询无法使用作为检索条件的索引。
mysql> SELECT actor_id FROM actor WHERE actor_id + 1 > 4;
MySQL无法解析这个WHERE
从句中的语句,尽管轻易得知actor_id
的范围。为保证使用索引,应将索引列放在运算符号的一侧。
下面这种将索引列作为函数参数的方式也无法使用索引。
mysql> SELECT ... WHERE TO_DAYS(current_date) - TO_DAYS(date_poop) < 10;
前缀索引和索引选择性
有长字符串的列被用作索引,会导致索引变大变慢,这种列除了使用哈希索引,还可以对前缀做索引。
使用部分字符做索引,可大大节省索引空间,但可能导致索引选择性降低。索引的选择性,是不重复的索引值(基数,cardinality)与数据表的记录总数的比值。该值从0趋近于1,越接近于1则查询效率越高,因选择性高的索引可以在查找是过滤掉更多的行。最好的索引其选择性是1,效率最高,性能最好。
对于字符型列的前缀选取,重点是在选择性和前缀长度之间做出平衡。前缀长度增加显然增强选择性,但是会浪费索引空间,以此类推。如果前缀的基数接近完整列的基数,则最佳。
为找到最适合的前缀长度,首先可以查看最常见值的情况,并和最常见的前缀值进行比较。
mysql> SELECT COUNT(*) as cnt, city FROM cities
GROUP BY city ORDER BY cnt DESC LIMIT 10;
返回城市名字出现次数的频度。
接下来对前缀进行统计,使用MySQL的LEFT(vol_name, n)
找出vol_name
字段每个值的前n
为字符。
mysql> SELECT COUNT(*), LEFT(city, 3) as ref FROM cities
GROUP BY cnt ORDER BY cnt DESC LIMIT 10;
只用3位查询,结果可能会比较大,可以将不同长度的前缀的统计结果都展示出来。首先查看整个列表的完整性指标
mysql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM cities;
之后查看不同长度前缀的完整性指标
SELECT COUNT(DISTINCT LEFT(city, 3)) as sel3,
COUNT(DISTINCT LEFT(city, 4)) as sel4,
COUNT(DISTINCT LEFT(city, 5)) as sel5,
COUNT(DISTINCT LEFT(city, 6)) as sel6,
COUNT(DISTINCT LEFT(city, 7)) as sel7
FROM cities;
从不同长度的结果中发现长度越长,则完整性指标越接近整个列表的指标。因而在长度变长后完整性基本不变时可以确定长度。
比如这个案例中可能确定长度最佳为7,则将cities
列的前7位作为索引,使用下面指令将其加为索引
mysql> ALTER TABLE cities ADD KEY (city(7));
前缀索引的缺点是,无法使用前缀列做ORDER BY
和GROUP BY
,也无法做覆盖索引。
多列单独索引
给一个关系中的多个列分别单独建立索引,并不一定能提升效率。比如在下面查询中,尽管WHERE
从句标明了被索引的列actor_id
和film_id
,但是系统仍然执行全盘扫描。
SELECT film_id, actor_id FROM movies
WHERE actor_id = 3 OR film_id = 3;
除非使用UNION
命令单独对索引列做查询条件
SELECT film_id, actor_id FROM movies
WHERE actor_id = 3
UNION ALL
SELECT film_id, actor_id FROM movies
WHERE film_id = 3 AND actor_id <> 1;
可通过EXPLAIN
的方式查看查询的基本信息,特别是其中的Extra
列的结果。
mysql> EXPLAIN SELECT film_id, actor_id FROM movies
WHERE actor_id = 3 OR film_id = 3;
选择合适的索引顺序
在多列的B-tree索引中,索引列的顺序意味着索引按照从左到右的顺序依次排序。
多列索引的一个经验是将选择性最高的列放在前面。此时索引的作用只是优化WHERE
条件查找。然而查询性能不止依赖索引列的选择性,也和值分布有关。
分析下面案例
SELECT * FROM payment WHERE staff_id = 2 AND customer_id = 1000;
其中的两个字段哪个应该在前面?我们先来看各自有多少个。
SELECT SUM(staff_id=2) ss, SUM(customer_id=1000) sc FROM payment;
返回结果
*********************1. row **********************
SUM(staff_id=2) = 7992
SUM(customer_id=1000) = 30
这个分析结果显示customer_id=1000
返回的结果远小于staff_id=2
的结果数目,显然customer_id=1000
放在前面的检索效果更好。为验证这个结果,我们查看满足customer_id
条件的staff_id
数目。
mysql> SELECT SUM(staff_id=2) WHERE customer_id=1000;
*********************1. row **********************
SUM(staff_id=2) = 17
但是这个案例的分析仅仅是基于单个数据,无法代表整体数据的分布情况。鉴于此,可以查看全局基数和选择性。
SELECT COUNT(DISTINCT staff_id)/COUNT(*) as sid_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) as cid_selectivity,
COUNT(*)
FROM payment;
*********************1. row **********************
sid_selectivity: 0.0001
cid_selectivity: 0.0373
COUNT(*): 16049
可见customer_id
的选择性更高,可作为索引的第一列
mysql> ALTER TABLE payment ADD KEY(customer_id, staff_id);
聚簇索引 Cluster index
(2022.06.20 Mon)
不同于前面提到的索引,聚簇索引不单是一种索引类型,更是一种数据存储方式。InnoDB中的聚簇索引在同一结构中保存了B-tree索引和数据行。存储引擎负责实现索引,并非所有引擎都支持索引。
如下所示的聚簇索引,在聚簇索引每个叶子页(leaf page)上,保存的是该索引对应的数据行。相比之下,索引的叶子页上保存的是该索引对应的数据的指针。实际上,cluster聚簇的意思就是数据行和对应的键值存储在一起。
无法同时把数据存在两个地方,因此一个表只能有一个聚簇索引。如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有,InnoDB会隐式的定义一个主键作为聚簇索引。
优缺点
优点:
- 把相关数据保存在一起,只需要读取少数数据页就可以获得所需数据,而没有使用聚簇索引可能导致磁盘I/O
- 数据访问更快,叶子页保存键值和数据,不需要普通索引中从指针读取数据的步骤
- 覆盖索引扫描的查询可以直接使用叶节点中的主键
缺点:
- 提高了I/O密集型应用的性能,但如果数据都存放在内存,则聚簇索引没有优势
- 插入速度依赖于插入顺序。按主键顺序插入是加载数据到InnoDB最快的方式,如果没有按主键顺序加载,则需使用
OPTIMIZE TABLE
组织一下表 - 更新聚簇索引的代价高,因为会强制将被更新的行移动到新的位置
- 插入新行可能面临页分离(page split)的问题,会导致表占用更多的磁盘空间
- 可能导致全盘搜索变慢,尤其是行稀疏的时候,或者分裂导致数据存在不同页的时候
- 二级索引,即非聚簇索引可能比想像的更大,因二级索引的叶子结点保存了引用行的键值。
关于二次索引:二次索引需要两次索引查找,而非一次。二次索引也通过B-tree保存,其叶子节点保存的是行的主键值,而非指向行的物理位置的指针。通过二次索引查找行,存储引擎需要找到二次索引中叶子节点对应的主键值,再根据这个值去聚簇索引中找到对应的行。这里使用了两次B-tree查找。
聚簇索引(InnoDB)和非聚簇索引(MyISAM)的数据分布对比
在聚簇索引中按主键顺序插入行
如果InnoDB中没有数据需要聚集,可定义一个代理键surrogate key作为主键,其数据和应用无关。最简单的方法是使用自增序列AUTO_INCREMENT
,可保证数据行按顺序写入,做关联操作的性能也会更好。
最好避免随机的,即不连续且值分布范围大的聚簇索引,特别是对于I/O密集型的应用,使得数据没有聚集性。
(2022.05.06 Fri)
聚集索引和非聚集索引的差别在于:聚集索引的物理顺序和逻辑顺序相同,而非聚集不同。一个关系只能有一个聚集索引,可以有多个非聚集索引。
聚集索引
一个关系中的主键Primary key就是该关系的索引。建立了主键/索引的关系,从一行行整齐但无序排列在硬盘上的结构变成了一个平衡树/B+树结构,这个关系变成了一个索引,也就是聚集索引。这解释了一个关系为什么只能有一个主键/聚集索引,却可以有多个非聚集索引。聚集索引把数据关系转变成平衡树的结构。
在聚集索引的平衡树中,所有节点都由主键字段中数据构成,即主键id字段。例如我们查询某个关系中主键id=199对应的记录,假设该平衡树保存了10000个记录,高度为3。通过平衡树找到id=199对应的叶节点,再通过该叶节点找到相应的数据行。
下面分析索引对查询效率的提升。上面例子中,平衡树高度为3,树的高度决定了IO开销,即查询3次可查到对应的数据。但是如果不使用平衡树结构,则最坏情况下需要遍历10000次匹配/IO开销才能找到所查询的结果。这里的效率对比是3次IO开销和10000次IO开销。设平衡树的每层树分支个数为b
,关系中记录总数为n
,则不经过索引查询特定记录的复杂度是,而经过索引/平衡树处理后的查询复杂度为。可见索引使得查询效率极大提升。
经过聚集索引处理的关系,查询速度得到显著提升,但是每次对关系的更新都会导致平衡树/索引的重新构建,带来额外的工作量。
非聚集索引
同样采用平衡树作为数据结构。索引树结构中各节点的值来⾃于表中的索引字段。每次给字段建立⼀个新索引, 字段中的数据就会被复制⼀份出来, ⽤于⽣成索引。 因此, 给表添加索引,会增加表的体积, 占⽤磁盘存储空间。
⾮聚集索引和聚集索引的区别在于,通过聚集索引可以查到需要查找的数据,⽽通过⾮聚集索引可以查到记录对应的主键值,再使⽤主键的值通过聚集索引查找到需要的数据。不管以何种方式查询
,都会利⽤主键通过聚集索引来定位到数据,聚集索引/主键是通往真实数据所在的唯⼀路径。
Reference
1 高性能MySQL第三版,宁等翻译,电子工业出版社