MySQL 认识索引
背景
在应用的开发过程中,由于初期数据量小,开发人员在写SQL
语句时更重视功能上的实现,但当应用系统正式上线后,随着生产数据量的急剧增长,很多SQL
语句开始逐渐暴露出性能问题,对生产的影响也越来越大,此时这些有问题的SQL
就成为了整个系统性能的瓶颈,这时必须对它们进行优化。
进行SQL
优化,索引将是数据库用来提高性能的最常用工具。所有MySQL
列类型可以被索引,对相关列使用索引是提高SELECT
操作性能的最佳途径。
Bad Case
count(*)
虽然下面的执行计划命中了idx_vflag
普通索引,但遍历该二级索引树时,扫描的行数依然很多,接近全表扫描,效率非常低。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
MySQL [bussinessc]> show index from bussinessc.t_bussiness;
+-------------+------------+-------------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+--------------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| t_bussiness | 0 | PRIMARY | 1 | id | A | 1779703 | NULL | NULL | | BTREE | | |
| t_bussiness | 0 | uk_bussiness_id | 1 | bussiness_id | A | 1799108 | NULL | NULL | | BTREE | | |
| t_bussiness | 1 | idx_tel | 1 | tel | A | 330417 | NULL | NULL | | BTREE | | |
| t_bussiness | 1 | idx_add_time | 1 | add_time | A | 291181 | NULL | NULL | | BTREE | | |
| t_bussiness | 1 | idx_create_time | 1 | create_time | A | 1190831 | NULL | NULL | | BTREE | | |
| t_bussiness | 1 | idx_update_time | 1 | update_time | A | 1061260 | NULL | NULL | | BTREE | | |
| t_bussiness | 1 | idx_vflag | 1 | vflag | A | 1 | NULL | NULL | | BTREE | | |
| t_bussiness | 1 | idx_tag | 1 | tag | A | 2 | NULL | NULL | | BTREE | | |
+------------ +------------+-------------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
8 rows in set (0.002 sec)
MySQL [bussinessc]> explain SELECT count(*) FROM bussinessc.t_bussiness \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: t_bussiness
partitions: NULL
type: index
possible_keys: NULL
key: idx_vflag
key_len: 1
ref: NULL
rows: 1879793
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.002 sec)
因为InnoDB
引擎实现了多版本并发控制(MVCC
),对同一个表,不同事物在同一时刻,看到的数据可能是不一样的。
所以在InnoDB
引擎中,不能像在MyISAM
中直接把对应值存到内存和磁盘文件中,当执行count(*)
的时候,它需要一行一行的进行统计和计数,并将最终的统计结果返回。
count(*)
的改进
在5.7.18
版本之前,InnoDB
处理select count(*)
是通过扫描聚簇索引,来获取总记录数。从5.7.18
版本开始,InnoDB
扫描一个最小的可用的二级索引来获取总记录数,或者由SQL hint
来告诉优化器使用哪个索引。如果二级索引不存在,InnoDB
将会扫描聚簇索引。
原因:InnoDB
是索引组织表,主键索引的叶子节点是数据,而普通索引的叶子节点是主键值,所以普通索引树比主键索引树小很多。对于COUNT(*)
来说,遍历哪颗树都一样,所以mysql
优化器会选择最小的树进行遍历。在保证逻辑正确的前提下,尽量减少扫描的数据量。加载同样的数据量, 由于二级索引叶子节点存放的是主键Id
,所有走二级索引需要的io
次数更少。
定位索引
索引是什么?
索引是对数据库表中一列或多列的值进行排序的一种结构,包含着对数据表里所有记录的引用指针,大多单独存储在磁盘上。使用索引可以提高数据库中特定数据的查询速度,如果表中查询的列有一个索引,Mysql
能快速到达一个位置去搜索数据文件,而不必查看所有数据,避免进行全表扫描。
常见索引的数据结构实现,一般可以分为B+tree
索引、B-tree
索引、Hash
索引、Full-text
索引和空间索引。
索引在哪里?
索引是在存储引擎层中实现的,不是在服务器层,所以每种存储引擎的索引都不一定完全相同,也不是所有存储引擎都支持所有的索引类型。
根据存储引擎,可以定义每个表的最大索引和最大索引长度,每种存储引擎对每个表至少支持16
个索引,总索引长度至少为256
字节。大多数存储引擎有更高的限制。
认识MySQL
体系
MySQL
体系结构,大致分下面几个部分,
客户端连接部分 → MySQL Server部分 → Service&Utilities部分 → SQL处理所需要的模块 → MySQL所支持的各个存储引擎 → 物理存储层
- 客户端连接部分
- 处理客户端的连接请求,
MySQL
支持所有的连接类型
- 处理客户端的连接请求,
MySQL Server
部分- 数据库给客户端创建的连接都在
connection pool
中处理和存储,用一个线程管理一个连接,包括用户认证、安全管理等。
- 数据库给客户端创建的连接都在
Service & Utilities
部分- 管理工具&工具集,用于备份恢复、安全管理、集群管理服务&工具
SQL
处理所需要的模块- 查询优化,会根据解析树生成执行计划,选择合适的索引,之后会按照执行计划去执行
SQL
,和各个引擎交互。 SQL
接口(接收用户的SQL
命令并处理)SQL
解析器(负责将请求的SQL
解析生成一个”解析树”,然后根据一些MySQL
规则进一步检查解析树是否合法)- 查询优化器(对查询语句进行优化,选择合适索引,优化器的作用就是以它认为的最优的执行方案去执行(有时候可能也不是最优),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来)
- 缓存(包括各个引擎的缓存部分,比如
InnoDB
存储有pool
,MyISAM
有KeyBuffer
等,也会缓存一些权限,是一些session
级别的缓存)
- 查询优化,会根据解析树生成执行计划,选择合适的索引,之后会按照执行计划去执行
MySQL
所支持的各个存储引擎- 插件式存储引擎(只要写好跟
MySQL Server
的接口,任何引擎都可以接入到MySQL
里。这也是MySQL
流行的原因之一)
- 插件式存储引擎(只要写好跟
- 物理存储层
- 文件的物理存储,包括二进制日志、数据文件、错误日志、慢查询日志、全日志、
redo
日志、undo
日志等等。
- 文件的物理存储,包括二进制日志、数据文件、错误日志、慢查询日志、全日志、
一条Select
的执行轨迹
权限校验(如果命中缓存)→ 查询缓存 → 分析器 → 优化器 → 权限校验 → 执行器 → 引擎
- 客户端通过
CS
通信协议和MySQL
建立连接, - 查询缓存,这是一个优化查询的地方,如果开启了
Query_cache
,且在缓存中有完全相同的SQL
,则会直接返回结果给客户端,如果没有完全一样的sql
,就会经过解析器进行语法解析,生成解析树, - 预处理器生成新解析树,
- 查询优化器,生成执行计划,
- 查询引擎执行真正的
SQL
,此时会根据SQL
中的表的存储引擎类型、对应的API
接口,与底层存储引擎、缓存或者物理文件进行交互,得到查询结果,由MySQL Server
过滤后,缓存起来,返回给客户端。若开启了Query_cache
,会将结果完全缓存起来,以后便于直接返回结果,当然这里的sql
语句是hash
过的。
什么是存储引擎?
插件式存储引擎,是MySQL
中具体与文件打交道的子系统,根据MySQL AB
公司提供的文件访问层抽象接口定制的一种文件访问机制。作为MySQL
最重要的特定之一,用户可以根据应用的需要选择存储和索引数据的方式、是否使用事务等,可以理解为表类型。
MySQL
默认支持多种存储引擎,适用于不同领域的数据库应用需要,用户可以选择不同的存储引擎提高应用的效率,提供灵活的存储,甚至还可以定制自己的存储引擎。
MySQL 5.6
之前,默认存储引擎是MyISAM
;5.6
之后,改成了InnoDB
。
常见存储引擎的功能特性
MyISAM
不支持事务,不支持外键,优势在于访问速度快,对事务完整性没有要求,表级锁。
InnoDB
提供了具有提交、回滚和崩溃回复能力的事务安全,相比于MyISAM
,InnoDB
写的处理效率差一些,还会占用更多的磁盘空间来保留数据和索引,表级锁/行级锁。
Memory
使用存在于内存中的内容创建表,访问表非常快,由于内存特性,表中的数据容易丢失。
常见存储引擎的存储特性
MyISAM
在磁盘上存储成3个文件,文件名和表名相同,扩展名分别是,
frm
(存储表定义)MYD
(存储数据)MYI
(存储索引)
InnoDB
存储表和索引有两种方式,
- 共享表空间存储(这种方式创建的表的表结构存储在
frm
文件,数据和索引分别存在不同定义的表空间下,可以是多个文件) - 多表空间存储(这种方式创建的表的表结构仍然保存在
frm
文件中,但每个表的数据和索引单独保存在ibd
中) - 即使在多表空间的存储方式下,共享表空间仍然是必须的,
InnoDB
把内部数据词典和在线重做日志放在这个文件中。Memory
则是使用存在于内存中的内容创建表。每个Memory
表只实际对应一个frm
格式的磁盘文件,一旦服务关闭,表中的数据就会丢失。
常见存储引擎支持的索引数据结构
BTREE
索引(B+TREE
索引)MyISAM
、InnoDB
的表默认创建的都是BTREE
索引,B+Tree
是B-tree
的升级结构。
Hash
索引MyISAM
不支持Hash
索引;InnoDB
支持自适应Hash
索引,但InnoDB
中Hash
索引的创建由存储引擎自动优化创建,不能人为干预是否创建Hash
索引;Memory
默认使用Hash
索引,但也支持BTREE
索引。
FULLTEXT
索引MySQL 5.6
之前,只有MyISAM
支持FULLTEXT
全文本索引,本质是对文本的内容进行分词,进行搜索,只限于CHAR、VARCHAR、TEXT
列,对整个列进行,不支持局部前缀索引。它的出现是为了解决WHERE name LIKE “%word%"
这类针对文本的模糊查询效率较低的问题。5.6
之后,InnoDB
也支持FULLTEXT
。
- 空间索引
MyISAM
表支持空间索引,在MySQL5.7
版本以后,InnoDB
还引入了空间索引,对GIS
数据的支持,即可以存储和查询空间数据。
常见存储引擎的适用场景
如果应用是以select
和insert
为主,只有很少的update
、delete
,并且对事务的完整性、并发性要求不是很高,使用MyISAM
很合适。
如果应用对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,数据操作除了select
和insert
外,还有很多update
、delete
,使用InnoDB
比较合适。
在需要快速定位记录时,Memory
可以提供极快的访问。但对表的大小有限制,太大的表无法缓存到内存,通常用在更新不太频繁的小表,快速得到访问结果。
索引的原理
索引分类标准
按数据结构分类可分为:B+tree
索引、B-Tree
索引、Hash
索引、Full-text
索引、空间索引。
按物理存储分类可分为:聚簇索引、二级索引(辅助索引)。
按字段特性分类可分为:主键索引、普通索引、唯一索引、前缀索引。
按字段个数分类可分为:单列索引、联合索引(复合索引、组合索引)。
聚簇索引
聚簇索引的每个叶子节点存储了一行完整的表数据,叶子节点间按id列递增连接,可以方便地进行顺序检索。
也就是说,聚集索引的B+
树中的叶子节点存放的是整张表的行记录数据。一个表最多只能有一个聚集索引,Innodb
通过主键聚集数据,如果没有定义主键, Innodb
会选择非空的唯一索引代替。如果没有这样的索引,Innodb
会隐式的定义一个主键来作为聚集索引。
二级索引
非聚集索引索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。Innodb
里非主键索引又被称为二级索引、辅助索引, 均属于非聚集索引。
回表
当通过辅助索引来查询数据时,InnoDB
存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据,这个过程称为回表。
索引覆盖
如果select
后面查询的字段都可以从这个索引的树中获取,而不用回表,这种情况一般可以说是用到了覆盖索引。
索引下推
ICP
,index condition pushdown
,MySQL 5.6
引入的一个优化,它可以在索引遍历过程中,对索引中包含的字段先做判断,直接在InnoDB
存储引擎层过滤掉 不满足条件的记录,减少回表次数。
- 索引下推发生在组合索引
- 索引下推触发条件必须
sql
会回表,否则下推没有意义
前缀索引
对字符类型字段的前几个字符或对二进制类型字段的前几个bytes
建立的索引,而不是在整个字段上建索引。
前缀索引可以建立在类型为char
、varchar
、binary
、varbinary
的列上,可以大大减少索引占用的存储空间,也能提升索引的查询效率。但在order by
和group by
时无法使用。
前缀索引的长度跟存储引擎相关,对于MyISAM
表,索引的前缀长度可以达到1000
字节长;对于InnoDB
表,索引的前缀长度最长是767
字节。
联合索引
建立在多个列上的索引被称为联合索引,又叫复合索引、组合索引。需要理解联合索引的有序性和最左前缀原理,否则会导致索引失效。
- 减少开销:建一个联合索引(
col1
,col2
,col3
),实际相当于建了(col1
),(col1
,col2
),(col1
,col2
,col3
)三个索引。每多一个索引,都会增加写操作的开销和磁盘空间的开销。对于大量数据的表,使用联合索引会大大的减少开销。 - 覆盖索引:对联合索引(
col1
,col2
,col3
),如果有如下的sql
:select col1,col2,col3 from test where col1=1 and col2=2
。那么MySQL
可以直接通过遍历索引取得数据,而无需回表,这减少了很多的随机io
操作。减少io
操作,特别的随机io
其实是dba
主要的优化策略。所以,在真正的实际应用中,覆盖索引是主要的提升性能的优化手段之一。 - 筛选效率高:索引列越多,通过索引筛选出的数据越少。有1000W条数据的表,有如下
sql
:select from table where col1=1 and col2=2 and col3=3
,假设假设每个条件可以筛选出10%
的数据,如果只有单值索引,那么通过该索引能筛选出1000W*10%=100w
条数据,然后再回表从100w
条数据中找到符合col2=2 and col3=3
的数据,然后再排序,再分页;如果是联合索引,通过索引筛选出1000w*10%*10%*10%=1w
。 - 有序:索引本身是有顺序的,当要对索引字段排序时,那么查询到的数据天然就是有顺序的,减少了排序的开销。
Hash索引、BTREE索引、B+TREE索引
Hash索引
Hash
索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree
索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO
访问,所以Hash
索引的查询效率要远高于B-Tree
索引。
虽然Hash
索引效率高,但是Hash
索引本身由于其特殊性也带来了很多限制,主要有以下这些,
Hash
索引仅仅能满足=
,IN
和<=>
(表示NULL
安全的等价) 查询,不能使用范围查询- 由于
Hash
索引比较的是进行Hash
运算之后的Hash
值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的Hash
算法处理之后的Hash
值的大小关系,并不能保证和Hash
运算前完全一样。对于BTREE
索引,使用>
、<
、>=
、<=
、BETWEEN
、!=
或者<>
,或者like
(pattern
不以通配符开始),都可以使用相关列上的索引。
- 由于
- 优化器不能使用
Hash
索引来加速order by
操作- 由于
Hash
索引中存放的是经过Hash
计算之后的Hash
值,而且Hash
值的大小关系并不一定和Hash
运算前的键值完全一样,所以数据库无法利用 索引的数据来避免任何排序运算。
- 由于
Hash
索引不能利用部分索引键查询,只能使用整个关键字搜索一行- 对于组合索引,
Hash
索引在计算Hash
值的时候是组合索引键合并后再一起计算Hash
值,而不是单独计算Hash
值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash
索引也无法被利用。
- 对于组合索引,
Hash
索引依然需要回表扫描Hash
索引只包含哈希值和行指针,而不存储字段值。Hash
索引是将索引键通过Hash
运算之后,将Hash
运算结果的Hash
值和所对应的行指针信息存放于一个Hash
表中,由于不同索引键可能存在相同Hash
值,所以即使取满足某个Hash
键值的数据的记录条数,也无法从Hash
索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
Hash
索引遇到大量Hash
值相等的情况后性能并不一定就会比B-Tree
索引高- 选择性比较低的索引键,如果创建
Hash
索引,那么将会存在大量记录指针信息存于同一个Hash
值相关联。这样要定位某一条记录时就会非常麻 烦,会浪费多次表数据的访问,而造成整体性能低下。 - 如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希仲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。
- 选择性比较低的索引键,如果创建
由于范围查询是MySQL
数据库查询中常见的场景,Hash
表不适合做范围查询,它更适合做等值查询。另外Hash
表还存在Hash
函数选择和Hash
值冲突等问题。因此,B+tree
索引要比Hash
表索引有更广的适用场景。
BTREE索引
B树
是一个平衡多路查找树,B
为Blanced
,是为磁盘等外存储设备设计的一种平衡查找树,形状偏“瘦高”。
- 关键字集合分布在整棵树中
- 任何一个关键字出现且只出现在一个结点中
- 搜索有可能在非叶子节点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
InnoDB
存储引擎中有页(Page
)的概念,页是其磁盘管理的最小单位。InnoDB
存储引擎中默认每个页的大小为16KB
,可通过参数innodb_page_size
将页的大小设置为4K
、8K
、16K
。
1
2
3
4
5
6
7
8
MySQL16KB
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.01 sec)
而系统一个磁盘块的存储空间往往没有这么大,因此InnoDB
每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小16KB
。
InnoDB
在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘I/O
次数,提高查询效率。
页分裂
B-Tree
为了维护索引有序性,在插入新值的时候需要做必要的维护,当插入入一个新记录时,如果新插入的ID
值在数据中间,就需要逻辑上挪动后面的数据,空出位置。
而更糟的情况是,所在的数据页已经满了,根据B-Tree
的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能就会受影响。不是按顺序插入,而是随机插入,产生很多移动数据,也会导致页分裂。
页分裂会产生表空间碎片
1 2 影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约`50%`。 可能导致查询扫描的`IO`成本提升,效率查询降低。当然有分裂就有合并。
当我们删除某一行记录时,其实MySQL
只是把此行记录标记为了“可复用”,但磁盘大小是不会变的,所以通过delete
表中记录是达不到回收表空间的。这些被标记为“可复用”而没有使用的空间看起来就像是“空洞”,当相邻两个页由于删除了数据,利用率很低之后,可将数据页做合并。即通过optimize table
来重组文件。
产生表空间碎片的其他常见的原因
1 2 记录被`Delete`,且原空间无法复用; 记录被`Update`(通常出现在变长字段中),原空间无法复用。
自增主键 VS 非自增主键
自增主键的插入数据模式,正符合了递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,由于每次插入主键的值近似于随机,则会产生很多移动数据,页分裂,进而造成了大量的碎片,大大影响性能。
为什么不用其他查询性能更好的树?
传统来搜索的平衡叉树有很多,如AVL
树,红树等。这些树在般情况下查询性能常好,但当数据常的时候它们就能为了。原因当数据量常时,内存不够,无法将全部数据读入内存,部分数据只能存放在磁盘上,只有需要的数据才加载到内存中。般内存访问的时间约为50ns
,磁盘在10ms
左右。速度相差了近5
个数量级,磁盘读取时间远远超过了数据在内存中较的时间。这说明程序部分时间会阻塞在磁盘IO
上。而B树数据存储在各个节点上,那么每次读入内存的信息就比较有效,一次查询可能产生很多次IO
,那么我们如何减少磁盘IO
次数,于是有B+
树。
B+TREE树
B+
树是在B
树基础上的一种优化,使其更适合实现存储索引结构,InnoDB
存储引擎就是用B+Tree
实现其索引结构,形状偏“矮胖”。
B+
树相对于B
树有几点不同:
- 非叶子节点只存储键值信息。
- 所有叶子节点之间都有一个链指针。
- 数据记录都存放在叶子节点中。
总结一下这种结构的优点:
B+
树的层级更少:相较于B
树B+
每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快B+
树查询速度更稳定:B+
所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B
树更稳定B+
树支持范围查询:叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针B+
树天然具备排序功能:B+
树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B
树高B+
树全节点遍历更快:B+
树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B
树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
InnoDB
存储引擎中页的大小为16KB
,一般表的主键类型为INT
(占用4
个字节)或BIGINT
(占用8
个字节),指针类型也一般为4
或8
个字节,也就是说一个页(B+Tree
中的一个节点)中大概存储16KB/(8B+8B)=1K
个键值(因为是估值,为方便计算,这里的K
取值为〖10〗^3
)。也就是说一个深度为3
的B+Tree
索引可以维护10^3 * 10^3 * 10^3 = 10
亿条记录。
实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree
的高度一般都在2~4
层。MySQL
的InnoDB
存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3
次磁盘I/O
操作。
最后
只有深刻理解了这些索引的基础概念,才能在后边的索引优化工作中做到有的放矢。