文章 62
浏览 15135
Mysql索引优化实战(一)

Mysql索引优化实战(一)

背景

MySQL 成为了业界主流的数据库存储,随着公司的发展和业务的复杂性,必须需要了解和掌握一些 MySQL 优化技巧

以下是结合自己的实践学习和网上的资料汇总成的一些场景

前置准备

以下 MySQL 版本是 5.7 执行所产生的效果,不排除后续 MySQL 版本升级,MySQL 进行了优化

示例表

首先创建一个 employees,插入 100000 条数据,如果以下 SQL,有可能因为 MySQL 版本不一致,导致无法执行,可以用 navicat 自动数据生成

  • navicat 数据生成

    image.png

    image.png

image.png

  • 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='员工记录表';

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('xiaohu',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';

image.png

虽然查询的字段为 name ,age position 满足最左匹配原则,但是第一个字段 name 后面是范围查询,explain 查询没有走索引

结论:联合索引第一个字段就用范围查找不会走索引,MySQL 内部可能觉得第一个字段就用范围,结果集应该很大,回表效率不高,还不如就全表扫描

2.强制走索引

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

image.png

结论: 虽然 SQL 语句加了 force index**(idx_name_age_position)**让它强制走索引,通过两个 SQL 语句对比,发现 rows 行数是减少了,type 是走索引了,但是最终扫描的效率不一定比全表快,因为产生了回表

做个实验:

-- 关闭查询缓存
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';

image.png

image.png

3.覆盖索引优化

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

image.png

结论: 查询 SQL 尽量不要随便用 select * 尽可能查询有索引的字段,不用执行回表

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

FAQ 😄当我们使用in和or的时候,一定不会走索引或者一定会走索引吗???

EXPLAIN SELECT * FROM employees WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
  • 数据量大的时候
    image.png

    image.png

发现用 in 和 or 的时候走了索引

  • 数据量少的时候
    我们新建一个 employees_temp 表,只保留 3 条数据试试

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

    image.png

发现数据量少的时候没有走索引

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

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

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

结论: like KK% 大概率会走索引,%kk% 不会走索引,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 的主键索引(聚簇索引)树叶子节点上保存的是全行数据,所以这个时候索引下推并不会起到减少查询全行数据的效果。

2.Mysql 如何选择合适的索引

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

如果用 name 索引需要遍历 name 字段联合索引树,然后还需要根据遍历出来的主键值去主键索引树里再去查出最终数据,成本比全表扫描还高,可以用覆盖索引优化,这样只需要遍历 name 字段的联合索引树就能拿到所有结果

3.trace 工具

有时候我们想知道,MySQL 最终是否选择走索引或者一张表涉及多个索引,MySQL 最终如何选择索引,我们可以用 trace 工具来一查究竟,开启 trace 工具会影响 MySQL 性能,所以只能临时分析 SQL 使用,用完之后立即关闭

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:

image.png

分析:

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

  • case2:

image.png

分析:

从 explain 的执行结果来看:key_len=74,查询使用了 name 索引,由于用了 position 进行排序,跳过了 age,出现了 Using filesort。

  • case3:

image.png

分析:

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

  • case4:
    image.png

分析:

和 Case 3 中 explain 的执行结果一样,但是出现了 Using filesort,因为索引的创建顺序为 name,age,position,但是排序的时候 age 和 position 颠倒位置了。

  • case5:
    image.png

分析:

与 Case 4 对比,在 Extra 中并未出现 Using filesort,因为 age 为常量,在排序中被优化,所以索引未颠倒,不会出现 Using filesort。

  • case6:
    image.png

分析:

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

优化总结:

  • 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 文件排序原理详解

  • 单路排序:是一次性取出满足条件行的所有字段,然后在 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 ,那么使用 双路排序模·式

索引设计原则

说了这么多,也举了相关的实例,基本上知道怎么进行sql优化了,那如何建立索引呢
以下是汇总的网上一些资料和自己经验所得

  • 代码先行,索引后上

不知大家一般是怎么给数据表建立索引的,是建完表马上就建立索引吗?

这其实是不对的,一般应该等到主体业务功能开发完毕,把涉及到该表相关 SQL 都要拿出来分析之后再建立索引。

  • 联合索引尽量覆盖条件

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

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

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

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

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

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

尽量对字段类型较小的列设计索引,比如说什么 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 也是同理。所以这里大家要对前缀索引有一个了解。

  • where 与 order by 冲突时优先 where

在 where 和 order by 出现索引设计冲突时,到底是针对 where 去设计索引,还是针对 order by 设计索引?到底是让 where 去用上索引,还是让 order by 用上索引?

一般这种时候往往都是让 where 条件去使用索引来快速筛选出来一部分指定的数据,接着再进行排序。

因为大多数情况基于索引进行 where 筛选往往可以最快速度筛选出你要的少部分数据,然后做排序的成本可能会小很多。

  • 基于慢 SQL 查询做优化

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

关于慢 SQL 查询不清楚的可以参考这篇文章:https://blog.csdn.net/qq_40884473/article/details/89455740

结尾

好了,这篇博客到此也就结束了,MySQL 优化是个复杂和需要不断积累的经验,这里只是抛砖引玉,不能照搬,具体的 SQL 优化,需要结合你自己的业务场景来进行优化,所有的技术脱离业务都是耍流氓,hhhhh,需要不断学习了解,emmm,我也是这么想的 🎉 🎉


标题:Mysql索引优化实战(一)
作者:xiaohugg
地址:https://xiaohugg.top/articles/2023/10/18/1697616041460.html

人民有信仰 民族有希望 国家有力量