MySQL6:索引使用原则,联合索引,联合主键/复合主键,覆盖索引、什么是回表?索引条件下推,索引的创建与使用,索引的创建与使用,索引失效
- 索引使用原则
- 列的离散(sdn)度
- 联合索引
- 创建联合索引
- 联合索引最左匹配
- 建立联合索引之后,联合索引的最左字段还要再建普通索引吗?
- 联合索引使用场景
- 什么时候能用到联合索引
- 联合主键/复合主键
- 覆盖索引
- 什么是回表?
- 什么是覆盖索引?
- 如何判断是覆盖索引
- 索引条件下推(Index Condition Pushdown)
- 判断使用索引条件下推
- 索引的创建与使用
- 索引的创建
- 索引失效
- MySQL合集
索引使用原则
我们容易有以一个误区,就是在经常使用的查询条件上都建立索引,索引越多越好,那到底是不是这样呢?
并不是越多越好,索引好占用磁盘空间的,你的表200M,你的索引可能就是4G,如果搞很多索引,磁盘空间浪费会很大,因此只在必要的字段上建立索引。那么什么样的字段适合建立索引呢,一般选择列的离散度高的列建立索引。
列的离散(sdn)度
列的离散度的公式:
## 列的全部不同值和所有数据行的比例
SELECT COUNT(DISTINCT column_name) / COUNT(*) FROM table_name;
数据行数相同的情况下,分子越大,列的离散度就越高。简单来说,如果列的重复值越多,离散度就越低,重复值越少,离散度就越高。
如果在B+Tree里面的重复值太多(比如性别建立索引),MySQL的优化器发现走索引跟使用全表扫描差不了多少的时候,就算建了索引,也不一定会走索引。
联合索引
前面我们说的都是针对单列创建的索引,但有的时候我们的多条件査询的时候,也会建立联合索引。单列索引可以看成是特殊的联合索引。
创建联合索引
CREATE TABLE `stu_innodb` (
`id` INT(11) NOT NULL,
`name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`sex` INT(11) NULL DEFAULT NULL,
`age` INT(11) NULL DEFAULT NULL,
`card_id` VARCHAR(10) NOT NULL COLLATE 'utf8mb4_unicode_ci'
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
;
比如我们在stu_innodb
表上面,给card_id和name
建立了一个联合索引。
## 删除索引
ALTER TABLE stu_innodb DROP INDEX idx_cardid_name;
## 创建联合索引
ALTER TABLE stu_innodb ADD INDEX idx_cardid_name (card_id,name);
## 查看索引
show index from stu_innodb;
联合索引最左匹配
联合索引在B+Tree中是复合的数据结构,它是按照索引的顺序从左到右的顺序来建立搜索树的,比如联合索引idx_cardid_name (card_id,name)
就是card_id
在左边,name
在右边。
如图,card_id
是有序的,name
是无序的。当card_id
相等的时候, name
才是有序的。
这个时候我们使用where card_id='stu006' and name='Blue'
去査询数据的时候,B+Tree会优先比较card_id
来确定下一步应该搜索的方向,往左还是往右。如果card_id
相同的时候再比较name
,但是如果查询条件没有card_id
,就不知道第一步应该查哪个节点,因为建立搜索树的时候card_id
是第一个比较因子,所以用不到索引。
建立联合索引之后,联合索引的最左字段还要再建普通索引吗?
CREATE INDEX idx_cardid on user_innodb(card_id);
CREATE INDEX idx_cardid_name on user_innodb(card_id,name);
当我们创建一个联合索引的时候,按照最左匹配原则,用左边的字段card_id
去査询的时候,也能用到索引,所以第一个索引完全没必要。因为联合索引(card_id,name)
相当于建立了两个索引(card_id)
,(card_id,name)
。
如果我们创建三个字段的索引index(a,b,c),相当于创建三个索引:
- index(a)
- index(a,b)
- index(a,b,c)
同理根据最左原则:
where a=?
:可以where a=? and b=?
:可以where a=? and b=? and c=?
:可以where a=? and c=?
:可以用到a的索引,不可以用到c的,因为中断(跳过)bwhere b=?
:不可以where b=? and c=?
:不可以
在不含最左的a的where条件中,是不能使用到联合索引的,且中断(跳过)不能使用到联合索引的。
联合索引使用场景
根据联合索引的特性,什么时候会用到联合索引呢?
那肯定是查询条件覆盖了联合索引的列。
比如查询高考成绩,必须输入考生姓名和学生的身份证号,那么这种场景,就可以对二者建立联合索引。
什么时候能用到联合索引
- 顺序使用两个字段,用到联合索引:
-
## 顺序使用两个字段,用到联合索引: explain select * from stu_innodb where card_id='stu006' and name='Blue';
-
- 乱序使两个字段,用到联合索引(查询优化器的功劳):
-
## 乱序使两个字段,用到联合索引: explain select * from stu_innodb where name='Blue' and card_id='stu006';
-
- 使用左边的索引字段,用到联合索引:
-
## 使用左边的索引字段,用到联合索引: explain select * from stu_innodb where card_id='stu006';
-
- 不使用左边的索引字段,无法使用索引,全表扫描:
-
## 不使用左边的索引字段,无法使用索引,全表扫描: explain select * from stu_innodb where name='Blue';
所以,我们在建立联合索引的时候,一定要把最常用的列放在最左边,查询条件必须由第一个索引字段开始,不能中断(跳过)。
-
联合主键/复合主键
主键有唯一的约束。当多个字段组合成唯一标识的时候,创建复合主键。
比如一个学生表中,学生卡号、姓名、学院是一条记录的唯一标识,那么就可以建立复合主键,通过定义复合主键,减少了表的数量,不是一个college(学院)一张表。
CREATE TABLE `stu_innodb` (
`name` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`sex` INT(11) NULL DEFAULT NULL,
`age` INT(11) NULL DEFAULT NULL,
`card_id` VARCHAR(10) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`college` VARCHAR(10) NOT NULL COLLATE 'utf8mb4_unicode_ci',
PRIMARY KEY (`card_id`, `name`, `college`) USING BTREE
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
;
复合主键的特点:
- 因为复合主键需要存储多个字段的值,相对于单列主键来说要消耗更多的存储空间
- 联合主键包含多个列的时候,不允许所有的字段都相同。因为判断是否重复更复杂(代码中重写hashCode和equals也是)
- 举例:一张表有a、b、c三个字段,不允许a、b、c三个字段的值都相同,可以有部分的值都相同,例如
- record1:1 2 3
- record1:2 3 4
- record1:1 1 3
- record1:1 1 3 不允许
- 举例:一张表有a、b、c三个字段,不允许a、b、c三个字段的值都相同,可以有部分的值都相同,例如
- 表结构修改或者数据迁移会更加困难
- 如果目的是限制唯一性,可以直接拼接两个字段的内容,比如CAIJING1001,CHUANMEI1001,或者用唯一索引UNIQUE KEY也可以实现
- 如果目的不是为了限制唯一性,或者有其它检查唯一性的方法,用自增ID之类的无业务意义的字段作为主键更合适。自增ID在插入数据时,引起的页分裂和合并更少
MBG(MyBatis Generator,MBG 在配置中为每个表简单的CRUD操作生成 SQL)对复合主键,会生成两个实体类
覆盖索引
什么是回表?
回表:非主键索引,我们先通过索引找到主键索引的键值,再通过主键值查出索引里面没有的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。
当我们用name索引查询一条记录,它会在二级索引的叶子节点找到name=Susan
,拿到主键值,也就是id = 3
,然后再到主键索引的叶子节点拿到数据。
什么是覆盖索引?
覆盖索引:在二级索引里面,不管是单列索引还是联合索引,如果select的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索索引,这样就避免了回表。。比如上图select name from tbl_xxx where name=?
,因为二级索引的叶子节点上就已经存储了name
的数据,因此不需要回表,性能就会高点。
如何判断是覆盖索引
覆盖索引并不是索引的一种类型,而是使用索引产生的一种情况。Explain中的Extra列里面值为'Using index'
代表属于覆盖索引的情况。
在联合索引idx_cardid_name (card_id,name)
中
这三个查询语句属于覆盖索引:
- ①使用联合索引②select的所有列已经包含在了用到的索引中:
-
属于覆盖索引explain select name,card_id from stu_innodb where card_id='stu006' and name='Blue';
-
- ①使用联合索引②select的所有列已经包含在了用到的索引中:
-
用到了索引,属于覆盖索引,因为联合索引的创建,使得当前的B+Tree存储了explain select name from stu_innodb where card_id='stu006';
card_id,name
的数据,因此不需要回表
-
- ①不使用联合索引②select的所有列已经包含在了用到的索引中:
-
还是用到了索引,并且属于覆盖索引,查询优化器优化了,优化器觉得用索引更快,所以还是用到了索引explain select name from stu_innodb where name='Blue';
-
覆盖索引减少了I/O次数,减少了数据的访问量,可以大大地提升查询效率。
索引条件下推(Index Condition Pushdown)
MySQL架构是分层的,可以分为连接层、服务层、存储引擎层。
不同的索引是在存储引擎层中实现的。如果where条件包含索引,那么是可以在存储引擎层完成数据过滤的。如果用不到索引过滤数据,就需要去Server层。那么如何优化呢?
如果返回了一大堆数据都不是就客户端需要的,那么能不能把这个过滤的动作先在存储引擎层做完,即使查询条件用不到索引,也先在存储引擎中过滤一遍,这个动作,就叫做索引条件下推。
索引条件下推(Index Condition Pushdown),5.6以后完善的功能,不需要我们开启,是InnoDB自动开启,自动优化的。只适用于二级索引。ICP的目标是减少访问表的完整行的读数量从而减少I/O操作。这里说的下推,其实是意思是把过滤的动作在存储引擎做完,而不需要到Server层过滤,把原来存储引擎无法使用的过滤条件,还是先让他在存储引擎中先过滤。
我们建个表来深入理解一下,idx_college_cardid
作为二级联合索引
-- 导出 表 test.stu_innodb 结构
-- 导出 表 test.stu_innodb 结构
CREATE TABLE IF NOT EXISTS `stu_innodb` (
`id` int(11) NOT NULL,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`sex` int(11) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`card_id` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL,
`college` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_college_cardid` (`college`,`card_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 正在导出表 test.stu_innodb 的数据:~12 rows (大约)
INSERT INTO `stu_innodb` (`id`, `name`, `sex`, `age`, `card_id`, `college`) VALUES
(2, 'Tom', 1, 23, 'stu001', '财经'),
(4, 'Bill', 1, 30, 'stu002', '财经'),
(5, 'Susan', 0, 27, 'stu003', '财经'),
(6, 'Marry', 0, 28, 'stu004', '财经'),
(7, 'Sky', 1, 19, 'stu005', '财经'),
(8, 'Blue', 1, 22, 'stu006', '财经'),
(12, 'Tom', 1, 23, 'stu001', '金融'),
(14, 'Bill', 1, 30, 'stu002', '金融'),
(15, 'Susan', 0, 27, 'stu003', '金融'),
(16, 'Marry', 0, 28, 'stu004', '金融'),
(17, 'Sky', 1, 19, 'stu005', '金融'),
(18, 'Blue', 1, 22, 'stu006', '金融');
当我们执行SQL语句SELECT * FROM stu_innodb where college='金融' AND card_id LIKE '%stu002';
,正常情况来说(也就是不适用索引条件下推的情况),因为字符是从左往右排序的,当你把%加在前面的时候,是不能基于索引去比较的,所以只有college
这个字段能够用于索引比较和过滤。
按有没有使用索引条件下推,查询过程分为两种情况:
-
没有索引下推,查询过程是这样的:
- ① 根据联合索引查出所有
college='金融'
的二级索引数据,在其叶子节点上得到对应的主键索引值(12,14,15,16,17,18) - ② 回表,到主键索引上查询全部复合条件的数据,6条数据
- ③ 把这6条数据返回给Server层,在Server层对这6条数据,进行符合
card_id LIKE '%stu002'
条件的过滤
注意:索引的比较是在存储引擎层进行的,数据记录的比较,是在Server层进行的。而当
card_id LIKE '%stu002'
条件不能用于索引过滤时,Server层不会把card_id LIKE '%stu002'
条件传递给存储引擎,所以读取很多条没有必要的记录(Server层并不需要)。
这时候,如果满足college='金融'
的记录有10万条,就会有99999条没有必要读取的记录,而且都还要返回给Server层。所以,根据college
字段过滤的动作,能不能在存储引擎层完成呢?就是下面的查询方法。 - ① 根据联合索引查出所有
-
使用索引下推,查询过程是这样的:
- ① 根据联合索引查出所有
college='金融'
的二级索引数据,在其叶子节点上得到对应的主键索引值(12,14,15,16,17,18) - ② 然后从二级索引中筛选出
card_id LIKE '%stu002'
条件的索引,这下就只有一个了,(14) - ③ 回表,到逐渐索引上查询全部符合条件的数据(1条数据),返回Server层
先用
college='金融'
条件进行二级索引范围扫描,读取数据表记录;然后进行比较,检查是否符合card_id LIKE '%stu002'
的条件。此时6条中只有1条符合条件。 - ① 根据联合索引查出所有
判断使用索引条件下推
Extra是Using index condition
就代表使用索引条件下推,全称其实就是Using index condition push down
。
查询是否开启索引条件下推
show variables LIKE 'optimizer_switch';
可以看到,index_condition_pushdown=on
,InnoDB中,默认开启索引条件下推
index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
我们先把索引条件下推关闭
set optimizer_switch='index_condition_pushdown=off';
关闭之后,查看SQL执行计划,会发现Extra是Using where
explain SELECT * FROM stu_innodb where college='金融' AND card_id LIKE '%stu002';
Extra返回Using where
,表示存储引擎返回的数据不全是Server层需要的,需要在Server层再做过滤
启用索引条件下推之后,再次查看SQL执行计划
set optimizer_switch='index_condition_pushdown=on';
explain SELECT * FROM stu_innodb where college='金融' AND card_id LIKE '%stu002';
会发现Extra变成了Using index condition
,全称其实就是Using index condition push down
。
索引的创建与使用
因为索引对于改善查询性能的作用是巨大的,所以我们的目标是尽量使用索弓I。
索引的创建
-
在用于where判断order排序和join的(on)、group by的字段上创建索引。
-
索引的个数不要过多。——浪费空间,更新变慢。
-
过长的字段,但是匹配只需要前面的内容(具体是几个字符在建立索引时指定),建立前缀索引。
alter table 表名 add index 索引名(列名(长度));
注意:在前缀索引中B+Tree里保存的就不是完整的该字段的值,必须要回表才能拿到需要的数据。所以,用了前缀索引,就用不了覆盖索引了。
-
区分度低的字段,例如性别,不要建索引。——离散度太低,导致扫描行数过多。
-
频繁更新的值,不要作为主键或者索引。——造成页分裂和合并,引起B+Tree的结构调整,会浪费很大的性能
-
随机无序的值,不建议作为索引(例如身份证、UUID)。——无序,分裂
-
联合索引把散列性高(区分度高)的值放在前面。
-
创建复合索引,而不是修改单列索引。
索引失效
-- 导出 表 test.stu_innodb 结构
CREATE TABLE IF NOT EXISTS `stu_innodb` (
`id` int(11) NOT NULL,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`sex` int(11) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`card_id` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL,
`college` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`card_id`,`name`,`college`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 正在导出表 test.stu_innodb 的数据:~6 rows (大约)
INSERT INTO `stu_innodb` (`id`, `name`, `sex`, `age`, `card_id`, `college`) VALUES
(2, 'Tom', 1, 23, 'stu001', ''),
(4, 'Bill', 1, 30, 'stu002', ''),
(5, 'Susan', 0, 27, 'stu003', ''),
(6, 'Marry', 0, 28, 'stu004', ''),
(7, 'Sky', 1, 19, 'stu005', ''),
(8, 'Blue', 1, 22, 'stu006', '');
以这个表为例,建立了复合主键
-
索引列上使用函数(
replace\SUBSTR\CONCAT\sum count avg
)、表达式计算('+'、'-'、'×'、'÷'
)。因为会得到一个不确定的结果,所以无法使用索引,宁愿计算出它的结果放在表达式的右边,都比函数计算要好explain SELECT * FROM stu_innodb where id+1 = 4;
-
字符串不加引号,出现隐式转换
explain SELECT * FROM stu_innodb where card_id = 11;
-
索引的最左前缀原则,like条件中前面带%。where条件中
LIKE '%001%'、LIKE '001%'、'%001'
都用不到索引,为什么?explain SELECT * FROM stu_innodb where card_id LIKE '%001%'; explain SELECT * FROM stu_innodb where card_id LIKE '001%'; explain SELECT * FROM stu_innodb where card_id LIKE '%001';
过滤的开销太大。这个时候可以用全文索引。 -
负向查询:
NOT LIKE
不能:explain SELECT * FROM stu_innodb where card_id NOT LIKE '001%';
<>
和NOT IN
在某些情况下可以:explain SELECT * FROM stu_innodb where card_id <> 'stu001';
explain SELECT * FROM stu_innodb where card_id NOT LIKE '001%';
其实,InnoDB到底用不用索引,最终都是优化器说了算。
优化器是基于什么的优化器?
基于cost开销(Cost Base Optimizer),也叫基于成本的优化器,成本包括两方面,I/O,CPU。它不是基于规则(Rule-Based Optimizer)的优化器,也不是基于语义。怎么样开销小就怎么来。
比如你回家的路线有三条,那么两个优化器会怎么做:
- 基于规则(Rule-Based Optimizer)的优化器:每次固定选择A路线
- 基于成本(Cost Base Optimizer)的优化器:会看当前哪条路到家最快(基于成本考虑),然后选择一条最合适的路线
也就是说,使用索引有基本原则,但是没有具体细则,没有什么情况一定用索引,什么情况一定不用索引的规则。
MySQL合集
MySQL1:MySQL发展史,MySQL流行分支及其对应存储引擎
MySQL2:MySQL中一条查询SQL是如何执行的?
MySQL3:MySQL中一条更新SQL是如何执行的?
MySQL4:索引是什么;索引类型;索引存储模型发展:1.二分查找,2.二叉查找树,3.平衡二叉树,4.多路平衡查找树,5. B+树,6.索引为什么不用红黑树?7.InnoDB的hash索引指什么?
MySQL5:MySQL数据存储文件;MylSAM中索引和数据是两个独立的文件,它是如何通过索引找到数据呢?聚集索引/聚簇索引,InnoDB中二级索引为什么不存地址而是存键值?row ID如何理解?
MySQL6:索引使用原则,联合索引,联合主键/复合主键,覆盖索引、什么是回表?索引条件下推,索引的创建与使用,索引的创建与使用,索引失效