MySQL 索引优化实践(上)

简介: MySQL 索引优化实践

案例


创建表的 DDL


# DDL 语句
CREATE TABLE `employees` (
  `id` INT(10) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '姓名' COLLATE 'utf8_general_ci',
  `age` INT(10) NOT NULL DEFAULT '0' COMMENT '年龄',
  `position` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '职位' COLLATE 'utf8_general_ci',
  `hire_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_name_age_position` (`name`, `age`, `position`) USING BTREE,
  INDEX `idx_age` (`age`) USING BTREE
)
COMMENT='员工记录表';
# 随机数(随机生成员工年龄)
drop function if exists `rand_num`;
delimiter ;;
create function `rand_num`(
    `start_num` integer,
    `end_num` integer
)
    returns int
    comment ''
begin
    return floor(start_num + rand() * (end_num - start_num + 1));
end ;;
# 随机字符串函数
drop function if exists `rand_str`;
delimiter ;;
create
    definer = `root`@`localhost` function `rand_str`(
    `n` int
)
    returns varchar(255)
begin
    declare chars_str varchar(100) default 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz';
    declare return_str varchar(100) default '';
    declare i int default 0;
    while i < n
        do
            set return_str = concat(return_str, substring(chars_str, floor(1 + rand() * 52), 1));
            set i = i + 1;
        end while;
    return return_str;
end ;;
# 生成测试数据
drop procedure if exists `insert_emp`;
delimiter ;;
create procedure `insert_emp`(
    in `max_num` int(10)
)
begin
    declare i int default 0;
    set autocommit = 0;
    repeat
        set i = i+1;
        insert into `employees` (`name`, `age`, `position`, `hire_time`)
        values (rand_str(6), rand_num(20, 40), 'dev', now());
    until i = max_num
        end repeat;
    commit;
end ;;
delimiter ;
call insert_emp(10000);


索引优化


索引下推


对于辅助的联合索引(name,age,position),正常情况按照最左前缀原则,SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager' 这种情况只会走name字段索引,因为根据name字段过滤完,得到的索引行里的age和position是无序的,无法很好的利用索引。 在MySQL5.6之前的版本,这个查询只能在联合索引里匹配到名字是 'LiLei' 开头的索引,然后拿这些索引对应的主键逐个回表,到主键索引上找出相应的记录,再比对ageposition这两个字段的值是否符合。


MySQL 5.6引入了索引下推优化,可以在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数。使用了索引下推优化后,上面那个查询在联合索引里匹配到名字是** 'LiLei' 开头的索引之后,同时还会在索引里过滤ageposition**这两个字段,拿着过滤完剩下的索引对应的主键id再回表查整行数据。


索引下推会减少回表次数,对于innodb引擎的表索引下推只能用于二级索引,innodb的主键索引(聚簇索引)树叶子节点上保存的是全行数据,所以这个时候索引下推并不会起到减少查询全行数据的效果。


为什么范围查找Mysql没有用索引下推优化?估计应该是Mysql认为范围查找过滤的结果集过大,like KK% 在绝大多数情况来看,过滤后的结果集比较小,所以这里Mysql选择给 like KK% 用了索引下推优化,当然这也不是绝对的,有时like KK% 也不一定就会走索引下推。


常见的几种场景


1、联合索引的第一个字段是范围查找不会走索引


EXPLAIN SELECT * FROM employees WHERE name > 'SAN ZHANG' AND age = 22 AND position ='dev';


image.png


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


2、强制走索引


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


image.png


结论:虽然使用了强制走索引让联合索引第一个字段范围查找也走了索引,扫描的行 rows 看上去少了一点,但是最终查找效率不一定比全表扫描搞,因为回表效率不高 做一个小实验:


# 关闭查询缓存
set global query_cache_size=0;
# 执行时间 0.1 秒
SELECT * FROM employees WHERE name > 'SAN ZHANG';
# 执行时间 0.15 秒
SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'SAN ZHANG';;


3、覆盖索引优化


EXPLAIN SELECT name,age,position FROM employees WHERE name > 'SAN ZHANG' AND age = 22 AND position ='dev';

image.pngimage.png


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


EXPLAIN SELECT name,age,position FROM employees WHERE name in ('SAN ZHANG', 'SI Li', 'MAZI WQNAG', 'LIU ZHAO') AND age = 22 AND position ='dev';


image.png


EXPLAIN SELECT name,age,position FROM employees WHERE 
(name = 'SAN ZHANG' or name = 'SI Li' or name = 'MAZI WQNAG' or name = 'LIU ZHAO') 
AND age = 22 AND position ='dev';


image.png


创建一张 employees_temp 表里面就保留少量几条记录


CREATE TABLE `employees_temp` (
  `id` INT(10) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '姓名' ,
  `age` INT(10) NOT NULL DEFAULT '0' COMMENT '年龄',
  `position` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '职位' ,
  `hire_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_name_age_position` (`name`, `age`, `position`) USING BTREE
)
COMMENT='员工记录表';
insert into `employees_temp`(name, age, position) values ('SAN ZHANG', 23, 'dev');
insert into `employees_temp`(name, age, position) values ('SI Li', 23, 'dev');
insert into `employees_temp`(name, age, position) values ('LIU ZHAO', 26, 'dev');
insert into `employees_temp`(name, age, position) values ('LING AI', 38, 'manager');


EXPLAIN SELECT * FROM employees_temp WHERE name in ('SAN ZHANG', 'SI Li', 'MAZI WQNAG', 'LIU ZHAO') AND age = 22 AND position ='dev';


image.png


EXPLAIN SELECT * FROM employees_temp WHERE (name = 'SAN ZHANG' or name = 'SI Li' or name = 'MAZI WQNAG' or name = 'LIU ZHAO') AND age = 22 AND position ='dev';


image.png


5、like 'SAN%' 一般都会走索引


EXPLAIN SELECT name,age,position FROM employees WHERE name like 'SAN%' AND age = 22 AND position ='dev';


image.png


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


image.png


选择合适的索引


索引分析案例


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


image.png


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


EXPLAIN select name,age,position from employees where name > 'a' ; 


image.png


EXPLAIN select * from employees where name > 'zzz' ;


image.png


Trace 工具使用


对于上面这两种 name>'a' 和 name>'zzz' 的执行结果,mysql最终是否选择走索引或者一张表涉及多个索引,mysql最终如何选择索引,我们可以用trace工具来一查究竟,开启trace工具会影响mysql性能,所以只能临时分析sql使用,用完之后立即关闭trace工具用法:


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;
查看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": 120085,  --扫描行数
                    "cost": 24372    --查询成本
                  } /* 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": 60042,           --索引扫描的行数
                        "cost": 72051,           --索引使用成本
                        "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": 120085,
                      "access_type": "scan",  --访问类型:为 scan, 全表扫描
                      "resulting_rows": 120085,
                      "cost": 24370,
                      "chosen": true,         --确定选择
                      "use_tmp_table": true
                    }
                  ] /* considered_access_paths */
                } /* best_access_path */,
                "condition_filtering_pct": 100,
                "rows_for_plan": 120085,
                "cost_for_plan": 24370,
                "sort_cost": 120085,
                "new_cost_for_plan": 144455,
                "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",
              "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": [
          {
            "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": 120003,
              "examined_rows": 120003,
              "number_of_tmp_files": 34,
              "sort_buffer_size": 262056,
              "sort_mode": "<sort_key, packed_additional_fields>"
            } /* filesort_summary */
          }
        ] /* steps */
      } /* join_execution */
    }
  ] /* steps */
}
结论:全表扫描的成本低于索引扫描,所以mysql最终选择全表扫描
select * from employees where name > 'zzz' order by position;
SELECT * FROM information_schema.OPTIMIZER_TRACE;
查看trace字段可知索引扫描的成本低于全表扫描,所以mysql最终选择索引扫描
set session optimizer_trace="enabled=off";    --关闭trace



相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
存储 SQL 关系型数据库
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
|
存储 关系型数据库 MySQL
MySQL数据库索引的数据结构?
MySQL中默认使用B+tree索引,它是一种多路平衡搜索树,具有树高较低、检索速度快的特点。所有数据存储在叶子节点,非叶子节点仅作索引,且叶子节点形成双向链表,便于区间查询。
289 4
|
存储 关系型数据库 MySQL
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
|
SQL 缓存 关系型数据库
MySQL 慢查询是怎样优化的
本文深入解析了MySQL查询速度变慢的原因及优化策略,涵盖查询缓存、执行流程、SQL优化、执行计划分析(如EXPLAIN)、查询状态查看等内容,帮助开发者快速定位并解决慢查询问题。
496 0
|
11月前
|
缓存 关系型数据库 MySQL
降低MySQL高CPU使用率的优化策略。
通过上述方法不断地迭代改进,在实际操作中需要根据具体场景做出相对合理判断。每一步改进都需谨慎评估其变动可能导致其他方面问题,在做任何变动前建议先在测试环境验证其效果后再部署到生产环境中去。
405 6
|
存储 SQL 关系型数据库
MySQL 核心知识与索引优化全解析
本文系统梳理了 MySQL 的核心知识与索引优化策略。在基础概念部分,阐述了 char 与 varchar 在存储方式和性能上的差异,以及事务的 ACID 特性、并发事务问题及对应的隔离级别(MySQL 默认 REPEATABLE READ)。 索引基础部分,详解了 InnoDB 默认的 B+tree 索引结构(多路平衡树、叶子节点存数据、双向链表支持区间查询),区分了聚簇索引(数据与索引共存,唯一)和二级索引(数据与索引分离,多个),解释了回表查询的概念及优化方法,并分析了 B+tree 作为索引结构的优势(树高低、效率稳、支持区间查询)。 索引优化部分,列出了索引创建的六大原则
319 2
|
存储 SQL 关系型数据库
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
|
存储 关系型数据库 MySQL
MySQL覆盖索引解释
总之,覆盖索引就像是图书馆中那些使得搜索变得极为迅速和简单的工具,一旦正确使用,就会让你的数据库查询飞快而轻便。让数据检索就像是读者在图书目录中以最快速度找到所需信息一样简便。这样的效率和速度,让覆盖索引成为数据库优化师傅们手中的尚方宝剑,既能够提升性能,又能够保持系统的整洁高效。
403 9
|
机器学习/深度学习 关系型数据库 MySQL
对比MySQL全文索引与常规索引的互异性
现在,你或许明白了这两种索引的差异,但任何技术决策都不应仅仅基于理论之上。你可以创建你的数据库实验环境,尝试不同类型的索引,看看它们如何影响性能,感受它们真实的力量。只有这样,你才能熟悉它们,掌握什么时候使用全文索引,什么时候使用常规索引,以适应复杂多变的业务需求。
353 12
|
存储 SQL 关系型数据库
MySQL 动态分区管理:自动化与优化实践
本文介绍了如何利用 MySQL 的存储过程与事件调度器实现动态分区管理,自动化应对数据增长,提升查询性能与数据管理效率,并详细解析了分区创建、冲突避免及实际应用中的关键注意事项。
505 0

推荐镜像

更多