索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的。MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。
打个比方,如果合理的设计且使用索引的MySQL是一辆兰博基尼的话,那么没有设计和使用索引的MySQL就是一个人力三轮车。
我们知道一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,所以查询语句的优化显然是重中之重。
实际上,索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录。过多的使用索引将会造成滥用。因此索引也会有它的缺点,虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行 INSERT
、UPDATE
和 DELETE
。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。建立索引会占用磁盘空间的索引文件。
索引原理
除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的目录等。它们的原理都是一样的,通过不断的缩小想要获得数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是我们总是通过同一种查找方式来锁定数据。
数据库也是一样,但显然要复杂许多,因为不仅面临着等值查询,还有范围查询(>
、<
、BETWEEN
、IN
)、模糊查询(LIKE
)、并集查询(OR
)等等。数据库应该选择怎么样的方式来应对所有的问题呢?我们回想字典的例子,能不能把数据分成段,然后分段查询呢?最简单的如果1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段……这样查第250条数据,只要找第三段就可以了,一下子去除了90%的无效数据。但如果是1千万的记录呢,分成几段比较好?稍有算法基础的同学会想到搜索树,其平均复杂度是lgN,具有不错的查询性能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的,数据库实现比较复杂,数据保存在磁盘上,而为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。
MySQL的索引类型
下面介绍几种常见的MySQL索引类型。
索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。
主键索引(PRIMARY)
它是一种特殊的唯一索引,不允许有空值。一般是在建表的时候同时创建主键索引。当然也可以用 ALTER 命令。
记住,一个表只能有一个主键。
MySQL中的主键在不同的引擎下,它们的主键索引形态不一样,InnoDB下主键是聚集索引,即索引的叶子节点的主键值与数据本身在一起存储。
创建表时指定:
CREATE TABLE `example_index` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
修改表时指定,如 exam_index 不存在主键时:
ALTER TABLE `example_index` ADD PRIMARY KEY (`id`);
唯一索引(UNIQUE)
唯一索引列的值必须唯一,但允许有空值(NULL),变相地可理解为NULL值不走索引,但是要注意的是空字符串是走索引的。如果是组合索引,则列值的组合必须唯一。可以在创建表的时候指定,也可以通过修改表结构指定。
创建表时指定:
CREATE TABLE `example_index` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) DEFAULT NULL,
`id_card` char(18) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_card` (`id_card`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
修改表时指定:
ALTER TABLE `example_index` ADD UNIQUE INDEX `id_card_idx` (`id_card`);
普通索引(INDEX)
这是最基本的索引,它没有任何限制。可以在创建表的时候指定,也可以修改表结构时指定。
创建表时指定:
CREATE TABLE `example_index` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) DEFAULT NULL,
`id_card` char(18) DEFAULT NULL,
`type` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `id_card_idx` (`id_card`),
KEY `type_idx` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
修改表时指定:
ALTER TABLE `example_index` ADD INDEX `type_idx` (`type`);
组合索引(INDEX)
组合索引,即一个索引包含多个列。可以在创建表的时候指定,也可以修改表结构时指定。
创建时指定:
CREATE TABLE `example_index` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) DEFAULT NULL,
`id_card` char(18) DEFAULT NULL,
`type` tinyint(4) NOT NULL DEFAULT '0',
`dt` int(11) NOT NULL DEFAULT '0',
`status` tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `id_card_idx` (`id_card`),
KEY `type_idx` (`type`),
KEY `dt_status_idx` (`dt`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
修改时指定:
ALTER TABLE `example_index` ADD INDEX `dt_status_idx` (`dt`, `status`);
全文索引(FULLTEXT)
全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用分词技术等多种算法智能分析出文本文字中关键字词的频率及重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。可以在创建表的时候指定,也可以修改表结构时指定。
创建时指定:
CREATE TABLE `example_index` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(16) DEFAULT NULL,
`id_card` char(18) DEFAULT NULL,
`type` tinyint(4) NOT NULL DEFAULT '0',
`dt` int(11) NOT NULL DEFAULT '0',
`status` tinyint(4) NOT NULL DEFAULT '0',
`desc` varchar(256) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_card_idx` (`id_card`),
KEY `type_idx` (`type`),
KEY `dt_status_idx` (`dt`,`status`),
FULLTEXT KEY `desc_idx` (`desc`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
全文索引原本只在 MyISAM 引擎中支持,在 5.6.4 版本之后 InnoDB 引擎也对 char、varchar 等字段类型支持全文索引。
所谓全文索引,是一种通过建立倒排索引,快速匹配文档的方式。详情将在后续详述。
修改表时指定:
ALTER TABLE `example_index` ADD FULLTEXT INDEX `desc_idx` (`desc`);
删除索引
可利用 ALTER TABLE
或 DROP INDEX
语句来删除索引。类似于 CREATE INDEX
语句,DROP INDEX
可以在 ALTER TABLE
内部作为一条语句处理,语法如下。
DROP INDEX index_name ON talbe_name;
ALTER TABLE table_name DROP INDEX index_name;
ALTER TABLE table_name DROP PRIMARY KEY;
其中,前2条语句是等价的,删除掉 table_name 中的索引 index_name。
第3条语句只在删除 PRIMARY KEY 索引时使用,因为一个表只可能有一个 PRIMARY KEY 索引,因此不需要指定索引名。如果没有创建 PRIMARY KEY 索引,但表具有一个或多个 UNIQUE 索引时,则 MySQL 将删除第一个 UNIQUE 索引。
显示索引信息
你可以使用 SHOW INDEX 命令来列出表中的相关的索引信息。可以通过添加 \G 来格式化输出信息。
SHOW INDEX FROM table_name; \G
磁盘IO与预读
这里先简单介绍一下磁盘IO和预读,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。
索引的数据结构
任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景。对于 MySQL 这种存储介质在磁盘的数据库其实很简单,那就是每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。
B+tree 介绍
如上图,是一颗 B+tree。浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针 P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。
真实的数据存在于叶子节点,即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。
B+tree 的查找过程
如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
B+tree 的性质
1、索引字段要尽量的小(占用空间小);
通过上面 B+tree 的查找过程,或者通过真实的数据存在于叶子节点这个事实可知,IO次数取决于 B+tree 的高度h。
假设当前数据表的数据量为N,每个磁盘块的数据项的数量是m,则树高 h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小。
而 m=磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的;如果数据项占的空间越小,数据项的数量m越多,树的高度h越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。
2、索引的最左匹配特性;
当 B+tree 的数据项是复合的数据结构,比如 (name,age,sex) 的时候,B+tree 是按照从左到右的顺序来建立搜索树的,比如当 (张三,20,F) 这样的数据来检索的时候,B+tree 会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当 (20,F) 这样的没有name的数据来的时候,B+tree 就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当 (张三,F) 这样的数据来检索时,B+tree 可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了,这个是非常重要的性质,即索引的最左匹配特性。
索引查询优化
优化语句很多,需要注意的也很多,针对平时的情况总结一下几点:
有索引但未被用到的情况(不建议)
- 尽量避免
LIKE
的参数以通配符开头,否则数据库引擎会放弃使用索引而进行全表扫描; WHERE
条件不符合最左前缀原则时;- 尽量避免使用
!=
或<>
操作,否则数据库引擎会放弃使用索引而进行全表扫描。使用>
或<
会比较高效; - 索引列参与计算,应尽量避免在
WHERE
子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描; - 应尽量避免对字段进行
NULL
值判断;应尽量避免在WHERE
子句中对字段进行NULL
值判断,否则将导致引擎放弃使用索引而进行全表扫描,因为NULL
不走索引; - 应尽量避免使用
OR
来连接条件,可以考虑通过UNION ALL
来代替;
避免 SELECT *
在解析的过程中,会将 *
依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间。
所以,应该养成一个需要什么就取什么的好习惯。
ORDER BY 语句优化
任何在 ORDER BY
语句的非索引项或者有计算表达式都将降低查询速度。
解决方法思路:
- 重写
ORDER BY
语句以使用索引; - 为所使用的列建立另外一个索引;
- 绝对避免在
ORDER BY
子句中使用表达式。
GROUP BY 语句优化
提高 GROUP BY
语句的效率, 可以通过将不需要的记录在 GROUP BY
之前过滤掉。
用 EXISTS 代替 IN
很多时候用 EXISTS
代替 IN
是一个好的选择。
使用 VARCHAR 代替 CHAR
尽可能的使用 VARCHAR
代替 CHAR
,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
能用 DISTINCT 的就不用 GROUP BY
能用 UNION ALL 就不要用 UNION
UNION ALL
不执行 SELECT DISTINCT
函数,这样就会减少很多不必要的资源。
在 JOIN 表的时候使用相当类型的例,并将其索引
如果应用程序有很多 JOIN
查询,你应该确认两个表中 JOIN
的字段是被建过索引的。这样,MySQL 内部会启动为你优化 JOIN
的 SQL 语句的机制。
而且,这些被用来 JOIN
的字段,应该是相同的类型的。例如:如果你要把 DECIMAL 字段和一个 INT 字段 JOIN
在一起,MySQL就无法使用它们的索引。对于那些 STRING 类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)