一、索引基础
索引:是存储引擎用于快速找到记录的一种数据结构
- 索引在大数据量时对性能的影响很重要
- 索引优化是查询优化最有效的手段
1、索引的类型
MySQL 中的索引是存储在引擎层,不同引擎的索引工作方式不同。
B-Tree 索引
- B-Tree 是比较常用的引擎,InnoDB 使用的是 B+Tree
- B-Tree 索引能够加快访问数据的速度,从索引的根节点扫描,不需要全表扫描
- B-Tree 对索引列是顺序组织存储的,所以很适合查找范围数据
- B-Tree 索引适用于全键值‘键值范围或者键前缀查找
有效范围:
- 全值匹配
- 匹配最左前缀
- 匹配列前缀
- 匹配范围值
- 精确匹配某一列并范围匹配另外一列
- 只访问索引的查询
限制:
- 如果不是按照索引的最左列开始查找,则无法使用索引。
- 不能跳过索引中的列。
- 有范围查询的列的右边所有列,都无法使用索引优化查找
Hash索引
- 基于 hash 表实现,只有精确匹配索引的所有列的查询才有效
- 对于每一行数据,存储引擎都会根据键值计算出一个 hashCode,存到 hash 索引中,同时保存指向每行数据的指针
- 只有 Memory 引擎显示支持 非唯一hash 索引。出现 hash 冲突的时候,会用链表存放多个记录指针到同一个 slot
限制:
- 只包含 hashcode 和指针,不存储字段值,所以不能用索引中的值来避免读取行。
- hash 索引数据不是按照索引值顺序,而是按照槽 slot 顺序来排序的,所以不支持排序。
- 不支持对部分索引列进行匹配查找,因为 hashcode 是根据索引列全部内容计算的。
- 只支持等值比较查询,
=、IN()、<=>
- 访问 hash 索引速度非常快,但是出现 hash 冲突时,存储引擎必须遍历链表中所有指针
- 如果 hash 冲突很多,索引维护的代价会很高
在 InnoDB 中使用 hash 索引
InnoDB 自适应 hash 索引:当某些索引值被用的很频繁时,他会在内存中基于 B-Tree 索引之上再创建一个 hash 索引。
创建自定义hash索引:
在 B-Tree 上创建一个伪 hash 索引,查找的时候 B-Tree 使用 hashcode 进行索引查找,在 where 中指定使用 hash 函数。
1、
select id from url where url="www.baidu.com" and url_crc=CRC32("www.baidu.com")
Mysql优化器会使用这个选择性很高而性能很好的 url_crc 列的索引来查找。
2、需要手动维护 hashcode:
CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url);
这样在每次insert 和 update 时都会利用crc32()
自动生成 hashcode
3、当有大量 hash 冲突的时候,可以自己实现一个64位 hash 函数
同时需要在查询的时候where包含常量值。
select id from url where url_crc=CRC32("www.baidu.com") AND url="www.baidu.com"
空间数据索引(R-Tree)
用作存储地理数据
全文索引
查找的是文本中的关键词,而不是直接比较索引中的值。
二、索引的优点
- 大大减少了服务器需要扫描的数据量,可以快速定位到表的指定位置
- 可以帮助服务器避免排序和临时表
- 可以将随机 IO 变为顺序 IO
限制:
- 只有对于中到大型的表,索引才非常有效。小型表全表扫描更快。
- 表太多时可以建立一个元数据表,用来查询需要用到的特性
三、高性能的索引策略
1、独立的列
- 索引列不能是表达式的一部分,也不能是函数的参数,否则索引失效
2、前缀索引和索引选择性
对于很长字符串的索引列,我们可以:
1、采用模拟 hash 索引
2、可以索引开始的部分字符,选择列的一个足够长的前缀
选择性:不重复的索引值和数据表记录总数的比值。
选择性低指的是,根据索引找到的记录需要过滤很多行。
如何选择合适长度的列前缀?
- 通过
select count
找出每个记录的count - 通过
select count,left(column,n)
来多次尝试,直到每行记录的 count 接近完整列。- 每次截取N 位前缀,进行 count
- 也可以通过计算完整列的选择性来判断
//count(DISTINCT city)不重复的索引值
//count(*) 记录总数
select count(DISTINCT city)/count(*) from tb
//选取不重复的前 N 位的前缀的 count,和记录总数对比
select count(DISTINCT LEFT(column,n))/count(*) AS selN
这个方法有个缺点:
当数据分布不均匀时,即使选择性比较大,实际可能select count,left(column,n)
的重复会比较多,因为他用了 DISTINCT
缺点:MySQL 无法使用前缀索引做 OrderBy 和 GroupBy,也无法使用前缀索引做覆盖扫描。
3、多列索引
- 在多个列上建立单独的索引,或者多列索引顺序错误并不能提高查询性能。
- 如果不能创建一个三星索引,不如忽略 where, 集中精力优化索引列的顺序,或者创建一个全覆盖索引。
MySQL5.0引入了索引合并策略(index merge):
一定程度上可以使用多个单列索引来定位数据。
mysql> select film_id,actor_id from movie
—> where actor_id=1 or film_id=1
从Explain
命令结果可以看出,该语句执行使用了合并索引。能够同时使用这俩单列索引进行扫描,并将结果合并。
索引合并的缺点:
- 当有多个 AND 条件时,意味着需要一个多列索引
- 当有多个 OR 条件时,需要耗费大量 CPU和内存资源在算法的排序缓存合并上,特别是选择性不高需要筛选的时候
- 优化器不会将这些计算到 cost 查询成本中,导致这种操作不如走全表扫描,而且还会影响查询的并发性。不如改写成UNION。
4、选择合适的索引列顺序
B-Tree 索引列的顺序是从左至右依次排序。所以索引可以按照升序降序进行扫描,以满足 ORDER BY、GROUP BY和 DISTINCT 需求。
如何选择合适的顺序?
- 经验法则:将选择性最高的列放到索引的最前列。这时候索引的作用只是优化 where 的查找。
- 性能不只是依赖于索引的选择性,对待具体数据集要进行具体分析,可以根据高频的查询来调整索引列顺序。
- 当使用前缀索引,某些条件值基数高于正常值时,需要考虑对数据集进行分组查询、范围查询。
5、聚簇索引
1)什么是聚簇索引
- 聚簇索引是一种数据存储方式。
- 聚簇索引是指键值和数据 data 存储在一起
2)聚簇索引的优势
- 可以把相关数据保存在一起,减少磁盘 IO 次数
- 从聚簇索引中访问数据比非聚簇索引快,因为存储在同一个 B+Tree 中
- 使用覆盖索引扫描的查询,可以直接使用页节点中的主键值
3)聚簇索引的缺点
- 当数据放在内存中时,聚簇索引没有优势
- 插入速度严重依赖于插入顺序
- 更新聚簇索引列的代价很高
- 当插入、主键更新等移动行的时候,可能会导致“页分裂”问题,导致占用更多磁盘空间
- 会导致全表扫描变慢,特别是数据稀疏,页分裂导致数据不连续的时候
- 二级索引会很大,因为包含了主键列
- 二级索引访问需要两次索引查找
二级索引:又叫非聚簇索引,辅助索引。指B+Tree 上存放的 data 不是数据行,而是主键值。仅当主键发生改变时,才会更新二级索引
4)InnoDB 的数据分布
- 在 InnoDB 中,聚簇索引“就是”表
- 聚簇索引的每一个叶子节点都包含了完整的数据
- InnoDB 中,二级索引存储的是主键值,减少了行移动和页分裂时二级索引的维护
5)在 InnoDB 中按主键顺序插入行
- 应该避免使用随机的,分布范围大的聚簇索引,不然会因为页分裂和碎片导致占用的时间和空间都很大
- 因为如果主键值是顺序的,当达到页的最大填充因子(15/16)时,下一条记录就会写进新的页中。
- 如果主键是随机的,那么每次插入都要寻找合适的位置并分配空间,导致大量随机 IO
- 因为写入是乱序的,所以页分裂操作很频繁,导致移动大量数据,最终会有数据碎片
- 所以使用 InnoDB 应该尽可能的按照主键顺序插入数据,使用单调增的聚簇键
6、覆盖索引
1)什么是覆盖索引
如果一个索引包含了所有要查询的字段的值,就是覆盖索引
2)覆盖索引的优点
- 覆盖索引只需要扫描索引而无需回表查询,极大的减少数据访问量和 IO,提高性能
- 如果 InnoDB 的二级索引可以覆盖查询,就可以避免对主键索引的二次查询
3)覆盖索引的陷阱和使用技巧
- 只有 B-Tree 索引才能做覆盖索引,因为必须存储列索引的值
-
使用延迟关联来使用覆盖索引:先使用索引的列进行筛选,然后再关联查询
- 使用覆盖索引优化的效果取决于 where 条件返回的行数,也就是数据集不同,效果不同
- InnoDB 可以使用主键列来覆盖查询
7、使用索引扫描来做排序
MySQL 排序的方式:通过排序操作,或者按索引顺序扫描。
EXPLAIN TYPE='index'
说明使用了索引扫描来做排序。
索引排序的条件:
- 只有索引的列顺序和 ORDER BY的顺序完全一致,并且所有列要么都倒序要么都正序
- 关联多表时,只有当 ORDER BY 引用的字段都是第一张表时,才能使用索引排序
- 需要满足最左前缀要求
- 当前一列指定了常量值时,可以不满足最左前缀
where a ='1' order by b
8、压缩(前缀压缩)索引
- MyISAM 通过前缀压缩来减少索引的大小,让更多的索引放入内存中。
- 默认只压缩字符串
- 先保存索引块中第一个值,然后其他值和第一个进行比较,得到相同前缀的字节数和剩余的后缀
1.perform 2.performance-->7.ance
缺点:
- 无法使用二分法查找,只能从头扫描
- 倒序扫描的速度很差,因为在块中查找某一行的操作,平均都要扫描半个索引块
- 对于 CPU 密集型应用,扫描需要随机查找,所以比索引查找慢好几倍
- 对于 IO 密集型应用,对某些查询会带来好处
9、冗余和重复索引
重复索引:在相同的列上按照相同的顺序创建的相同类型的索引。
冗余索引:和已有索引不重复,但是功能已经包含的索引。
- 大多数情况不需要冗余索引,应该尽量扩展已有索引而不是创建新索引
- 在扩展索引变得过大时,可以考虑冗余索引
- 表中的索引越多,增删改速度会越慢
10、索引和锁
InnoDB 只有在访问行的时候才会对其加锁,而索引能够减少 InnoDB 访问的行数,从而减少锁的数量。
当 InnoDB 检索到数据并返回给服务层以后,这时候才能应用 where,然后锁定行。
所以就算用了索引,也可能会锁住一些不需要的数据。
InnoDB 在二级索引上使用读锁,但是访问主键索引需要写锁,这就不能使用覆盖索引了。
四、索引案例学习
1、支持多种过滤条件
- 考虑表上所有的选项,将高频但是选择性低的列加入索引,通过
IN()
绕过 - 通过
IN()
的技巧避免冗余索引 - 忽略选择性高,使用频率低的列
- 将范围查询放到最后,尽可能使用更多的索引
2、避免多个范围条件
MySQL 的EXPLAIN
无法区分范围查询和多等值查询:
对于where actor_id>54
和 where actor_id in (1,2,45)
,EXPLAIN 的结果都是 Range。
但是后者对后面的列使用索引没有限制。
为什么要避免多个范围条件?
因为即使在多个条件上都有索引,在查询的时候也只能使用一个,其他的都不会起作用。
可以将一些范围查询转换为等值比较。
3、优化排序
- 对于翻页到很后面的查询,MySQL 需要随着偏移量的增加,花大量时间来扫描需要丢弃的数据
- 可以用反范式化、预先计算和缓存来解决,或者限制用户翻页
- 可以用延迟关联,先使用覆盖索引查询返回的主键,再根据主键关联原表查询行。