Mysql如何选择合适的索引,常见sql深入优化。
示例表
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
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 ( 'zhuge' , i ), i , 'dev' );
set i = i + 1 ;
end while ;
end ;;
delimiter ;
call insert_emp ();
1. 联合索引第一个字段用范围不会走索引
EXPLAIN SELECT * FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';
1718782157859.jpg
结论:在联合索引中,若首个字段使用范围查询,MySQL可能认为结果集较大,回表效率不高,因此倾向于选择全表扫描而非使用索引。
2. 强制走索引
EXPLAIN SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'LiLei' AND age = 22 AND position ='manager';
1718782414455.jpg
结论:即使通过强制使用索引让联合索引的第一个字段在范围查询时也利用索引,尽管扫描的行数减少,但查找效率可能仍不及全表扫描,因为回表操作效率较低。
做了一个小实验:
1
2
3
4
5
6
7
-- 关闭查询缓存
set global query_cache_size = 0 ;
set global query_cache_type = 0 ;
-- 执行时间0.333s
SELECT * FROM employees WHERE name > 'LiLei' ;
-- 执行时间0.444s
SELECT * FROM employees force index ( idx_name_age_position ) WHERE name > 'LiLei' ;
3. 覆盖索引优化
EXPLAIN SELECT name,age,position FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';
1718782548691.jpg
4.in和or在表数据量比较大的情况会走索引,在表记录不多的情况下会选择全表扫描
EXPLAIN SELECT * FROM employees WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
1718782616973.jpg
EXPLAIN SELECT * FROM employees WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position ='manager';
1718782670408.jpg
做一个小实验,将employees 表复制一张employees_copy的表,里面保留两三条记录
EXPLAIN SELECT * FROM employees_copy WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
1718782728671.jpg
EXPLAIN SELECT * FROM employees_copy WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position ='manager';
1718782779186.jpg
5. like KK% 一般情况都会走索引
EXPLAIN SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager';
1718782833403.jpg
EXPLAIN SELECT * FROM employees_copy WHERE name like 'LiLei%' AND age = 22 AND position ='manager';
1718782892169.jpg
这里给大家补充一个概念,索引下推(Index Condition Pushdown,ICP) , like KK%其实就是用到了索引下推优化
什么是索引下推了?
对于辅助的联合索引(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这两个字段的值是否符合。
MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。使用了索引下推优化后,上面那个查询在联合索引里匹配到名字是 ‘LiLei’ 开头的索引之后,同时还会在索引里过滤age和position这两个字段,拿着过滤完剩下的索引对应的主键id再回表查整行数据。
索引下推会减少回表次数,对于innodb引擎的表索引下推只能用于二级索引,innodb的主键索引(聚簇索引)树叶子节点上保存的是全行数据,所以这个时候索引下推并不会起到减少查询全行数据的效果。
为什么范围查找Mysql没有用索引下推优化?
MySQL在判断范围查找(如LIKE ‘KK%’)时,若预计过滤后的结果集不大,可能会选择使用索引下推优化来减少回表次数,尽管这并不总是绝对的,因为是否应用该优化还取决于其他因素。
Mysql如何选择合适的索引
EXPLAIN select * from employees where name > 'a';
1718783046553.jpg
如果用name索引需要遍历name字段联合索引树,然后还需要根据遍历出来的主键值去主键索引树里再去查出最终数据,成本比全表扫描还高,可以用覆盖索引优化,这样只需要遍历name字段的联合索引树就能拿到所有结果,如下:
EXPLAIN select name,age,position from employees where name > 'a' ;
1718783051085.jpg
EXPLAIN select * from employees where name > 'zzz' ;
1718783055785.jpg
对于上面这两种 name>‘a’ 和 name>‘zzz’ 的执行结果,mysql最终是否选择走索引或者一张表涉及多个索引,mysql最终如何选择索引,我们可以用trace工具来一查究竟,开启trace工具会影响mysql性能,所以只能临时分析sql使用,用完之后立即关闭
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
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
mysql> set session optimizer_trace="enabled=on",end_markers_in_json=on; --开启trace
mysql> select * from employees where name > 'a' order by position;
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;
查看trace字段:
{
"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": 10123, --扫描行数
"cost": 2054.7 --查询成本
} /* 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": 5061, --索引扫描行数
"cost": 6074.2, --索引使用成本
"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": 10123,
"access_type": "scan", --访问类型:为scan,全表扫描
"resulting_rows": 10123,
"cost": 2052.6,
"chosen": true, --确定选择
"use_tmp_table": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 10123,
"cost_for_plan": 2052.6,
"sort_cost": 10123,
"new_cost_for_plan": 12176,
"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": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
结论:全表扫描的成本低于索引扫描,所以mysql最终选择全表扫描
mysql> select * from employees where name > 'zzz' order by position;
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;
查看trace字段可知索引扫描的成本低于全表扫描,所以mysql最终选择索引扫描
mysql> set session optimizer_trace="enabled=off"; --关闭trace
常见sql深入优化
Order By与Group By优化
Case1:
1718783507970.jpg
分析: 利用最左前缀法则:中间字段不能断,因此查询用到了name索引,从key_len=74也能看出,age索引列用在排序过程中,因为Extra字段里没有using filesort
Case 2:
1718783511786.jpg
分析:
从explain的执行结果来看:key_len=74,查询使用了name索引,由于用了position进行排序,跳过了age,出现了Using filesort。
Case 3:
1718783516152.jpg
分析:
查找只用到索引name,age和position用于排序,无Using filesort。
Case 4:
1718783520021.jpg
分析:
和Case 3中explain的执行结果一样,但是出现了Using filesort,因为索引的创建顺序为name,age,position,但是排序的时候age和position颠倒位置了。
Case 5:
1718783524070.jpg
分析:
与Case 4对比,在Extra中并未出现Using filesort,因为age为常量,在排序中被优化,所以索引未颠倒,不会出现Using filesort。
Case 6:
1718783527770.jpg
分析:
虽然排序的字段列与索引顺序一样,且order by默认升序,这里position desc变成了降序,导致与索引的排序方式不同,从而产生Using filesort。Mysql8以上版本有降序索引可以支持该种查询方式。
Case 7:
1718783531003.jpg
分析:
对于排序来说,多个相等条件也是范围查询
Case 8:
1718783534819.jpg
可以用覆盖索引优化
1718783538301.jpg
优化总结:
MySQL支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序。index效率高,filesort效率低。
order by满足两种情况会使用Using index。
order by语句使用索引最左前列。
使用where子句与order by子句条件列组合满足索引最左前列。
尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀法则。
如果order by的条件不在索引列上,就会产生Using filesort。
能用覆盖索引尽量用覆盖索引
group by与order by很类似,其实质是先排序后分组,遵照索引创建顺序的最左前缀法则。对于group by的优化如果不需要排序的可以加上order by null禁止排序。注意,where高于having,能写在where中的限定条件就不要去having限定了。
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 ,那么使用 单路排序模式;
如果 字段的总长度大于max_length_for_sort_data ,那么使用 双路排序模·式。
示例验证下各种排序方式
1718783542702.jpg
查看下这条sql对应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
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
mysql> set session optimizer_trace="enabled=on",end_markers_in_json=on; --开启trace
mysql> select * from employees where name = 'zhuge' order by position;
mysql> select * from information_schema.OPTIMIZER_TRACE;
trace排序部分结果:
"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": 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 */
mysql> set max_length_for_sort_data = 10; --employees表所有字段长度总和肯定大于10字节
mysql> select * from employees where name = 'zhuge' order by position;
mysql> select * from information_schema.OPTIMIZER_TRACE;
trace排序部分结果:
"join_execution": {
"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 */
mysql> set session optimizer_trace="enabled=off"; --关闭trace
先看单路排序的详细过程:
从索引name找到第一个满足 name = ‘zhuge’ 条件的主键 id
根据主键 id 取出整行,取出所有字段的值,存入 sort_buffer 中
从索引name找到下一个满足 name = ‘zhuge’ 条件的主键 id
重复步骤 2、3 直到不满足 name = ‘zhuge’
对 sort_buffer 中的数据按照字段 position 进行排序
返回结果给客户端
再看下双路排序的详细过程:
从索引 name 找到第一个满足 name = ‘zhuge’ 的主键id
根据主键 id 取出整行,把排序字段 position 和主键 id 这两个字段放到 sort buffer 中
从索引 name 取下一个满足 name = ‘zhuge’ 记录的主键 id
重复 3、4 直到不满足 name = ‘zhuge’
对 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很多参数设置都是做过优化的,不要轻易调整。
索引设计原则
代码先行,索引后建
在数据表创建后,不建议立即为其建立索引。正确的做法是在主体业务功能开发完成后,分析所有涉及该表的SQL语句,然后根据这些SQL语句的需求来合理地建立索引。
联合索引优化策略
为了提高查询效率,应优先设计联合索引(而非单值索引),并确保每个联合索引尽量覆盖SQL语句中的WHERE
、ORDER BY
、GROUP BY
等关键字段。同时,要确保联合索引中的字段顺序满足SQL查询的最左前缀原则。
避免在小基数字段上创建索引。
小基数字段指的是表中某个字段的不同值数量很少,如性别字段仅有“男”和“女”两种值。对于这类字段,建立索引的效益并不高,因为索引树中的节点数量有限,无法有效发挥B+树快速二分查找的优势。当执行查询时,全表扫描可能更加高效。因此,建议优先选择基数较大的字段来创建索引,以充分利用索引的优势。
对于长字符串,建议采用前缀索引。
尽管为字段类型较小的列(如TINYINT
)设计索引能减少磁盘占用并提高搜索性能,但某些情况下我们仍需要对VARCHAR(255)
等大字段建立索引。为减少空间占用,可以考虑对字段的前20个字符创建前缀索引,如KEY index_name(name(20), age, position)
。在搜索时,系统会先根据前20个字符在索引树中定位,再回表比对完整数据。但请注意,前缀索引不适用于ORDER BY
或GROUP BY
操作,因为索引中仅包含字段的部分信息。
当WHERE
与ORDER BY
冲突时,优先考虑WHERE
。
在索引设计面临WHERE
与ORDER BY
的冲突时,通常建议优先考虑WHERE
条件,利用索引快速筛选数据,再进行排序。因为基于索引的筛选通常能迅速缩小数据范围,从而减少排序的成本。
基于慢SQL查询进行优化。
针对监控后台发现的慢SQL查询,可以进行特定的索引优化。有关慢SQL查询的更多信息,请参考文章Mysql慢查询 。
索引设计实践
在社交场景APP中,设计用户搜索功能的索引时,关键在于平衡查询频率、数据筛选的精确性和索引的维护成本。以user
表为例,当涉及地区、性别、年龄、爱好等多维度筛选时,合理的索引设计至关重要。
首先,针对经常使用的筛选条件,如省市和性别,可以设计一个联合索引(province, city, sex)
。这些字段虽然基数较小,但查询频率高,因此值得优化。
当需要加入年龄范围筛选时,可以将age
字段加入索引的末尾,形成(province, city, sex, age)
。注意,范围查询会限制索引的效益,所以应尽量避免在索引的前列使用范围查询。
对于爱好等非频繁筛选条件,可以将其加入索引,但需要注意索引的长度和维护成本。如果爱好字段很长或变化频繁,则可能需要重新考虑索引策略。
对于活跃用户的筛选,引入is_login_in_latest_7_days
字段是个有效的策略。这样可以将此字段加入索引,如(province, city, sex, is_login_in_latest_7_days, age)
,以优化查询性能。
在排序和分页场景中,如果主要基于某些字段进行排序,则可以将这些字段加入索引,以便数据库能够更有效地利用索引进行排序。
对于特定查询,如按受欢迎度排序的女性用户,可以设计一个辅助索引(sex, score)
,以满足此类非典型查询的需求。
总之,索引设计的核心在于根据查询的实际情况和频率,平衡查询性能和维护成本。通过合理地选择和使用联合索引、辅助索引等策略,可以显著提高大数据量表的查询速度和性能。
1718783552453.jpg