所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。多个索引列,按照定义索引时列的顺序对数据进行排序。
适用场景:全键值、键值范围或最左前缀查询。
限制:多个索引列,按最左匹配,不能跳过中间的索引列;如果存在某个列的范围查询,则其右边所有列都无法使用索引优化。
举例:
WHERE last_name='Smith' AND first_name LIKE 'J%' AND dob='1976-12-13'
索引为 key(last_name, first_name, dob),这里只有前两列用到了索引。
Mysql 中,只有 Memory 引擎显式支持哈希索引。体积小,选择性高。
略
略
典型的 B-Tree 按照顺序存储数据,所以 Mysql 可以用来做 ORDER BY 和 GROUP BY操作。
大致有如下三个优点:
- 索引大大减少了服务器需要扫描表的数据量。
- 索引可以帮助服务器避免排序和临时表。
- 索引可以将随机I/O变为顺序I/O
索引列不能是表达式的一部分也不能是函数的参数 例如,下面的这个查询使用使用 actor_id 列的索引
SELECT actor_id FROM sakila.actor WHERE actor_id+1 = 5
另一个常见的错误
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10
索引选择性 —— 不重复的索引值(也称为基数)和数据表的记录总数的比值。 索引列的选择性越高,越适合作索引,查询效率也越高。唯一索引的选择性是1,即每一条都不会重复。。
对于 BLOB、TEXT 或很长的 VARCHAR 类型的列必须使用前缀索引。 为了提高前缀索引的选择性,同时又不能太长(以便节约空间),因此需要对前缀的长度进行考量。
计算完整列的选择性,并使前缀的选择性接近于完整列的选择性。
select count(distinct city)/count(*) from sakila.city_demo
# (平均索引)使用不同的前缀索引进行比较,如果一致或接近,即可考虑使用该长度作为最终前缀长度。
select count(distinct left(city,3)/count(*) as sek3,
count(distinct left(city,4)/count(*) as sek4,
count(distinct left(city,5)/count(*) as sek5,
count(distinct left(city,6)/count(*) as sek6,
count(distinct left(city,7)/count(*) as sek7
from sakila.city_demo;
(公差)如果数据分布很不均匀,可能导致某个字符串的索引选择性高,而其它字符的索引选择性比较低的情况发生。 使用下面的的查询,观察每个字符串重复的情况,重复的个数越少越好。
select count(*) as cnt, city
from sakila.city_demo group by city order by cnt desc limit 10;
select count(*) as cnt, left(city,4) as perf
from sakila.city_demo group by perf order by cnt desc limit 10;
mysql 无法使用前缀索引做 ORDER BY 和 GROUP BY,也无法使用前缀索引做覆盖扫描。
多个列上建立单独的索引大部分情况下并不能提高 Mysql 的查询性能。Mysql 5.0引入了一种“索引合并”(index merge)的的策略, 一定程度上可以使用表上的多个单列索引来定位指定的行。更早的Mysql只能使用其中某一个单列索引。 举例:
SELECT film_id, actor_id FROM sakila.film_actor
WHERE actor_id = 1 OR film_id = 1;
在老的 mysql 版本,mysql 会对这个查询使用全表扫描,除非改写成两个查询 UNION的方式:
SELECT film_id, actor_id FROM sakila.film_actor
WHERE actor_id = 1;
UNION ALL
SELECT film_id, actor_id FROM sakila.film_actor
WHERE film_id = 1;
通常来说,选择性高的索引列排在前面。
聚簇索引是指索引的存储方式。术语“聚簇”表示数据行和相邻的键值紧凑第存储在一起。
InnoDB 默认使用主键聚集数据,如果没有定义主键,使用一个唯一的非空索引代替,如果没有这样的索引,InnoDB 会隐式定义一个主键作为聚簇索引。InnoDB 只聚集在同一个页面中的记录,不同页面可能会相距甚远。
优点
- 将相邻的数据保存在一起。查询相关数据时,只需要从磁盘读取少数的数据页就能获得全部数据。
- 使用索引覆盖扫描的查询可以直接使用页节点的主键值。
缺点
- 插入速度严重依赖插入顺序。
- 插入新行或主键被更新导致需要移动行时,可能面临“页分裂”的问题。
- 可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
- 二级索引(非聚簇索引)可能比想象的要更大,因为二级索引的叶子节点包含了引用行的主键列。
- 二级索引需要两次索引查找。
InnoDB 二级索引的叶子节点中存储的不是“行指针”,而是主键值,并以此作为指向行的“指针”。 优点:减少了当出现行移动或数据页分裂时二级索引的维护工作。 缺点:使用主键值当作指针会让二级索引占用更多的空间。
- 写入的目标页可能已经刷到磁盘上并从缓存中移除,或者是还没有加载到缓存中,InnoDB 在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机I/O。
- 因为写入是乱序的,InnoDB 不得不频繁地做页分裂操作,以便为新的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页面而不是一个页。
- 由于频繁的页分裂,页会变得稀疏并被不规则地填充,所以最终数据会有碎片。
如果一个索引包含所有需要查询的字段的值,我们就称之为“覆盖索引”。
特点:
① 覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引都不存储索引列的值,所以 MySQL 只能使用 B-Tree索引做覆盖索引。
②当发起一个被索引覆盖的查询时,在 EXPLAIN 的 Extra 列可以看到“Using index”信息。
示例:
EXPLAIN SELECT * FROM products WHERE actor='SEAN CARREY' AND title like '%APOLL%'\G;
这里索引无法覆盖,有两个原因:
- 索引无法覆盖所有列。
- MySQL 只能在索引中做最左前缀匹配的 LIKE 比较,无法做通配符开头的 LIKE 索引。
改进
EXPLAIN SELECT *
FROM product
JOIN (
SELECT prod_id
FROM products
WHERE actor='SEAN CARREY' AND title LIKE '%APOLL%'
) AS t1 ON (t1.prod_id=products.prod_id)\G;
我们把这种方式叫做 延迟关联(deferred join) ,因为延迟了对列的访问。在查询的第一阶段使用了覆盖索引,根据查询到的 prod_id值在外层查询匹配获取需要的所有列值。
MySQL 有两种方式生成有序的结果:通过排序操作;或按索引顺序扫描。 如果EXPLAIN出来的type列的值为“index”,则说明 MySQL使用了索引扫描来做排序。
① 索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向都一样。
② 如果需要查询多张表,ORDER BY 子句引用的字段必须全部为第一个表。
③ ORDER BY 子句跟查找型查询的限制是一样的:需满足索引的最左前缀要求。
④ 多表查询时,由于优化器优化可能发生交换联表顺序,导致原来的第一张表变成第二张表。
MyISAM 使用前缀压缩来减少索引的大小,从而让更多的索引可以放入内存中。默认只压缩字符串,通过参数配置也可以对整数做压缩。
重复索引指相当列上创建多个索引,通过没有必要这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求。 比如创建 KEY(col) 和 FULLTEXT(col) 两种索引。
冗余索引指,创建了索引(A,B),再创建索引 (A) 就是冗余索引,因为这只是前一个索引的前缀索引(这种冗余只是对 B-Tree 来说的)。
表中的索引越多,导致INSERT、UPDATE、DELETE 等操作变慢。
在决定哪些索引可以被删除的时候要非常小心。
比如某排序场景中,在列(A)上的二级索引相当于(A,ID),如果有像WHERE A=5 ORDER BY ID
这样的查询,这个索引会很有作用。
但如果将索引扩展为 (A,B),则实际上就变成了(A,B,ID),那么上面的查询的 ORDER BY 子句就无法使用该索引做排序,而只能使用文件排序了。
服务器永远不用的索引,建议考虑删除(某些索引的功能相当于唯一约束,虽然没有被查询使用,但是可以避免产生重复数据)。 打开 userstates 服务器变量,通过查询 INFORMATION_SCHEMA.INDEX_STATISTICS 查询每个索引的的使用频率。
略
-
使用 IN 考虑在选择性低的sex字段作索引的可行性? 某些场景下,可能频繁用到sex过滤条件的查询,那么可以在索引中加上之一列。即使查询没有使用 sex 这一列,也可以通过下面的“诀窍”绕过: 如果某个查询不限制性别,那么可以通过在查询中新增 AND SEX IN('m', 'f') 来让 MySQL 选择该索引。虽然这样写不会过滤任何行,但是必须加上这个条件才能利用索引的最左匹配原则。 比如建立 (sex, country, region, age) 和 (sex, country, region, city, age) 这样的索引非常有用。
-
尽可能将需要做范围查询的列放到索引的后面
-
注意额外增加一个IN()条件,优化器需要做的组合都将以指数形式增加,最终可能会极大地降低查询性能。比如:
WHERE eye_color IN('brown', 'blue', 'hazel')
AND hair_color IN('black', 'red', 'blonde', 'brown')
AND sex IN('M','F')
优化器则会转换成 4x3x2 = 24 种组合,执行计划需要检查 WHERE 子句中所有的 24 种组合。如果组合达到上千个需要特别小心,可能会消耗大量的内存。
比较下面两个查询 范围查询(后续索引列无效)
EXPLAIN SELECT actor_id FROM sakila.actor
WHERE actor_id >45\G;
多个等值条件查询(可以利用索引)
EXPLAIN SELECT actor_id FROM sakila.actor
WHERE actor_id IN(1, 4, 99)\G;
对于那些选择性低的列,可以增加一些特殊的索引来优化排序。例如,可以创建(sex,rating)索引用于下面的查询:
SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;
这个查询同时使用了ORDER BY 和 LIMIT,如果没有索引的话会很慢。
例如下面的查询
SELECT <cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 100000,10;
因为随着偏移量的增加,MySQL 需要花费大量的时间来扫描需要丢弃的数据。反范式化、预先计算和缓存可能是解决之类查询的仅有策略。一个更好的办法是限制用户能够翻页的数量。
优化这类索引的另一个比较好的策略是使用延迟关联,通过使用覆盖索引查询返回需要的主键,再根据这些主键关联原表获得需要的行。之可以减少 MySQL 扫描那些需要丢弃的行数:
SELECT <cols> FROM profiles INNER JOIN (
SELECT <primary key cols> FROM profiles
WHERE x.sex='M' ORDER BY rating IMIT 100000,10) AS x USING(<primary key cols>)
可以使用 SHOW INDEX FROM 命令来查看当前索引的基数(Cardinality)。
在选择索引和编写利用这些索引的查询时,有如下三个原则需要始终记住:
- 单行访问是很慢的。特别是在机械硬盘存储中。最好读取的块中能包含尽可能多所需要的行。
- 按顺序访问范围数据是很快的,这有两个原因。第一,顺序I/O不需要多次磁盘寻道,所以比随机I/O要快很多(特别是针对机械硬盘)。第二,如果服务器能够按照需要顺序读取数据,那么就不需要额外的排序操作, 并且 GROUP BY 查询也无须再做排序和将行按组进行聚合计算了。
- 索引覆盖查询是很快的。如果一个索引中包含了查询需要的所有列,那么存储引擎就不需要再回表查找行。这避免了大量的单行访问。而上面第1点已经写明单行访问是很慢的。
总的来说,编写查询语句时应该尽可能选择合适的索引以避免单行查找、尽可能第使用数据原生顺序从而避免额外的排序操作,并尽可能使用索引覆盖查询。