MySQL索引优化实战

联合索引第一个字段用范围不会走索引 type = all

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

因为MySQL有自己的优化机制,当索引中第一个字段是 大于 小于 那么MySQL就会认为会扫描出很多条数据 并且是二级索引可能还要回表,那么多数据需要回表还不如不走当前使用索引直接走主键索引全表扫描

强制索引

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

因为where中第一个是name 原本应该走索引但是因为 使用的是范围查询 MySQL认为索引第一个字段就是范围查询 计算得出成本比全表扫描成本还大 就会走全表扫描

但是如果MySQL计算成本错误,走索引还是比较快的 那么我们可以强制让MySQL走索引。

使用 force index(索引名称) 就会强制走索引

MySQL走不走索引也会跟 name > 'LiLei' 得出的数据多少有关 如果很多 但是后面还有字段 age作为条件 这时候就会全表扫描,后面age = 22 是肯定不能走索引了,因为前边 使用 name > 'LiLei' 已经让age无序了

使用索引优化

select 后面不要跟 * ,尽量使用二级索引中索引字段 这样不需要回表

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

因为 数据比较少的话 MySQL认为走in 还要回表 并且数据本身就少 直接全表查询也比较容易就那么点数据 还不需要回表 并且 in 还可能包好全表数据 所以MySQL在数据少的情况下会全表

如果数据量大,那么走索引还是比较快的,因为那么多数据 走 in 索引比全表扫描成本低

like 'KK%' 一般情况都会走索引

假设表中有联合索引:index_test(name,age,gend);

where name like 'KK%' and age = 10 and gend = '男';

like 'kk%'走索引是因为MySQL中都是排好序的 通过 'kk%'其实就是相当于是一个 范围查询 的意思,但是MySQL会认为 模糊查询得到的数据量会比 > 、< 这种得到的数据量小 所以会走索引但是第一个字段是 <、>不走索引

索引下推(ICP)

MySQL5.6版本之前 使用的是 二级索引name找到的所有主键id 去回表主键索引

MySQL5.6版本之后使用的是 二级索引name匹配到符合的值以后再根据后面的 age 和 gend再过滤拿到最终的主键id去主键索引中回表 这样的好处就是回表的时候主键id会减少。

like 'kk%' 会走索引下推

<、>不走索引下推 因为MySQL认为 like 'kk%' 已经过滤掉很多数据了 所以走索引下推,而 <、>认为过滤以后还有很多数据 这样还不如全表查询还需要回表 所以不走索引并且不走索引下推

范围查询不会走索引下推,因为MySQL认为范围查询数据量会比较多,所以不走索引

查看MySQL 成本计算方式

set session optimizer_trace="enabled=on",end_markers_in_json=on; ‐‐开启trace
select * from employees where name > 'a' order by position;
SELECT * FROM information_schema.OPTIMIZER_TRACE;

第一句第三句固定写死 第二句是要执行的sql

{
 "steps": [
 {
 "join_preparation": { ‐‐第一阶段:SQL准备阶段,格式化sql
 "select#": 1,
 "steps": [
 {
 "expanded_query": "/* select#1 */ select `employees`.`id` AS `id`,`employees`.`name` AS `name`,`empl
oyees`.`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

group by

order by

使用最左前缀原则,这样会走索引 并且 exra是可以看到 走没走索引 如果走了索引就是 using index

比如有索引 name,age,gender

sql: select name,age from employ order by age;

上边 sql就是using filesort。代表是加载到内存或者硬盘排序。

sql: select name,age from employ order by name,age;

这个sql就是走的 using index 索引排序 因为索引本身就是排好序的。但是要最左前缀原则,不然不走索引。因为就变成无序了

sql: select name,age from employ where name = 'aa' order by age;

这个sql其实order by并没有最左前缀,因为第一个应该是name但是跳过了name。但是还是using index,因为name就是一个常量 aa ,MySQL会进行优化这样就是最左前缀了。

如果是 using filesort

代表可能是内存排序或者硬盘排序。

MySQL内存排序

因为使用的是using filesort排序,所以MySQL会先开辟出来一块内存 sort_buffer 默认1M,用来存放需要排序的数据

单双链路

mysql会通过max_length_for_sort_data

using_filesort 是将数据加载到sort_buffer中进行排序的,

单链路是将 需要排序的数据整行数据加载到sort_buffer,

双链路是将需要排序的数据主键和需要排序的字段加载到sort_buffer中进行排序,排完序再通过主键给主键索引中找到该数据。也是回表排序

mysql会通过max_length_for_sort_data 来判断使用什么排序。默认是1024字节,如果一行数据大于1024字节,那么就会走双链路排序,如果小于那就走单链路排序

单链路排序肯定比双链路排序快,因为不用回表,但是如果一行数据太大就比较占用内存所以使用双链路直接在主键和需要排序的字段。

如果sort_buffer 默认1M空间承受不了需要排序的数据,就会创建一个临时文件,先排序一部分放到文件中再取出一部分放到内存sort_buffer排序,最后合并成整个排好序的数据

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筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可 能会小很多。

分页查询

在使用分页查询时,比如会写

sql select * FROM employees LIMIT 9000,10 ORDER BY id;

因为id是主键,所以是有序的,order by还比较快,但是limit MySQL是查询出来9010条数据,扔掉9000条数据,这样查询是比较慢的

我们可以使用联合索引走索引覆盖

sql select id FROM employees LIMIT 9000,10 ORDER BY id;

但是如果我们查询并不是只要id 而是id、name、age、gender。但是如果这样就不能走 覆盖索引了。

优化方案:select * FROM employees emp inner join (select id FROM employees ORDER BY id LIMIT 9000,10 ) tem ON emp.id = tem.id

这样最外层的数据使用主键关联,走的主键索引肯定很快,里面的sql走的覆盖索引不需要回表完成优化

单位遇到的优化方案: 因为是使用的id进行排序取值 可以将id返回给前端,前端分页的时候将最后一个id作为参数带给后端。 比如最后一条id=9000

sql select * FROM employees WHERE id > 9000 ORDER BY id LIMIT 10;

如果只是limit 10 条数据,回表次数也不多,我理解是不需要再去联合。这样走了主键索引而且也每次取10条。

inner查询优化 挤走索引又查询全部字段

select * FROM employees emp inner join (select id FROM employees ORDER BY id LIMIT 9000,10 ) tem ON emp.id = tem.id

上边sql 中 子查询 只返回id可以走覆盖索引不需要回表,在根据主键id联合 走主键索引找到全部数据。

这种情况我理解是需要扫描数据多,导致回表次数多,使用改方案。

NLG和BLG

比如A 有 10万条数据 B 100条数据

sql: select * from A inner join B on A.id = B.id;

MySQL会根据 联合字段有没有走索引来判断走那种算法。 比如A表中id为索引,那么就走NLG,如果不为索引,就要走无序的磁盘扫描,每扫描一次A表就相当于扫描10万行记录。所以就会走BLG

NLG

也就是索引链接。是内嵌套连接

这条sql语句执行逻辑为:

for(select id from B){

select * from A where id = B.id

}

第一步: B中去一条数据

第二步: 拿到B数据的id字段给A表中通过主键id查询 主键索引很快,几乎相当于常量查询

第三步: 在A中找到的那条数据

上边可以看出,B表越小越好,因为B越小循环次数越少。所以上边这条sql语句适合小表在后,大表在前,小表先执行,大表后执行。但是如果没有索引MySQL会执行BLG算法

假设走索引id为主键索引,如果B表中 有100条数据,A表中有100000条数据。那么执行的时候,会先全表扫描B表加载在内存中,再逐一取出一条数据去主键索引中寻找数据 。

也就是第一次全表扫描需要扫描 100 条数据,第二步通过每一条数据给A表中主键查询,每查询一次都相当于100,那么为什么给A表查询一次相当于一次查询,不应该扫描100000数据找到和取出的数据id相等的那条数据吗?那是因为主键索引非常快,通过主键索引查询非常快,几乎是常量查询,所以每查询一次主键索引相当于查询一次常量。所以 100 条数据查询完了也就相当于查询了100次A表。所以整个查询只用了200

BLG

比如上边sql中id不是索引只是普通字段。会将B表中的100行记录全表扫描放到join_buffer中。全表扫描100次,再去扫描A中10万行记录,每扫描一条就去A中join_buffer中比对id是否相同,相同就纪录下来。又因为A在join_buffer是无序的,那么就会没扫描一次A就是相当于扫描100行记录取出id和A中该数据id相等的数据。而A有需要扫描10万次,所以一共也就是 扫描A 10万次 + B 100 条 * 10万次 + 全表扫描B加入到join_buffer的100条 = 1100100 一百多万次

如果没有索引但是走NLG算法是什么样子的

索引取到B表100数据加载到 join_buffer 扫描100记录

每取出一条B表一条记录就去A表查询id相等的数据,注意这里id是普通字段不是索引。所以每查询一次就是10万条数据中找到id和当前B中取到的这条数据相等的数据。也就是 取B表100条数据需要重复 给A表10万数据查询100次也就是需要 100 * 10万 = 1000万次。并且还是磁盘扫描这么多。肯定性能不高

exit和in

exits

exit 是 select * from A where exits (select 1 from B where A.id = B.id)

相当于:

for (select * from A){

select 1 from B where B.id = A.id;

}

从上边可以看出,A表数据越小越好,因为A表越少循环次数越少,查询次数也就越少。

所以exits适合前边表小,后面表大的。

in

select * from A where A.id in (select id from B)

相当于

for(select id from B){

select * from A where A.id = B.id;

}

显然这种想要查询次数少需要B表中数据少,因为B表中数据少,就会减少循环次数从而减少查询次数。

这种数据适合于子后面表小,前边表达的sql。

count(*)、count(1)、count(字段)、count(主键)

这四种其实都是走的二级索引,因为二级索引占用内存少

如果 count(字段) 字段是索引的话: count(*)约等于 count(1) > count(字段) > count(主键)

count(*) 做了优化,就是扫描的时候,记录行数,二级索引中 扫描到一行就是 +1 不用取到某一个字段

count(1) 也是扫描到一行就 +1 不用取到某一个字段

count(字段) 因为字段是索引所以会走二级索引,所以不用回表就能取到该字段,取到这个字段才会进行+1 因为要取到 这个字段才能进行 +1 所以会比 count(*) count(1)慢

count(id) 5.7版本之前是走的主键索引,之后做了优化也是二级索引。然后取出id值进行+1

为什么 count (id) 比同是走二级索引并且 count(字段) 要慢呢?个人理解因为 count要去非叶子节点取出id,而count(字段)是扫描一条数据就是一条数据 不找非叶子节点。就可以取出字段进行+1所以会快

如果字段不走索引:

count(*)约等于 count(1) > count(主键) > count(字段)

这时候就是扫描主键索引,因为字段没有索引肯定是最慢的。

个人学习笔记,如有错误欢迎大佬指正

你可能感兴趣的:(mysql)