一本书的目录就是它的索引。
不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。
InnoDB使用的是其变种 B+tree。B-tree通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。
建立在B-tree结构(准确地说是B+tree)上的索引
数据存储示例
sqlCREATE TABLE People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
key(last_name, first_name, dob)
);
(最上面一行有问题,Angelina < Cuba,应该在前面)
索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。
InnoDB存储引擎有一个被称为自适应哈希索引的特性。当InnoDB发现某些索引值被非常频繁地被访问时,它会在原有的B-tree索引之上,在内存中再构建一个哈希索引。可以通过参数关闭这个特性。
todo: 书中说可以使用非前缀。 可以使用B-tree索引的查询类型。
全值匹配
指索引中的列全部出现在 where 条件中。
匹配最左前缀
指索引中从左到右顺序的列出现在 where 条件中。
匹配列前缀
指 where 的某个条件是某个索引的第一个字段,并且是该字段值的前缀。
匹配范围值
指 where 指定的两个限定范围的条件都是某个索引的第一个字段。
精确匹配某一列而范围匹配另外一列
即索引的第一列全匹配,第二列是范围匹配。
只访问索引的查询
即覆盖索引。
因为索引树中的节点是有序的,所以除了按值查找,索引还可以用于查询中的ORDER BY操作(按顺序查找)。
B-tree索引的限制
FULLTEXT是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。
相关概念
可以对字段的前一部分进行索引,可以减少空间占用,但是会降低选择性。索引的选择性指不重复的索引值(也称为基数,cardinality)和数据表的记录总数(#T)的比值。选择性越大查询效率越高。对于BLOB、TEXT或很长的 VARCHAR 类型,必须使用前缀索引,因为 MySQL 不支持这些列的完整内容索引。
设计索引时可以使前缀索引的选择性接近完整列的选择性来优化索引。这需要测试不同的前缀长度的选择性。但是需要注意数据分布不均匀的情况。
创建前缀索引
sqlALTER TABLE tb_name ADD KEY (column(7));
MySQL无法使用前缀索引做ORDER BY和GROUP BY操作,也无法使用前缀索引做覆盖扫描。
一个常见的错误是对多个列单独建立索引,这在大部分情况下不能提升性能。因为 MySQL 引入了一种叫索引合并(index merge)的策略,其同时使用那些单独的索引进行扫描,并将结果合并。
这种算法有三个变种:
索引合并的问题在于
可以通过参数optimizer_switch来关闭索引合并功能,还可以使用IGNORE INDEX语法让优化器强制忽略掉某些索引,从而避免优化器使用包含索引合并的执行计划。
正确的顺序依赖于使用该索引的查询语句,同时,还需要考虑如何更好地满足排序、分组和范围查询操作的需要。
经验法则
聚簇索引不是新的的索引类型,而是一种存储方式。在 InnoDB 中聚簇索引就是 B-tree,但是会在叶子页中存储数据行。因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引(不过,覆盖索引可以模拟多个聚簇索引的情况。)
聚簇索引的数据分布。中间的节点页没有数据,叶子节点存储了数据。
MySQL 内置的存储引擎都不支持选择用于聚簇的索引,InnoDB 根据主键聚簇数据。如果你没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚簇索引。这样做的缺点在于,所有需要使用这种隐藏主键的表都依赖一个单点的“自增值”,这可能会导致非常高的锁竞争,从而出现性能问题。
聚簇数据的优点
聚簇索引的缺点
二级索引的叶子节点存储的是主键的值,所以查询数据需要访问两次索引。
sqlCREATE TABLE layout_test (
col1 int NOT NULL,
col2 int NOT NULL,
PRIMARY KEY(col1),
KEY(col2)
);
InnoDB表layout test的主键物理存储分布
InnoDB表layout test的二级索引分布
代理键(surrogate key)
这种主键的值和应用无关,最简单的方法是使用AUTO_INCREMENT自增列。这样可以保证数据行是按顺序写入的,对于根据主键做联接操作的性能也会更好。
最好避免随机的(不连续且值的分布范围非常大)聚簇索引,特别是对于I/O密集型的应用。例如,从性能的角度考虑,使用UUID作为聚簇索引会很糟糕:它使得聚簇索引的插入变得完全随机,这就是最糟糕的情况,数据本身没有任何聚集特性。
顺序的插入 vs 随机插入
向聚簇索引顺序地插入索引值
向聚簇索引中插入无序的记录
随机插入的缺点
在把这些随机值载入聚簇索引后,最好做一次OPTIMIZE TABLE来重建表并优化页的填充情况。
sqlOPTIMIZE TABLE tb_name;
什么时候按主键顺序插入反而会更糟
对于高并发的工作负载,在InnoDB中按主键顺序插入可能会造成明显的写入竞争。主键的上界会成为“热点”。因为所有的插入都发生在这里,所以并发插入可能导致间隙锁竞争。另一个热点可能是AUTO_INCREMENT锁机制;如果遇到这个问题,则可能需要考虑重新设计表或者应用,或者更改innodb_autoinc_lock_mode配置。
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为覆盖索引。需要注意的是,只有B-tree索引可以用于覆盖索引。当然,二级索引的叶子节点包含主键值,所以在查询列出现主键也可以覆盖。
覆盖索引的优点
MySQL有两种方式可以生成有序的结果:通过排序操作,或者按索引顺序扫描。如果在EXPLAIN的输出结果中,type列的值为“index”,则说明MySQL使用了索引扫描来做排序。
如果索引不能覆盖查询所需的全部列,那么就不得不每扫描一条索引记录都回表查询一次对应的记录。这基本上都是随机I/O,因此按索引顺序读取数据的速度通常要比顺序地全表扫描慢。
只有当索引的顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySQL才能使用索引来对结果做排序。如果查询需要联接多张表,则只有当ORDER BY子句引用的字段全部在第一个表中时,才能使用索引做排序。ORDER BY子句和查找型查询的限制是一样的:需要满足索引的最左前缀的要求,否则,MySQL需要执行排序操作,而无法利用索引排序。
例子
sqlCREATE TABLE rental (
...
PRIMARY KEY (rental_id),
UNIQUE KEY rental_date (rental_date,inventory_id,customer_id),
KEY idx_fk_inventory_id (inventory_id),
KEY idx_fk_customer_id (customer_id),
KEY idx_fk_staff_id (staff_id),
...
);
可以使用索引排序的例子
sql... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC;
... WHERE rental_date > '2005-05-25' ORDER BY rental_date, inventory_id;
不能使用索引排序的例子
sql-- 索引中的列都是按正序排序的, 下面的 inventory_id 使用逆序了
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id DESC, customer_id ASC;
-- 引用了一个不在索引中的列
... WHERE rental_date = '2005-05-25' ORDER BY inventory_id, staff_id;
-- WHERE和ORDER BY中的列无法组合成索引的最左前缀
... WHERE rental_date = '2005-05-25' ORDER BY customer_id;
-- 在索引列的第一列上是范围条件,所以MySQL无法使用索引的其余列排序
... WHERE rental_date > '2005-05-25' ORDER BY inventory_id, customer_id;
-- 这个查询在inventory_id列上有多个等于条件。对于排序来说,这也是一种范围查询
... WHERE rental_date = '2005-05-25' AND inventory_id IN(1,2) ORDER BY customer_id;
MySQL允许在相同列上创建多个相同的索引,这会影响性能同时也浪费磁盘空间。
大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新的索引。但有时候出于性能方面的考虑也需要冗余索引,因为扩展已有的索引会导致其变得太大,从而影响其他使用该索引的查询的性能。
没什么用,应该删除。查找未使用的索引:
sqlSELECT * FROM sys.schema_unused_indexes;
维护表有三个主要目的:找到并修复损坏的表,维护准确的索引统计信息,减少碎片。
找到并修复损坏的表
如果你遇到了古怪的问题,可以尝试运行CHECK TABLE来检查是否发生了表损坏(有些存储引擎不支持)。可以使用REPAIR TABLE命令来修复损坏的表,但同样不是所有的存储引擎都支持该命令。如果存储引擎不支持,可通过一个不做任何操作(n o-o p)的ALTER操作来重建表,例如,将表的存储引擎修改为当前的引擎。
sqlALTER TABLE <table> ENGINE=INNODB;
此外,还可以将数据导出再导入一次。不过,如果损坏的是系统区域,或者是表的“行数据”区域,而不是索引,那么上面的办法就没有用了。在这种情况下,可以从备份中恢复表,或者尝试从损坏的数据文件中尽可能地恢复数据。
更新索引统计信息
MySQL的优化器使用的是基于成本的模型,而衡量成本的主要指标就是一个查询需要扫描多少行。如果表没有统计信息,或者统计信息不准确,优化器就很有可能做出错误的决定。可以通过运行ANALYZE TABLE来重新生成统计信息,以解决这个问题。
可以使用SHOW INDEX FROM命令来查看索引的基数(cardinality)。
sqlSHOW INDEX FROM tb_name\G
索引统计信息何时更新
如果有大量的表,会带来性能问题,可以关闭innodb_stats_on_metadata参数来避免。
减少索引和数据的碎片
B-tree索引可能会产生碎片化,这会降低查询的效率。碎片化的索引可能会以很差或者无序的方式存储在磁盘上。根据设计,B-tree索引需要随机磁盘访问才能定位到叶子页,所以随机访问总是不可避免的。
表的数据存储也可能发生碎片化。有三种类型的数据碎片:
可以通过执行OPTIMIZE TABLE或者导出再导入的方式来重新整理数据。对于那些不支持OPTIMIZE TABLE的存储引擎,可以通过一个不做任何操作(no-op)的ALTER TABLE操作来重建表。
sqlALTER TABLE <table> ENGINE=<engine>;
总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找,尽可能地使用数据内部顺序从而避免额外的排序操作,并尽可能地使用索引覆盖查询。
本文作者:jdxj
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!