索引优化实战
索引优化实战
MYSQL 索引优化实战笔记
优化实战
联合索引第一个字段用范围不会走索引
mysql内部优化规则可能会觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不知直接全表扫
强制走索引
-- 语句
EXPLAIN SELECT * FROM user force index(idx)
添加强制索引可以让语句走索引,但是最总查找的效率不一定会比扫全表高,因为回表效率不高
覆盖索引优化
对于不走索引的语句,可以尝试使用覆盖索引来进行优化
in和or在表数据大会走索引,反之不会
like语句一般情况下都会走索引
like语句看着其实跟大于小于号差不多,之所以会走索引是因为用到了索引下推
索引下推
联合索引是按照最左前缀原则来进行匹配,正常情况下联合索引首个字段就使用like,那只会匹配到这个字段, 后面的因为不能确保是有序的就无法再利用索引。
5.6版本以前,首个字段就停止索引匹配,那就会拿这些对应索引的主键逐个回表找数据,再比对后序字段是否符合。5.6对此进行优化,在索引便利过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。这就是索引下推。
索引下推回减少回表次数,对于innodb引擎只能适用于二级索引,对主键索引并不适用
trace工具
trace
-- 开启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";
结果集
{
"steps": [
{
"join_preparation": {
/* 第一阶段:准备阶段,格式化SQL */
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `t_menu`.`menu_id` AS `menu_id`,`t_menu`.`menu_name` AS `menu_name`,`t_menu`.`menu_url` AS `menu_url`,`t_menu`.`parent_id` AS `parent_id`,`t_menu`.`level` AS `level`,`t_menu`.`icon` AS `icon`,`t_menu`.`order_by` AS `order_by`,`t_menu`.`hidden` AS `hidden`,`t_menu`.`remark` AS `remark` from `t_menu` where (`t_menu`.`menu_name` > 'a') order by `t_menu`.`parent_id`"
}
]
/* steps */
}
/* join_preparation */
},
{
"join_optimization": {
/* 第二阶段:SQL优化阶段 */
"select#": 1,
"steps": [
{
"condition_processing": {
/* 条件处理 */
"condition": "WHERE",
"original_condition": "(`t_menu`.`menu_name` > 'a')",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(`t_menu`.`menu_name` > 'a')"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(`t_menu`.`menu_name` > 'a')"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "(`t_menu`.`menu_name` > 'a')"
}
]
/* steps */
}
/* condition_processing */
},
{
"substitute_generated_columns": {
}
/* substitute_generated_columns */
},
{
"table_dependencies": [
/* 表依赖详情 */
{
"table": "`t_menu`",
"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": "`t_menu`",
"range_analysis": {
"table_scan": {
/* 全表扫描情况 */
"rows": 15,
/* 扫描行数 */
"cost": 6.1
/* 扫描成本 */
}
/* table_scan */,
"potential_range_indexes": [
/* 查询可能使用的索引 */
{
"index": "PRIMARY",
/* 主键索引 */
"usable": false,
"cause": "not_applicable"
},
{
"index": "idx_test",
/* 二级索引 */
"usable": true,
"key_parts": [
"menu_name",
"menu_url",
"parent_id",
"menu_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_test",
"ranges": [
"a < menu_name"
/* 索引使用范围 */
]
/* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
/* 使用该索引获取的记录是否按照主键排序 */
"using_mrr": false,
"index_only": false,
/* 是否使用覆盖索引 */
"rows": 15,
/* 扫描行数 */
"cost": 19.01,
/* 使用成本 */
"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": "`t_menu`",
"best_access_path": {
/* 最有访问路径 */
"considered_access_paths": [
/* 最终选择的访问路径 */
{
"rows_to_scan": 15,
"access_type": "scan",
/* 访问类型:scan为全表扫描 */
"resulting_rows": 15,
"cost": 4,
"chosen": true,
"use_tmp_table": true
}
]
/* considered_access_paths */
}
/* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 15,
"cost_for_plan": 4,
"sort_cost": 15,
"new_cost_for_plan": 19,
"chosen": true
}
]
/* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`t_menu`.`menu_name` > 'a')",
"attached_conditions_computation": [
]
/* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`t_menu`",
"attached": "(`t_menu`.`menu_name` > 'a')"
}
]
/* attached_conditions_summary */
}
/* attaching_conditions_to_tables */
},
{
"clause_processing": {
"clause": "ORDER BY",
"original_clause": "`t_menu`.`parent_id`",
"items": [
{
"item": "`t_menu`.`parent_id`"
}
]
/* items */,
"resulting_clause_is_simple": true,
"resulting_clause": "`t_menu`.`parent_id`"
}
/* clause_processing */
},
{
"reconsidering_access_paths_for_index_ordering": {
"clause": "ORDER BY",
"steps": [
]
/* steps */,
"index_order_summary": {
"table": "`t_menu`",
"index_provides_order": false,
"order_direction": "undefined",
"index": "unknown",
"plan_changed": false
}
/* index_order_summary */
}
/* reconsidering_access_paths_for_index_ordering */
},
{
"refine_plan": [
{
"table": "`t_menu`"
}
]
/* refine_plan */
}
]
/* steps */
}
/* join_optimization */
},
{
"join_execution": {
/* 第三阶段:执行阶段 */
"select#": 1,
"steps": [
{
"filesort_information": [
{
"direction": "asc",
"table": "`t_menu`",
"field": "parent_id"
}
]
/* filesort_information */,
"filesort_priority_queue_optimization": {
"usable": false,
"cause": "not applicable (no LIMIT)"
}
/* filesort_priority_queue_optimization */,
"filesort_execution": [
]
/* filesort_execution */,
"filesort_summary": {
/* 文件排序信息 */
"rows": 15,
/* 预计扫描行数 */
"examined_rows": 15,
/* 参与排序行 */
"number_of_tmp_files": 0,
/* 使用临时文件个数,如果为0表示全部用sort_puffer内存排序 */
"sort_buffer_size": 14656,
/* 排序缓存大小 */
"sort_mode": "<sort_key, rowid>"
/* 排序方式,双路 <sort_key, packed_additional_fields>:单路排序*/
}
/* filesort_summary */
}
]
/* steps */
}
/* join_execution */
}
]
/* steps */
}
ORDER BY 与 GROUP BY 优化
order by排序的优化主要是利用索引已经排好序的规律来优化,如果没有使用到会通过文件进行排序消耗性能
- 使用order by 联合索引,查询条件与排序条件中间字段不能断
- order by 多个字段时,顺序要与索引字段一致
- 使用降序排序不会走索引排序
- in 不会走索引排序,因为结果集不确定是否有序
- 使用 > 条件的不会走索引排序,可能是数据量太大
总结
- 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限定了。
文件排序原理
排序方式
单路排序
一次性取满足条件的数据的所有字段,然后在sort_buffer中进行排序。
trace工具的sort_mode显示<sort_key, additional_fields>或<sort_key, packed_additional_fields>
双路排序
双路排序又称回表
排序。首先根据条件查询出相应的字段,然后取排序字段和主键在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,那么使用双路排序模式;
排序过程
假设条件为 where name = 'a'
单路排序
- 从索引中找到第一个符合条件的主键ID
- 根据ID取出
所有字段
存入sort_buffer中 - 查找下一个符合条件的ID
- 一直重复2,3直到没有符合的记录
- 排序
- 返回结果
双路排序
- 从索引中找到第一个符合条件的主键ID
- 根据ID取出
ID和排序字段
存入sort_buffer中 - 查找下一个符合条件的ID
- 一直重复2,3直到没有符合的记录
- 排序
- 按照排序好的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. 代码先行,索引后上
不知大家一般是怎么给数据表建立索引的,是建完表马上就建立索引吗? 这其实是不对的,一般应该等到主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立索引。
2. 联合索引尽量覆盖条件
比如可以设计一个或者两三个联合索引(尽量少建单值索引),让每一个联合索引都尽量去包含sql语句里的where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原则。
3. 不要在小基数字段上建立索引
索引基数是指这个字段在表里总共有多少个不同的值,比如一张表总共100万行记录,其中有个性别字段,其值不是男就是女,那么该字段的基数就是2。
如果对这种小基数字段建立索引的话,还不如全表扫描了,因为你的索引树里就包含男和女两种值,根本没法进行快速的二分查找,那用索引就没有太大的意义了。
一般建立索引,尽量使用那些基数比较大的字段,就是值比较多的字段,那么才能发挥出B+树快速二分查找的优势来。
4. 长字符串我们可以采用前缀索引
尽量对字段类型较小的列设计索引,比如说什么tinyint之类的,因为字段类型较小的话,占用磁盘空间也会比较小,此时你在搜索的时候性能也会比较好一点。
当然,这个所谓的字段类型小一点的列,也不是绝对的,很多时候你就是要针对varchar(255)这种字段建立索引,哪怕多占用一些磁盘空间也是有必要的。
对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对这个字段的前20个字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于
KEY index(name(20),age,position)。
此时你在where条件里搜索的时候,如果是根据name字段来搜索,那么此时就会先到索引树里根据name字段的前20个字符去搜索,定位到之后前20个字符的前缀匹配的部分数据之后,再回到聚簇索引提取出来完整的name字段值进行比对。
但是假如你要是order by name,那么此时你的name因为在索引树里仅仅包含了前20个字符,所以这个排序是没法用上索引的, group by也是同理。所以这里要对前缀索引有一个了解。
5. where与order by冲突时优先where
在where和order by出现索引设计冲突时,到底是针对where去设计索引,还是针对order by设计索引?到底是让where去用上索引,还是让order by用上索引?
一般这种时候往往都是让where条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。 因为大多数情况基于索引进行where筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可能会小很多。
6. 基于慢sql查询做优化
可以根据监控后台的一些慢sql,针对这些慢sql查询做特定的索引优化。