索引优化实战(上)
实例sql:
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 = '员工记录表';
# 存储过程
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('edg_is_champion_',i),i,'dev');
set i=i+1;
end while;
end;;
delimiter ;
call insert_emp();
简介:根据上面的sql employees有name, age, position
的一个联合索引,并且创建有10万条数据用于测试
一些不容易理解的sql索引例子
1.联合索引第一个字段用范围一般不会走索引
根据上面的sql,理论可以用到name的索引,因为满足了最左前缀原则,虽然是范围查询,那也是age和position的长度没用到,按理说name的长度应该能用到。但是实际上没有
结论:mysql内部会优化分析,如果分析出的结果集可能很大,那么回表效率不高,还不如就全表扫描。
这时候修改一下范围
即使10万条的数据,范围缩小到>90000 也没有使用索引。
结论:基本上来说 如果联合索引第一个字段就用范围查找不会走索引,mysql内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不 如就全表扫描。
为什么说是基本上呢,再看下面例子
当范围缩小的93000的时候索引生效了,只是用了name的长度(索引长度怎么计算之前有讲过)。
那么如果第一条sql要想使用到索引怎么办,可以使用覆盖索引,稍微修改下sql 改为
这样就能使用到索引。
2.强制走索引
#‐‐ 关闭查询缓存 这个后面再说
set global query_cache_size=0;
set global query_cache_type=0;
没有使用索引。
改成一下sql
EXPLAIN SELECT * FROM employees force index(idx_name_age_position)
WHERE name > ‘edg_is_champion_100’;
使用了索引。
因为mysql内部会对sql进行优化和成本计算(后面再说),有可能使用索引的时候不一定使用,根据成本值来判断是否使用索引。但是可以通过 force index(索引名)强制使用索引。
那么是强制使用索引快呢还是mysql内部优化后选择不走索引快呢?
mysql内部优化不选择使用索引:
用时0.164s
强制使用索引:
用时0.269s
mysql优化后选择不使用索引比使用索引快。
结论:可以通过 force index(索引名) 强制使用索引,效率嘛,不一定比mysql内部优化后选择不使用索引快。
3.in和or在表数据量比较大的情况会走索引,在表记录不多的情况下会选择全表扫描
将employees 表复制一张employees_copy1的表,里面保留五六条记录
使用到索引。
没有使用到索引
基本上如果in、or的范围如果小的话,一般都是能使用到索引的。在上面实验中,如果employees_copy1数据量有10也会走索引。
索引下推和like查询
drop procedure if exists insert_emp;
delimiter ;;
create procedure insert_emp()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into employees(name,age,position) values(CONCAT('Labce_',i),i,'manager');
set i=i+1;
end while;
end;;
delimiter ;
call insert_emp();
通过上面的存储过程插入1000条记录。
不使用索引
使用到了索引。并且长度为140 说明使用到了name、age、position的长度。
看到这里就有两个疑问:
- like查询 百分号在后面不是可以用到索引吗,为什么查询edg%不行
- 为什么查询L%能用到索引。而且like百分号在后面不是类似于范围查询吗,而且上图也看到了type类型为range,那么范围查询后面的索引不是会失效吗,为什么又可以用到age和position的长度。
第一个疑问: like查询 百分号在后面不是可以用到索引吗,为什么查询edg%不行
上面有提到过mysql内部会有优化和成本计算。再看rows那一列,查询edg%的时候是12万多,查询L%只有1000多,这个是预估的扫描行数,并不是实际扫描行数。也就是说查询edg%时,mysql认为它要扫描12万行,而且由于是二级索引,后续也需要回表,因此还不如全盘扫描更直接。
第二个疑问:为什么L%能用到索引并且是全长度的索引。
mysql内部会有优化和成本计算。因为在查询L%的时候,mysql认为它只要扫描1000行,数据远远少于表的总数。因此成本计算后认为使用索引会更快。至于为什么用到了全长度的联合索引,这里涉及到一个概念叫做索引下推,like就使用到了索引下推。
索引下推:MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。
再回到上面的例子,对于辅助的联合索引(name,age,position),正常情况按照最左前缀原则,SELECT * FROM employees WHERE name like ‘L%’ AND age = 22 AND position =‘manager’ 这种情况只会走name字段索引,因为根据name字段过滤完,得到的索引行里的age和 position是无序的,无法很好的利用索引。
在MySQL5.6之前的版本,这个查询只能在联合索引里匹配到名字是 ‘L’ 开头的索引,然后拿这些索引对应的主键逐个回表,到主键索引上找出相应的记录,再比对age和position这两个字段的值是否符合。
使用了索引下推优化后,上面那个查询在联合索引里匹配到名字是 ‘L’ 开头的索引之后,同时还会在索引里过 滤age和position这两个字段,拿着过滤完剩下的索引对应的主键id再回表查整行数据。这样就减少了回表的数据。
看到这估计又会有第三个疑问:为什么范围查找Mysql没有用索引下推优化?
估计应该是Mysql认为范围查找过滤的结果集过大,like KK% 在绝大多数情况来看,过滤后的结果集比较小,所以这里Mysql选择给 like KK% 用了索引下推优化,当然这也不是绝对的,有时like KK% 也不一定就会走索引下推。
mysql成本计算和trace工具
上面查询like edg%的例子中,看到没有使用到索引。但是查询liek L%的时候有使用到了索引。这个例子说过mysql内部会进行成本计算。那么mysql是如何进行成本计算,并且最终选择合适的索引呢,可以通过trace工具使用进行分析。开启trace工具会影响mysql性能,所以只能临时分析sql使用,用完之后立即关闭
trace工具使用:
开启trace工具:set session optimizer_trace="enabled=on",end_markers_in_json=on;
关闭trace工具:set session optimizer_trace="enabled=off";
开启后,查询时如下图
SELECT * FROM employees WHERE
name
LIKE ‘edg%’ and age = 20 and
position = ‘dev’; SELECT * FROM information_schema.OPTIMIZER_TRACE;
两条sql要同时查询。
将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` like 'edg%') and (`employees`.`age` = 20) and (`employees`.`position` = 'dev'))"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { ‐‐第二阶段:SQL优化阶段
"select#": 1,
"steps": [
{
"condition_processing": { ‐‐条件处理
"condition": "WHERE",
"original_condition": "((`employees`.`name` like 'edg%') and (`employees`.`age` = 20) and (`employees`.`position` = 'dev'))",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "((`employees`.`name` like 'edg%') and (`employees`.`position` = 'dev') and multiple equal(20, `employees`.`age`))"
},
{
"transformation": "constant_propagation",
"resulting_condition": "((`employees`.`name` like 'edg%') and (`employees`.`position` = 'dev') and multiple equal(20, `employees`.`age`))"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "((`employees`.`name` like 'edg%') and (`employees`.`position` = 'dev') and multiple equal(20, `employees`.`age`))"
}
] /* 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": 120915, ‐‐扫描行数
"cost": 24602 ‐‐查询成本
} /* 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": [ ‐‐索引使用范围
"edg\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 <= name <= edg"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false, ‐‐使用该索引获取的记录是否按照主键排序
"using_mrr": false,
"index_only": false, ‐‐是否使用覆盖索引
"rows": 60457, ‐‐索引扫描行数
"cost": 72549, ‐‐索引使用成本
"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": 120915,
"access_type": "scan", ‐‐访问类型:为scan,全表扫描
"resulting_rows": 120915,
"cost": 24600,
"chosen": true ‐‐确定选择
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 120915,
"cost_for_plan": 24600,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "((`employees`.`age` = 20) and (`employees`.`name` like 'edg%') and (`employees`.`position` = 'dev'))",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`employees`",
"attached": "((`employees`.`age` = 20) and (`employees`.`name` like 'edg%') and (`employees`.`position` = 'dev'))"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"refine_plan": [
{
"table": "`employees`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": { ‐‐第三阶段:SQL执行阶段
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
由上面trace分析出全盘扫描的cost值为24600,使用联合索引cost为72549,所以mysql选择全盘扫描而不是走索引。
继续使用trace来分析
{
"steps": [
{
"join_preparation": {-- 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` like 'L%') and (`employees`.`age` = 20) and (`employees`.`position` = 'manager'))"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { -- sql优化
"select#": 1,
"steps": [
{
"condition_processing": { --条件处理
"condition": "WHERE",
"original_condition": "((`employees`.`name` like 'L%') and (`employees`.`age` = 20) and (`employees`.`position` = 'manager'))",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "((`employees`.`name` like 'L%') and (`employees`.`position` = 'manager') and multiple equal(20, `employees`.`age`))"
},
{
"transformation": "constant_propagation",
"resulting_condition": "((`employees`.`name` like 'L%') and (`employees`.`position` = 'manager') and multiple equal(20, `employees`.`age`))"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "((`employees`.`name` like 'L%') and (`employees`.`position` = 'manager') and multiple equal(20, `employees`.`age`))"
}
] /* 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": 120915,
"cost": 24602
} /* 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": [
"L\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 <= name <= L"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 1000,
"cost": 1201,
"chosen": true --确定选择
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */,
"chosen_range_access_summary": {--选择索引总结
"range_access_plan": {
"type": "range_scan",
"index": "idx_name_age_position",
"rows": 1000,
"ranges": [
"L\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 <= name <= L"
] /* ranges */
} /* range_access_plan */,
"rows_for_plan": 1000,
"cost_for_plan": 1201,
"chosen": true
} /* chosen_range_access_summary */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`employees`",
"best_access_path": {
"considered_access_paths": [ --最终访问路径
{
"rows_to_scan": 1000,
"access_type": "range", --查询类型是范围查询
"range_details": {
"used_index": "idx_name_age_position"
} /* range_details */,
"resulting_rows": 1000,
"cost": 1401,
"chosen": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 1000,
"cost_for_plan": 1401,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "((`employees`.`age` = 20) and (`employees`.`name` like 'L%') and (`employees`.`position` = 'manager'))",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`employees`",
"attached": "((`employees`.`age` = 20) and (`employees`.`name` like 'L%') and (`employees`.`position` = 'manager'))"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"refine_plan": [
{
"table": "`employees`",
"pushed_index_condition": "((`employees`.`age` = 20) and (`employees`.`name` like 'L%') and (`employees`.`position` = 'manager'))",
"table_condition_attached": null
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
根据上面trace分析 全盘扫描cost:24602,使用索引cost:1401 因此选择了使用索引。
由于篇幅原因,后续实战会对group by 、order by、where、分页等等常见sql优化