索引优化二

索引设计原则

代码先行,索引后上

建完表不要立马就建立索引,一般应等主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立
索引。

联合索引尽量覆盖条件

可设计一个或两三个联合索引,尽量少建单值索引,让每一个联合索引都尽量去包含sql语句里的where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原则

不要在小基数字段上建立索引

索引基数是指这个字段在表里总共有多少个不同的值,若一张表总共100万行记录,其中性别字段其值不是男就是女,则该字段基数就是2。

对这种小基数字段建立索引,还不如全表扫描,因为你的索引树里就包含男和女两种值,根本没法进行快速的二分查找,那用索引就没有太大的意义了。

一般建立索引,尽量使用那些基数比较大的字段,才能发挥出B+树快速二分查找的优势

当然也存在特例,类似delete_status这类字段,用01分别表示未删除和已删除,一般仅仅只用到0的情况下也可以建立索引。

长字符串我们可以采用前缀索引

尽量对字段类型较小的列设计索引,如tinyint类型的字段,字段类型较小的话,占用磁盘空间也会比较小,搜索时性能也会比较好。所谓的字段类型小一点的列,也不是绝对的,很多时候要针对varchar(255)这种字段建立索引,哪怕多占用一些磁盘空间也是有必要的。

对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对该字段的20个字符建立索引,对这个字段里的每个值的前20个字符放在索引树里,类似于KEY,index(name(20),age,position)

在where条件里搜索时,若根据name字段来搜索,此时就会先到索引树里根据name字段的前20个字符去搜索,定位到之后前20个字符的前缀匹配的部分数据之后,再回到聚簇索引提取出来完整的name字段值进行比对。

若要对字段name做排序order by分组group by,此时name因为在索引树里仅仅包含了前20个字符,故该排序和分组没法用上索引

where与order by冲突时优先where

一般这种时候往往都是让where条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。大多数情况基于索引进行where筛选往往可以最快速度筛选出需要的少部分数据,然后做排序的成本可能会小很多

基于慢sql查询做优化

可根据监控后台的一些慢sql,针对这些慢sql查询做特定的索引优化。

索引下推

对于辅助的联合索引(name, age, position),正常情况按照最左前缀原则SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager'该情况只会走name字段索引,因为根据name字段过滤完,得到的索引行里age和position是无序的,无法很好的利用索引。

MySQL5.6之前的版本,该查询只能在联合索引里匹配到名字是’LiLei’开头的索引,然后拿这些索引对应的主键逐个回表,到主键索引上找出相应的记录,再比对age和position这两个字段的值是否符合。

MySQL5.6引入了索引下推优化可在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可有效减少回表次数。使用索引下推优化后,查询在联合索引里匹配到名字’LiLei’开头的索引后,同时还会在索引里过滤age和position这两个字段,拿着过滤完剩下的索引对应的主键id再回表查整行数据。

索引下推会减少回表次数,对于InnoDB引擎的表索引下推只能用于二级索引,InnoDB的主键索引是聚簇索引叶子节点上保存的是全行数据,故这时索引下推并不会起到减少查询全行数据的效果。

Using filesort文件排序原理详解

filesort文件排序方式有单路排序双路排序又叫回表排序模式

  • 单路排序一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可看到sort_mode信息里显示<sort_key, additional_fields>或者<sort_key, packed_additional_fields>
  • 双路排序:首先根据相应条件取出相应的排序字段和可直接定位行数据的行ID,然后在sort buffer中进行排序,排序完后需要再次取回其它需要的字段;用trace工具可看到sort_mode信息里显示<sort_key, rowid>

MySQL通过比较系统变量max_length_for_sort_data与需查询字段总大小来判断使用哪种排序模式,默认1024字节,若字段总长度小于max_length_for_sort_data 则使用单路排序模式;否则使用双路排序模式。

1
2
3
set session optimizer_trace="enabled=on",end_markers_in_json=on;
explain select * from employees where name = 'zhangsan' order by position;
select * from information_schema.OPTIMIZER_TRACE;
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
{//Sql执行阶段
"select#": 1,
"steps": [
{
"filesort_information": [
{
"direction": "asc",
"table": "`employees`",
"field": "position"
}
] /* filesort_information */,
"filesort_priority_queue_optimization": {
"usable": false,
"cause": "not applicable (no LIMIT)"
} /* filesort_priority_queue_optimization */,
"filesort_execution": [
] /* filesort_execution */,
"filesort_summary": {//文件排序信息
"rows": 10000, //预计扫描行数
"examined_rows": 10000, //参与排序的行
"number_of_tmp_files": 3, //使用临时文件的个数,这个值若为0代表全部使用sort_buffer内存排序,否则使用磁盘文件排序
"sort_buffer_size": 262056, //排序缓存的大小,单位Byte
"sort_mode": "<sort_key, packed_additional_fields>" //排序方式,这里用的单路排序
} /* filesort_summary */
}
] /* steps */
} /* join_execution */
1
2
3
4
set max_length_for_sort_data = 10; --employees表所有字段长度总和肯定大于10字节
explain select * from employees where name = 'zhangsan' order by position;
select * from information_schema.OPTIMIZER_TRACE;
set session optimizer_trace="enabled=off"; --关闭trace
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
{
"select#": 1,
"steps": [
{
"filesort_information": [
{
"direction": "asc",
"table": "`employees`",
"field": "position"
}
] /* filesort_information */,
"filesort_priority_queue_optimization": {
"usable": false,
"cause": "not applicable (no LIMIT)"
} /* filesort_priority_queue_optimization */,
"filesort_execution": [
] /* filesort_execution */,
"filesort_summary": {
"rows": 10000,
"examined_rows": 10000,
"number_of_tmp_files": 2,
"sort_buffer_size": 262136,
"sort_mode": "<sort_key, rowid>"// 排序方式, 这里用的双路排序
} /* filesort_summary */
}
] /* steps */
} /* join_execution */

单路排序:从索引name找到第一个满足name = ‘zhangsan’ 条件的主键id,根据主键id取出整行,取出所有字段的值,存入sort_buffer中,直到将所以满足条件的数据找完,对sort_buffer中的数据按照字段position进行排序。

双路排序:从索引name找到第一个满足name = ‘zhuge’的主键id,根据主键id取出整行,把排序字段position和主键id放到sort buffer中,直到将所以满足条件的数据找完,对sort_buffer中的字段position和主键id按照字段 position进行排序,遍历排序好的id和字段position,按照id的值回到原表中取出所有字段的值返回给客户端

对比两个排序模式,单路排序会把所有需要查询的字段都放到sort buffer,而双路排序只会把主键和需要排序的字段放到sort buffer中进行排序然后再通过主键回到原表查询需要的字段

若MySQL排序内存sort_buffer配置比较小且没有条件继续增加,可适当把max_length_for_sort_data配置小点,让优化器选择使用双路排序算法,可在sort_buffer中一次排序更多的行,只需要再根据主键回到原表取数据。

若MySQL排序内存有条件可配置比较大,可以适当增大max_length_for_sort_data的值,让优化器优先选择单路排序,把需要的字段放到sort_buffer中,这样排序后就会直接从内存里返回查询结果。

MySQL通过max_length_for_sort_data 参数来控制排序,在不同场景使用不同的排序模式,从而提升排序效率。若全部使用sort_buffer内存排序一般情况下效率会高于磁盘文件排序,但不能因为这个就随便增大sort_buffer默认1M,mysql很多参数设置都是做过优化的,不要轻易调整。

优化实例

示例数据

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
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='员工记录表';

INSERT INTO employees(name,age,position,hire_time) VALUES('LiLei',22,'manager',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('HanMeimei', 23,'dev',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('Lucy',23,'dev',NOW());

-- 插入一些示例数据
drop procedure if exists insert_emp;
delimiter ;;
create procedure insert_emp()
begin
declare i int;
set i=1;
while(i<=100000)do
insert into employees(name,age,position) values(CONCAT('zhangsan',i),i,'dev');
set i=i+1;
end while;
end;;
delimiter ;
call insert_emp();

联合索引第一个字段用范围查找

第一个字段用范围查找,可能不会走索引,mysql内部计算第一个字段查询范围若结果集很大,回表效率不高,或扫描的行很多,还不如就全表扫描,就不会走索引,很明显这里没有走索引,但是不同的数据是有可能走索引的,即使走索引也只是name字段能走索引

1
EXPLAIN SELECT * FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';

这里使用like就能走索引,且三个字段的索引都走了,但like不一定必定走索引

1
EXPLAIN SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager';

强制索引

使用force index强制使用索引,虽然使用了强制走索引让联合索引第一个字段范围查找也走索引,扫描的行rows看上去也少了点,但是最终查找效率不一定比全表扫描高,因为回表效率不高

1
EXPLAIN SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'LiLei' AND age = 22 AND position ='manager';

关闭查询缓存,分别查询如下两个语句,很明强制索引比不走索引慢得多

1
2
3
4
5
6
set global query_cache_size=0;
set global query_cache_type=0;
-- 执行时间0.103s
SELECT * FROM employees WHERE name > 'LiLei';
-- 执行时间0.623s
SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'LiLei';

覆盖索引优化

1
EXPLAIN SELECT name,age,position FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';

in和or

inor表数据量比较大的情况会走索引,在表记录不多的情况下会选择全表扫描

1
2
EXPLAIN SELECT * FROM employees WHERE name in ('LiLei', 'HanMeimei', 'Lucy') AND age = 22 AND position = 'manager';
EXPLAIN SELECT * FROM employees WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position = 'manager';

将employees表复制一张employees_copy的表,只保留两三条记录

1
2
EXPLAIN SELECT * FROM employees_copy WHERE name in ('LiLei', 'HanMeimei', 'Lucy') AND age = 22 AND position = 'manager';
EXPLAIN SELECT * FROM employees_copy WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position = 'manager';

like

like KK%一般情况都会走索引,之所以走索引,是因为用到了索引下推优化

1
EXPLAIN SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager';

但若扫表数据过多,也可能不走索引

1
EXPLAIN SELECT * FROM employees WHERE name like 'zhangsan%' AND age = 22 AND position ='manager';

order by与group by优化

最左前缀法则,中间字段不能断,因此查询用到了name索引,从key_len=74也能看出,age索引列用在排序过程中,因为Extra字段里没有using filesort

1
explain select * from employees where name = 'Lilei' and position = 'dev' order by age;

key_len=74查询使用了name索引,由于用了position进行排序,跳过了age,出现了Using filesort

1
explain select * from employees where name = 'Lilei' order by position;

查找只用到索引name,age和position用于排序,Using filesort

1
explain select * from employees where name = 'Lilei' order by age, position;

出现Using filesort,因为索引的创建顺序为name,age,position,但是排序的时候age和position颠倒位置了

1
explain select * from employees where name = 'Lilei' order by position,age;

未出现Using filesort,因为age为常量,在排序中被优化,所以索引未颠倒

1
explain select * from employees where name = 'Lilei' and age = 18 order by position,age;

虽然排序的字段列与索引顺序一样,且order by默认升序,这里position desc变成了降序,导致与索引的排序方式不同,从而产生Using filesort。Mysql8以上版本有降序索引可以支持该种查询方式。

1
explain select * from employees where name = 'zhangsan' order by age asc, position desc;

对于排序来说,多个相等条件也是范围查询

1
explain select * from employees where name in ('zhangsan', 'LiLei') order by age, position;

1
explain select * from employees where name > 'a' order by name;

用覆盖索引优化

1
explain select name, age, position from employees where name > 'a' order by name;

MySQL支持两种方式的排序filesortindexUsing index是指MySQL扫描索引本身完成排序index效率高,filesort效率低

order by满足两种情况会使用Using index:order by语句使用索引最左前列;使用where子句与order by子句条件列组合满足索引最左前列

尽量在索引列上完成排序,遵循索引建立时的最左前缀法则。若order by的条件不在索引列上,就会产生Using filesort。能用覆盖索引尽量用覆盖索引

group byorder by很类似,其实质是先排序后分组,遵照索引创建顺序的最左前缀法则,对于group by的优化若不需要排序的可加上order by null禁止排序

where高于having,能写在where中的限定条件就不要去having限定了

trace工具

示例数据中name大于a的数据非常多,而大于zzz的数据几乎没有,由于用name索引需要遍历name字段联合索引树,然后还需要根据遍历出来的主键值去主键索引树里再去查出最终数据,成本比全表扫描还高;

1
EXPLAIN select * from employees where name > 'a';

使用覆盖索引优化

1
EXPLAIN select name,age,position from employees where name > 'a' ;

1
EXPLAIN select * from employees where name > 'zzz' ;

最终是否选择走索引或一张表涉及多个索引,最终如何选择索引,可通过trace工具来具体分析,开启trace工具会影响MySQL性能,只能临时分析sql使用,用完之后立即关闭,如下是开启trace工具命令,然后调用具体的查询语句,在查询information_schema.OPTIMIZER_TRACE的trace字段。

1
2
3
4
5
6
-- 开启trace
set session optimizer_trace = "enabled=on",end_markers_in_json = on;
select * from employees where name > 'a' order by position;
select * from information_schema.OPTIMIZER_TRACE;
-- 关闭trace
set session optimizer_trace="enabled=off";

查询结果如下,可以很明显看到全表扫描的成本低于索引扫描,所以mysql最终选择全表扫描:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
{
"steps": [
{
"join_preparation": { -- 第一阶段:SQL准备阶段,格式化sql
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `employees`.`id` AS `id`,`employees`.`name` AS `name`,`employees`.`age` AS `age`,`employees`.`position` AS `position`,`employees`.`hire_time` AS `hire_time` from `employees` where (`employees`.`name` > 'a') order by `employees`.`position`"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { --第二阶段:SQL优化阶段
"select#": 1,
"steps": [
{
"condition_processing": { --条件处理
"condition": "WHERE",
"original_condition": "(`employees`.`name` > 'a')",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(`employees`.`name` > 'a')"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(`employees`.`name` > 'a')"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "(`employees`.`name` > 'a')"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
"table_dependencies": [ --表依赖详情
{
"table": "`employees`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [ --预估表的访问成本
{
"table": "`employees`",
"range_analysis": {
"table_scan": { --全表扫描情况
"rows": 97275, --扫描行数
"cost": 19810 --查询成本
} /* table_scan */,
"potential_range_indexes": [ --查询可能使用的索引
{
"index": "PRIMARY", --主键索引
"usable": false,
"cause": "not_applicable"
},
{
"index": "idx_name_age_position", --辅助索引
"usable": true,
"key_parts": [
"name",
"age",
"position",
"id"
] /* key_parts */
}
] /* potential_range_indexes */,
"setup_range_conditions": [
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
"analyzing_range_alternatives": { --分析各个索引使用成本
"range_scan_alternatives": [
{
"index": "idx_name_age_position",
"ranges": [
"a < name" --索引使用范围
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false, --使用该索引获取的记录是否按照主键排序
"using_mrr": false,
"index_only": false, --是否使用覆盖索引
"rows": 48637, --索引扫描行数
"cost": 58365, --索引使用成本
"chosen": false, --是否选择该索引
"cause": "cost"
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`employees`",
"best_access_path": { --最优访问路径
"considered_access_paths": [ --最终选择的访问路径
{
"rows_to_scan": 97275,
"access_type": "scan", --访问类型:为scan,全表扫描
"resulting_rows": 97275,
"cost": 19808,
"chosen": true, --确定选择
"use_tmp_table": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 97275,
"cost_for_plan": 19808,
"sort_cost": 97275,
"new_cost_for_plan": 117083,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`employees`.`name` > 'a')",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`employees`",
"attached": "(`employees`.`name` > 'a')"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"clause_processing": {
"clause": "ORDER BY",
"original_clause": "`employees`.`position`",
"items": [
{
"item": "`employees`.`position`"
}
] /* items */,
"resulting_clause_is_simple": true,
"resulting_clause": "`employees`.`position`"
} /* clause_processing */
},
{
"reconsidering_access_paths_for_index_ordering": {
"clause": "ORDER BY",
"steps": [
] /* steps */,
"index_order_summary": {
"table": "`employees`",
"index_provides_order": false,
"order_direction": "undefined",
"index": "unknown",
"plan_changed": false
} /* index_order_summary */
} /* reconsidering_access_paths_for_index_ordering */
},
{
"refine_plan": [
{
"table": "`employees`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": { --第三阶段:SQL执行阶段
"select#": 1,
"steps": [
{
"filesort_information": [
{
"direction": "asc",
"table": "`employees`",
"field": "position"
}
] /* filesort_information */,
"filesort_priority_queue_optimization": {
"usable": false,
"cause": "not applicable (no LIMIT)"
} /* filesort_priority_queue_optimization */,
"filesort_execution": [
] /* filesort_execution */,
"filesort_summary": {
"rows": 100003,
"examined_rows": 100003,
"number_of_tmp_files": 31,
"sort_buffer_size": 262056,
"sort_mode": "<sort_key, packed_additional_fields>"
} /* filesort_summary */
}
] /* steps */
} /* join_execution */
}
] /* steps */
}