文章目录
- 前置知识
- 表准备
- 一. 不在索引列上使用任何操作
- 二. 联合索引字段列全值匹配
- 三. 最佳左前缀法则
- 四. 范围条件放最后
- 五. 覆盖索引使用
- 六. 不等于导致索引失效
- 七. is null/not null 影响
- 八. like 查询的使用
- 九. 字符类型加引号
- 十. OR关键字前后索引问题
- 十一. 利用索引来做排序和分组
- 十二. 升降序使用
- 十三. 按主键顺序插入
- 十四. 优化count查询
- 十五. 优化limit分页
- 十六. MySQL Null 说明
前置知识
MySQL 高性能索引创建策略
表准备
-- 表1
drop table a;
create table a
(
id int primary key auto_increment,
user_name varchar(10),
sex tinyint(2),
age int(10),
birth datetime,
city varchar(10),
create_time bigint(20)
);
一. 不在索引列上使用任何操作
查询列不是独立的,MySQL不会走索引的。
独立的列:索引列不能时表达式的一部分,也不能是函数的参数。
如,对主键索引添加表达式操作:
-- MySQL无法自动解析这个表达式。属于用户行为、
-- 始终要将索引列单独放在比较符号一侧
explain select * from a where id + 1 = 2;
如,对索引进行函数操作:
-- 添加索引
drop index idx_index on a;
create index idx_index on a (city);
-- 查询
explain select birth from a where UPPER(city) = '1'
二. 联合索引字段列全值匹配
全值匹配:在建立联合索引前提下,搜索条件中的列和索引列一致。
-- 索引创建
drop index idx_index on a;
create index idx_index on a (city,user_name, age);
-- 查询
explain select * from a where city = '辽宁' and user_name = '张三' and age = 20;
建立三个索引列被查询语句引用。
延伸出一个疑问,几个搜索条件顺序对结果是否有影响?
-- 按照乱序条件全值匹配的搜索条件
explain select * from a where user_name = '张三' and age = 20 and city = '辽宁';
通过执行计划可看出,并没有影响,因为MySQL通过查询优化器分析所搜条件并且按照索引中列的顺序决定先试用哪个作为搜索条件,后使用哪个搜索条件
三. 最佳左前缀法则
当搜索条件不满足联合索引全字段时,需要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。
原理是B+Tree中,联合索引中最左字段作为B+Tree范围的排序,所以搜索条件中必须出现左边的列才可以使用这个B+Tree索引。
比如,创建了index(a,b,c)一个联合索引,B+Tree的数据页和记录先按照a字段值进行排序,在a字段值相同情况下,才会对b字段值进行排序,也就是说a字段值不同的情况下,对应的b字段值是有可能无序的,而如果跳过a字段搜索条件,直接把b字段当作搜索条件是不现实的。
但如果你只想对b进行查询,那就重新再建立个索引即可。
而且需要注意的一点,使用联合索引中的列,搜索条件的各个列是联合索引从最左边连续的列。
以上的文案如果晦涩难懂,请看下面的4个示例进行实战理解。
⭐️创建索引
drop index idx_index on a;
create index idx_index on a (city,user_name, age);
-
示例一
利用最左前缀的字段
city
,单独作为查询条件explain select * from a where city = '辽宁';
key_len
使用了33个字节,只使用了一个city
作为索引,后面索引失效我们指定了city的字符长度是10,用的utf-8 所以10 * 3 + 2 (varchar存储长度开销的字节) + 1(可为null,占用1个字节)
-
示例二
利用最左前缀的字段
city
和下一个字段user_name
作为搜索条件explain select * from a where city = '辽宁' and user_name = '张三';
key_len
使用了66个字节,说明用到了city
,user_name
作为索引,age
索引失效city varchar(10) user_name varchar(10) (10 * 3 + 2 + 1) + (10 * 3 + 2 + 1) = 66 byte
-
示例三
使用最左前缀字段
city
和跳过索引列字段age
作为搜索条件-- 查询条件谁前谁后,MySQL优化器会优化,不用care explain select * from a where age = 20 and city = '辽宁' ;
key_len
使用了33个字节,说明只用到了city
。age
索引失效。city varchar(10) age int 10 * 3 + 2 + 1= 33 byte; ps: 如果用到age索引的话,会用int数据类型的4个字节,那么key_len应为33 + 4 = 37 byte
-
示例四
跳过最左前缀字段,用
user_name
和age
作为搜索条件explain select * from a where age = 20 and user_name = '张三' ;
key_len
为null,说明没走索引
四. 范围条件放最后
这一点是针对联合索引,所有记录都是按照索引列的值按顺序排放。
比如创建个index(a,b,c),B+Tree的数据页和记录会按照a的字段值进行排序。
若对a进行区间内的范围查询,如a>1 and a< 10
像这种的话
找到a = 1 的记录
找到a = 10的记录
由于这些记录是通过链表连起来的,所以他们之间的记录取出来很容易,通过这些索引页记录的bookmark,在到聚簇索引中回表查找对应行的完整记录。
但对于多个列之间的范围查询,只有对索引的最左字段的范围查询会用到B+Tree的索引。
⭐️创建索引
drop index idx_index on a;
-- birth datetime 6 byte
-- user_name varchar(10) 33 byte
-- age int 4 byte
create index idx_index on a (birth,user_name,age);
-
示例一
使用
birth
和user_name
作为范围查询explain select * from a where birth > '2004-07-28 00:00:00' and user_name > '张';
key_len
占用6个字节,只使用了birth
而没使用user_name
当`birth`值不等的情况下,`user_name`的顺序是无法确定的,所以MySQL优化器选择了只走了`birth`字段的索引。
-
示例二
使用
birth
作为范围查询,user_name
精准匹配explain select * from a where birth > '2004-07-28 00:00:00' and user_name = '张三';
key_len
同样也是6字节,birth
使用了索引,user_name
索引失效查询中,`birth`进行范围查询查找的记录中可能并不是按照`user_name`列进行排序,所以在搜索条件中继续以`user_name`列进行查找时是用不到B+Tree索引的。 - 所以对于一个联合索引,多个列的范围查询,只能用到最左边的列 - 如果最左边的列是精准查找,则下一个列可以进行范围查询,以此类推
-
示例三
对示例二进行求证,对
birth
精准查找,user_name
范围查找,age
精准匹配explain select * from a where birth = '2004-07-29 00:00:00' and user_name > '张' and age = 20;
key_len
占用39个字节,说明age
索引失效,证明了示例二的解释
五. 覆盖索引使用
覆盖索引是非常重要的一个工具,能够极大提高性能。
三星索引里最重要的那颗星就是宽索引星(覆盖索引)。
覆盖索引带来的好处:索引条目通常远小于数据行大小,对于只读取索引,MySQL就会极大减少数据访问量。这对缓存的负载非常重要,因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于IO密集型应用也有帮助,因为索引比数据更小,更容易全部放入内存中。
因为索引是按照列值顺序存储的,所以对于I/O密集型的范围查询会比随机从磁盘读取每一行数据I/O少得多。
对于InnoDB多聚簇索引,覆盖索引对InnoDB表作用页非常大,InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,就可以避免对主键索引的二次查询。
可以在select *
时,用指定所需的字段,并创建覆盖其对应的联合索引。
六. 不等于导致索引失效
MySQL在使用!=
和<>
时无法使用索引,而进行全表扫描。
这是因为不等于操作符使得数据库无法准确地通过索引找到特定的行,因为索引是排序的,不等于操作符破坏了这种排序,使得数据库不得不进行全表扫描或者范围查询来找出满足条件的行。
七. is null/not null 影响
⭐️如果对字段a进行的单个字段索引设置,如index(a)
a字段不允许为null
is null
是MySQL直接表示Impossible WHERE
(查询语句的WHERE子句永远为FALSE时,将会提示该额外信息)is not null
走的全表扫描
a字段允许为null
is null
走的索引is not null
走的全表扫描
⭐️如果是联合索引,如index(a,b)
a字段不允许为null
is null
是MySQL直接表示Impossible WHERE
(查询语句的WHERE子句永远为FALSE时,将会提示该额外信息)is not null
走的全表扫描
a字段允许为null
is null
走的索引,ref类型is not null
走的索引,range类型
小伙伴自己尝试即可。笔者就不做结果图的证明了。
但在设计表时,尽量不要声明为null。
八. like 查询的使用
创建index(a)
若使用a like ‘%xxx%’,会造成索引失效,全表扫描。
解决方式:
-
可以使用 a like ‘xx%’。
-
可以使用覆盖索引,进行优化
drop index idx_index on a; create index idx_index on a (user_name,age); -- 查询 explain select user_name, age from a where user_name like '%张';
九. 字符类型加引号
字符串类型字段作为搜索条件,不加引号会导致索引失效
MySQL的查询优化器,会自动进行类型的转换,比如下面的user_name会尝试转换为数字进行和1比较,造成索引失效
explain select user_name from a where user_name = '1';
explain select user_name from a where user_name = 1;
十. OR关键字前后索引问题
解决or前所索引失效:
-
or前后字段是同一个并且有索引
单独用一个字段进行or处理
drop index idx_index on a; create index idx_index on a (user_name); explain select user_name, age from a where user_name = '张' or user_name = '张三';
索引是没失效的
-
使用
in
代替or
create index idx_index on a (user_name); select * from a where user_name in ('1','2');
-
使用union
-
创建索引
-
查询语句修改
select * from a where user_name = '张三' or city = '四川'
改为
select * from a where user_name = '张三' union select * from a where city = '四川'
-
走了2个索引,创建个临时表,去重
-
-
覆盖索引
-
创建索引
create index idx_index on a (user_name,city);
-
查询
explain select user_name,city from a where user_name = '张' or city = '四川';
-
索引是没失效的
-
如果前后字段不一,索引会失效么
如果像OR
前后字段不一的话,即使创建索引,MySQL还是会走全表扫描,因为MySQL查询优化器难以同时用多个索引,只能用一个。利用OR
的操作符,会导致MySQL无法利用单一索引进行有效的过滤。
如
create index idx_index on a (user_name);
create index idx_index2 on a (city);
explain select * from a where user_name = '张' or city = '四川';
索引是失效的
十一. 利用索引来做排序和分组
MySQL可以使用同一个索引既满足搜索,又满足于排序/分组,这样是最好的。
搜索条件和排序或分组顺序一致
十二. 升降序使用
对于联合索引进行排序的场景。我们要求各个排序列的顺序是一致的。要么都是ASC
,要么都是DESC
。
如果排序多个列字段,不再一个索引里,这种情况不能使用索引排序,如:
index(a);
index(b);
order by a,b
十三. 按主键顺序插入
要避免随机的(不连续且分布范围非常大的)聚簇索引,特别是对I/O密集型的应用而言。列如,使用UUID当作聚簇索引,它会使插入变得完全随机,而且插入不仅花费很长时间(主键字段长),索引占据空间大(页分裂导致),数据没有任何聚集特性。
最简单的解决方案使用MySQL自带的AUTO_INCREMENT
自增列,既保证了数据行的顺序写入,又根据主键做关联的操作的性能会更好。
主键值是顺序性的,所以InnoDB把每一条记录都存储在上一条记录后面。当达到页的最大填充因子时(InnoDB默认的最大填充因子是页大小的15/16,留出的1/16用与以后的修改),下一条记录就会写入新的页中。若数据按照这种顺序方式加载,主键页就会近似于被顺序记录填满,也正是所期望的结果。
若新行的主键值不一定比之前的大,带来什么结果?
InnoDB无法直接把新行插入到索引的最后,而是需要重新寻找合适的位置,通常是已有数据的中间位置,这样会增加额外的工作,导致数据分布不够优化。
写入的目标页可能已经刷到磁盘上并从缓存移除,或者还没加载到缓存中,InnoDB在插入前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的IO。
写入是乱序的,InnoDB不得不频繁的做页分裂操作,以便信的行分配空间。页分裂会导致移动大量数据,一次插入最少需要修改三个页而不是一个页。
所以使用InnoDB时应该尽可能安好主键顺序插入数据,并且尽可能的使用单调增加的聚簇键值进行插入新行。
十四. 优化count查询
count()是一个特殊的函数
- 可以统计某个列的数量(不统计NULL),如count(a)。
- 也可以统计行数,如count(1),count(*)
count()需要扫描大量行才能获取精确的数据,因此很难优化。在MySQL层面能做的就是索引覆盖扫描了,或者从下面的几个点做优化
- 需要考虑修改应用架构
- 增加汇总表
- Redis做缓存统计
十五. 优化limit分页
分页操作,对于业务系统是必不可少的。我们通常使用limit
加上偏移量实现。
如果偏移量很大,会导致付出很大代价。如limit 10000,10
,这时MySQL需要查询10010条数据记录后,返回10条,前面的1w条将抛弃,这样导致的成本很高。
⭐️解决方案一:
先查分页中需要的主键值,然后根据主键值回表查询所需记录,再次过程查询的数据通过id主键索引,效率会高一些。
explain select * from (select id from a limit 10000,10) b, a where a.id = b.id;
执行计划及过可看出,首先执行了子查询,根据主键做索引全表扫描,然后通过与a表通过id主键关联查询。相比传统写法效率会高一些。
⭐️解决方式二:
但解决方式一还是存在性能问题,最佳的方式是在业务进行配合修改
explain select * from a where id > 10000 order by id limit 10
采用这种写法,前端需通过点击More来获取更多数据,而不是纯粹的分页。因此,每次查询只需通过上次查询出的数据的id来获取接下来的数据即可,这种写法是需要配合业务。
十六. MySQL Null 说明
null的分歧:
- null是未确定的值,每个null都是独一无二
- 所有null就一个
- null无意义,不计入统计。
MySQL NULL说明
在MySQL,null有3个等级配置,通过innodb_stats_method
配置:
- nulls_equal(默认配置):所有NULL值都是相等的,如果某个索引列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别多,所以倾向于不使用索引进行访问。
- nulls_unequal:认为所有NULL值不相等,如果某个索引列中NULL值特别多的话,这种统计方式会让优化器认为某个列中平均一个值重复次数特别少,所以倾向于使用索引进行访问。
- nulls_ignored:忽略NULL值。
show VARIABLES like 'innodb_stats_method'
set global innodb_stats_method = 'nulls_equal'
而且有迹象表明,在MySQL5.7.22以后的版本,对这个innodb_stats_method的修改不起作用,MySQL把这个值在代码里写死为nulls_equal。也就是说MySQL在进行索引列的数据统计行为又把null视为第二种情况(NULL值在业务上就是代表没有,所有的NULL值和起来算一份),看起来,MySQL中对Null值的处理也很分裂。所以总的来说,对于列的声明尽可能的不要允许为null。