慢 SQL 优化之索引的作用是什么?

这篇具有很好参考价值的文章主要介绍了慢 SQL 优化之索引的作用是什么?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

本文针对 MySQL 数据库的 InnoDB 存储引擎,介绍其中索引的实现以及索引在慢 SQL 优化中的作用。

本文主要讨论不同场景下索引生效与失效的原因。

慢SQL与索引的关系

慢SQL优化原则

数据库也是应用,MySQL 作为一种磁盘数据库,属于典型的 IO 密集型应用,并且随机 IO 比顺序 IO 更昂贵。

真实的慢 SQL 往往会伴随着大量的行扫描、临时文件排序,直接影响就是磁盘 IO 升高、CPU 使用率升高,正常 SQL 也变为了慢 SQL,对于应用来说就是大面积执行超时。

线上很多事故都与慢 SQL 有关,因此慢 SQL 治理已成为 DBA 与业务研发的共识。

慢 SQL 优化之索引的作用是什么?

慢SQL的优化原则为:减少数据访问量与减少计算操作

减少访问量:

•创建合适的索引

•减少不必要访问的列

•使用覆盖索引

•语句改写

•数据结转

减少计算操作:

•排序列加入索引

•适当的列冗余

•SQL 拆分

•计算功能拆分

可以将慢 SQL 优化的方法分为三类:

•查询优化

索引优化

•库表结构优化

其中索引是数据库中用来提升性能的最常用工具。

可是,为什么索引可以加快查询,索引一定可以加快查询吗?

索引的作用

要回答这个问题,可以对比没有索引与有索引时查询操作的性能差异。

在此之前,首先介绍下查询操作的处理流程。

查询操作可以分为以下两步:

•定位到记录所在的页

•从所在的页中定位到具体的记录

其中从页中定位记录的方法依赖每个页面中创建的Page Directory(页目录),因此关键在于如何定位页。

数据保存在磁盘上,数据处理发生在内存中,数据页是磁盘与内存之间交互的基本单位,也是 MySQL 管理存储空间的基本单位,大小默认为 16KB。

因此通常一次最少从磁盘中读取 16KB 的内容到内存中,一次最少把内存中的 16KB 内容刷新到磁盘中。

要理解索引的作用,需要首先明确没有索引时如何定位页。

没有索引时,由于每个页中的数据没有规律,因此无法快速定位记录所在的页,只能从第一个页沿双向链表向后遍历,也就是说需要遍历所有数据页依次判断是否满足查询条件。

简单来说,没有索引时每次查询都是全表扫描。

因此索引需要解决的主要问题就是实现每个数据页中数据有规律,具体是保证下一个数据页中用户记录的索引列值必须大于上一个页中用户记录的索引列值

索引是存储引擎用于快速查找的一种排序的数据结构

有索引时,优化器首先基于成本自动选择最优的执行计划,然后基于索引的有序性可以通过扫描更少的数据页定位到满足条件的数据。

具体原因与索引的数据结构有关,下面基于索引的数据结构介绍常见的索引生效与索引失效的场景。

索引

索引的数据结构

索引是一种以空间换时间思想的具体实现,用于加速查询。

MySQL 中由存储引擎层实现索引,InnoDB 存储引擎中基于 B+ 树实现,因此每个索引都是一棵 B+ 树。

索引用于组织页,页用于组织行记录。在介绍索引的结构之前首先介绍页的结构,如下图所示。

慢 SQL 优化之索引的作用是什么?

其中:

•每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,依赖行记录的Page Header中next_record属性实现,其中保存下一条记录相对于本条记录的地址偏移量;

•数据页之间组成一个双向链表,依赖数据页的File Header中FIL_PAGE_PREV和FIL_PAGE_NEXT属性实现,其中保存本页的上一个和下一个页的页号。

多个页通过树进行组织,其中保存用户数据与目录项。目录项中保存页的用户记录中主键的最小值与页号,从而保证下一个数据页中用户记录的主键值大于上一个页中用户记录的主键值

慢 SQL 优化之索引的作用是什么?

其中:

•用户数据保存在叶子节点,目录项保存在非叶子节点,每个节点中可能保存多个页;

•最上面的节点称为根节点,根节点的地址保存在内存的数据字典中;

•B+ 树的深度一般控制在 3 层以内,因此定位到单条记录不超过 3 次 IO

因此,页面和记录是排好序的,就可以通过二分法来快速定位查找

有索引时,查询操作变成了什么样呢?

•从 B+ 树的根节点出发,一层一层向下搜索目录项,由于上层节点保存的都是下层节点的最小值,因此可以快速定位到数据可能所在的页;

•如果数据页在缓存池中,直接从内存中获取,否则从磁盘加载到内存中;

•数据页内部二分查找定位满足条件的记录行。

慢 SQL 优化之索引的作用是什么?

索引保存的数据

索引中保存的数据与索引的类型有关。

索引可以分为两种类型:

•聚簇索引,主键索引。叶子节点中保存主键值+对应的完整行记录,目录项中保存主键最小值+页号。InnoDB 属于索引组织表,每张表都有聚簇索引,因此表必须有主键,表中行的物理顺序与索引的逻辑顺序相同;

•非聚簇索引,二级索引,在非主键的其他列上建的索引。叶子节点中保存索引列的值+对应的主键值,目录项中保存索引列最小值+对应的主键值+页号

慢 SQL 优化之索引的作用是什么?

介绍三个与索引性能相关的概念:

概念 explain.extra
覆盖索引 Using index
回表 Using where
索引下推 Using index condition

•覆盖索引,当二级索引中包含要查询的所有字段时,这个索引称为覆盖索引;

•回表,当二级索引中不包含要查询的所有字段时,就需要先通过二级索引查出主键索引,再通过主键索引查询二级索引中没有的其他列的数据,这个过程叫做回表;

•索引下推,用于优化使用二级索引从表中检索行的实现。条件过滤可以下推到存储引擎层进行,先由索引元组(index tuple)根据查询条件进行过滤,满足条件的前提下才回表,否则跳过,相当于延迟加载数据行,因此 ICP 可以降低回表次数与 IO 次数

正常情况下看不到二级索引中隐藏的主键,但实际上,如下所示查看锁信息,显示LOCK_DATA: '17118168721', 2,其中 2 就是二级索引中保存的主键值。

               ENGINE:INNODB
       ENGINE_LOCK_ID:140123070938328:14:7:4:140122972537552
ENGINE_TRANSACTION_ID:2032566
            THREAD_ID:157
             EVENT_ID:44
        OBJECT_SCHEMA: test_zk
          OBJECT_NAME: t_lock_test
       PARTITION_NAME:NULL
    SUBPARTITION_NAME:NULL
           INDEX_NAME: idx_uk_mobile
OBJECT_INSTANCE_BEGIN:140122972537552
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA:'17118168721',2

如下所示,工作中多次遇到研发创建二级索引时显式指定主键,实际上是不需要的,二级索引末尾自动保存主键。

alter table payable_unsettled 
add index idx_seller_no_recno_id(seller_no, receipt_no,id) using BTREE;

因此二级索引+主键与联合索引的相同点是依次排序,不同点是索引中保存的数据不同。

索引生效的场景

等值查询

线上环境多次遇到表没有创建二级索引,只有主键索引。

SQL

select sys_no,area_no,area_name,dc_no,dc_name,wh_no,wh_name, business_type,business_no,item_code,item_grade, item_result,item_remark,status, yn,ts,create_time,create_pin,update_time,update_pin
from
  evaluate_result
where
  yn =1
  and wh_no ='611-887-2'
  and business_no ='QNSYKF23020900000018'
  and create_pin ='13940137489'
orderby
  update_time desc;

# 执行用时
5 rows in set(7.311125 sec)

执行计划,显示全表扫描

慢 SQL 优化之索引的作用是什么?

表结构,显示查询字段无索引

mysql>show create table evaluate_result \G
***************************1.row***************************
       Table: evaluate_result
CreateTable:CREATE TABLE `evaluate_result`(
  `sys_no` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '内部主键',
  `wh_no` varchar(32) NOT NULL DEFAULT '' COMMENT'仓库编码',
  `business_no` varchar(20) NOT NULL DEFAULT '' COMMENT'调研业务主键',
   ...
   PRIMARY KEY(`sys_no`)
)ENGINE=InnoDB AUTO_INCREMENT=2007412 DEFAULT CHARSET=utf8 COMMENT='评价结果表'
1rowinset(0.00 sec)

优化方法:创建索引

alter table evaluate_result add index idx_wh_bus_no(wh_no,business_no);

执行计划

***************************1.row***************************
           id:1
  select_type: SIMPLE
        table: evaluate_result
   partitions:NULL
         type: ref
possible_keys: idx_wh_bus_no
          key: idx_wh_bus_no
      key_len:160
          ref: const,const
         rows:5
     filtered:1.00
        Extra:Using index condition;Using where;Using filesort
1rowinset,1 warning (0.00 sec)

# 执行用时
5 rows in set(0.01 sec)

其中:

•key_len: 160,表明联合索引的两个字段都用到了,(32+20) * 3+2 * 2 = 160。

等值查询索引生效的原因是相同值的数据组成单向链表,因此定位到满足条件的 5 行数据需要扫描的行数从 1377442 行降低到 5 行

范围查询

SQL

select
  id
from
  board_chute
where
  status=1
  and create_time <= date_sub(now(),interval 24 hour);

执行计划,显示全表扫描

***************************1.row***************************
           id:1
  select_type: SIMPLE
        table: board_chute
   partitions:NULL
         type:ALL
possible_keys: idx_create_time
          key:NULL
      key_len:NULL
          ref:NULL
         rows:407632
     filtered:5.00
        Extra:Using where
1rowinset,1 warning (0.00 sec)

查询字段有索引,但是索引失效

  KEY`idx_create_time`(`create_time`),

status 字段的区分度

mysql> select status,count(*) from board_chute group by status;
+--------+----------+
| status | count(*) |
+--------+----------+
|      0 |   407317 |
|      1 |     4309 |
+--------+----------+
2 rows in set (0.17 sec)

因此范围查询索引失效的原因是查看数据量大并且需要回表。

优化方法:创建联合索引实现覆盖索引

alter table board_chute add index idx_status_create_time(status, create_time);

执行计划,显示 Using index 表明用到了覆盖索引,不需要回表。

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: board_chute
   partitions: NULL
         type: range
possible_keys: idx_create_time,idx_status_create_time
          key: idx_status_create_time
      key_len: 8
          ref: NULL
         rows: 203816
     filtered: 10.00
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

范围查询索引生效的原因是叶子节点中除了保存索引,还保存指向下个节点的指针,因此遍历叶子节点就可以获得范围值

因此建议使用 between and 代替 in,如select * from T where k in (1,2,3,4,5);对应 5 次树的搜索,而select * from T where k between 1 and 5;对应 1 次树的搜索。

假设索引基于哈希表实现,可以通过散列函数将 key 值转换成一个固定的地址,如果发生哈希碰撞就在这个位置拉出一个链表。因此哈希表的优点是插入操作的速度快,根据 key 直接往后追加即可。但由于散列函数的离散特性,经过散列函数处理后的 key 将失去原有的顺序,所以哈希表无法满足范围查询,只适合等值查询。

注意上述索引生效的场景并非绝对成立,需要回表的记录越多,优化器越倾向于使用全表扫描,反之倾向于使用二级索引 + 回表的方式。

回表查询成本高有两点原因:

•需要使用到两个 B+ 树索引,一个二级索引,一个聚簇索引;

•访问二级索引使用顺序 I/O,访问聚簇索引使用随机 I/O。

因此有两条建议:

•建议为区分度高的字段创建索引,并且将区分度高的字段优先放在联合索引前面;

•建议优先使用覆盖索引,必须要回表时也需要控制回表的记录数,从而降低索引失效的风险。

索引失效的场景

违反最左匹配原则

SQL

select
  count(*)
from
  sort_cross_detail
where
  yn =1
  and org_id =3
  and site_type =16
  and site_code ='121671';

执行计划,显示全表扫描

慢 SQL 优化之索引的作用是什么?

尽管当前有联合索引 idx_site_type(SUB_TYPE, THIRD_TYPE, SITE_TYPE ),但由于查询条件中不包括 SUB_TYPE 字段,因此违反最左匹配原则,导致索引失效。

当前查询条件的多个字段区分度由高到低为 site_code、org_id、site_type。

慢 SQL 优化之索引的作用是什么?

优化方法:site_code 字段区分度很高,创建单列索引。

alter table sort_cross_detail add index `idx_site_code` (`SITE_CODE`);

执行计划

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: sort_cross_detail
   partitions: NULL
         type: ref
possible_keys: idx_site_code
          key: idx_site_code
      key_len: 99
          ref: const
         rows: 1336
     filtered: 0.10
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

其中:

•使用联合索引过程中可以通过执行计划中的 key_len 字段评估具体 SQL 使用到了联合索引中的几个字段;

•联合索引中页面和记录首先按照联合索引前面的列排序,如果该列值相同,再按照联合索引后边的列排序。

违反最左匹配原则导致索引失效的原因是只有当索引前面的列相同时,后面的列才有序

下面结合 innodb_ruby 工具解析 InnoDB 数据文件查看记录保存的顺序验证联合索引中索引前面的列不同时,后面的列可能无序。

准备测试数据。

mysql> show create table t_index \G
*************************** 1. row ***************************
       Table: t_index
Create Table: CREATE TABLE `t_index` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `age` int(11) DEFAULT '0',
  `name` varchar(10) DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `idx_age_name` (`age`,`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> insert into t_index(age, name) values(8, "Tom"),(8, "David"), (10, "Andy");
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> select * from t_index;
+----+------+-------+
| id | age  | name  |
+----+------+-------+
|  2 |    8 | David |
|  1 |    8 | Tom   |
|  3 |   10 | Andy  |
+----+------+-------+
3 rows in set (0.00 sec)

mysql> explain select * from t_index \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t_index
   partitions: NULL
         type: index
possible_keys: NULL
          key: idx_age_name
      key_len: 38
          ref: NULL
         rows: 3
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

其中:

•insert 时,三条记录按照 name 字段逆序;

•select 时,三条记录按照联合索引排序,并不是按照主键排序。

分别查看索引以及索引中保存的数据

[root@exps-test3 data]# innodb_space -s ibdata1 -T test_zk/t_index space-indexes
id          name                            root        fseg        fseg_id     used        allocated   fill_factor 
218         PRIMARY                         3           internal    1           1           1           100.00%     
218         PRIMARY                         3           leaf        2           0           0           0.00%       
219         idx_age_name                    4           internal    3           1           1           100.00%     
219         idx_age_name                    4           leaf        4           0           0           0.00%       
[root@exps-test3 data]# 
[root@exps-test3 data]# innodb_space -s ibdata1 -T test_zk/t_index -p 3 page-records
Record 127: (id=1) → (age=8, name="Tom")
Record 158: (id=2) → (age=8, name="David")
Record 191: (id=3) → (age=10, name="Andy")
[root@exps-test3 data]# 
[root@exps-test3 data]# innodb_space -s ibdata1 -T test_zk/t_index -p 4 page-records
Record 145: (age=8, name="David") → (id=2)
Record 127: (age=8, name="Tom") → (id=1)
Record 165: (age=10, name="Andy") → (id=3)

其中:

•主键与二级索引的根节点页号分别是 3 与 4;

•查看聚簇索引中保存的记录,按照主键排序;

•查看二级索引中保存的记录,联合索引中当 age 相同时,name 有序,age 不同时,name 无序。

上面提到,数据字典中保存表的根节点的地址,具体是 INFORMATION_SCHEMA.INNODB_SYS_INDEXES 系统表的 space 与 page_no 字段,分别保存表空间 ID 与根节点页号。

mysql> SELECT
  tables.name, indexs.space, indexs.name, indexs.page_no
FROM
  INFORMATION_SCHEMA.INNODB_SYS_TABLES as tables
  inner join INFORMATION_SCHEMA.INNODB_SYS_INDEXES as indexs on tables.table_id = indexs.table_id
WHERE
  tables.NAME = 'test_zk/t_index';
+-----------------+-------+--------------+---------+
| name            | space | name         | page_no |
+-----------------+-------+--------------+---------+
| test_zk/t_index |   106 | PRIMARY      |       3 |
| test_zk/t_index |   106 | idx_age_name |       4 |
+-----------------+-------+--------------+---------+
2 rows in set (0.00 sec)

其中 test_zk/t_index 表有两个索引,对应的根节点页号分别等于 3 与 4,与上面数据文件解析的结果一致。

order by limit

SQL,不建议使用 select *

select 
  * 
from 
  waybill_order_added_value_report_detail goodsInfo 
WHERE 
  goodsInfo.is_delete = 0 
  AND goodsInfo.org_no = '418' 
  AND goodsInfo.distribute_no = '636' 
  AND (
    goodsInfo.company_code = 'EBU4418046542406' 
    OR goodsInfo.company_name = 'EBU4418046542406'
  ) 
  AND goodsInfo.network_type = 1 
  AND goodsInfo.should_operator_time >= concat('2022-05-01', ' 00:00:00') 
  AND goodsInfo.should_operator_time <= concat('2022-05-31', ' 23:59:59') 
  AND goodsInfo.uniform_status = 0 
ORDER BY 
  goodsInfo.id DESC 
LIMIT 
  0, 20 \G
  
# 执行用时
2 rows in set (1 min 9.71 sec)

执行计划,主键全索引扫描,联合索引失效

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: goodsInfo
         type: index
possible_keys: idx_org_dc_operator_time,idx_operator_time_network
          key: PRIMARY
      key_len: 8
          ref: NULL
         rows: 16156
        Extra: Using where
1 row in set (0.00 sec)

表结构与查询条件

# 查询条件
WHERE 
  goodsInfo.org_no = '418' 
  AND goodsInfo.distribute_no = '636' 
  AND goodsInfo.should_operator_time >= concat('2022-05-01', ' 00:00:00') 
  AND goodsInfo.should_operator_time <= concat('2022-05-31', ' 23:59:59') 

# 索引
  KEY `idx_org_dc_operator_time` (`org_no`,`distribute_no`,`should_operator_time`),
  KEY `idx_operator_time_network` (`should_operator_time`,`network_type`)

将 limit 20 修改为 limit 30,SQL 如下所示。

select 
  * 
from 
  waybill_order_added_value_report_detail goodsInfo 
WHERE 
    ...
ORDER BY 
  goodsInfo.id DESC 
LIMIT 
  0, 30 \G

# 执行用时
2 rows in set (0.06 sec)

执行计划显示当改为 limit 30 时,联合索引生效。

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: goodsInfo
         type: range
possible_keys: idx_org_dc_operator_time,idx_operator_time_network
          key: idx_org_dc_operator_time
      key_len: 132
          ref: NULL
         rows: 19024
        Extra: Using index condition; Using where; Using filesort
1 row in set (0.00 sec)

可见,索引的选择与 limit n 的 n 值也有关系。

从现象上看,当 limit n 的 n 值变大时,SQL的执行反倒有可能变快了。

实际上,这是 MySQL 低版本中的 bug #97001,优化器认为排序是个昂贵的操作,因此在执行 order by id limit 这条 SQL 时,为了避免排序,并且认为当 limit n 的 n 很小时,全表扫描可以很快执行完,因此选择使用全表扫描,以避免额外的排序。

针对 MySQL 中 order by limit 或 group by limit 优化器选择错误索引的场景,常见的优化方法有四种:

•强制索引,通过 hint 固化执行计划,比如可以通过 force index 指定使用的索引,但是当条件发生变化时有可能失效,因此生产环境中不建议使用;

•prefer_ordering_index,5.7.33 中已修复该 bug,因此建议新申请时使用 5.7.33 及以上版本,存量低版本建议升级,建议优先使用该方法;

•联合索引,建议在合适的字段加联合索引, 增强可选索引的区分度,让优化器认为这种方式优于有序索引;

•order by (id+0),通过 trick 的方式欺骗优化器,由于 id 上进行了加法这种耗时操作,使优化器认为此时基于全表扫描的会更耗性能,因此选择基于成本选择的索引。

优化方法:order by (id+0)

select ...
ORDER BY goodsInfo.id+0 DESC
LIMIT 0, 20\G

执行计划

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: goodsInfo
         type: range
possible_keys: idx_org_dc_operator_time,idx_operator_time_network
          key: idx_org_dc_operator_time
      key_len: 132
          ref: NULL
         rows: 19024
        Extra: Using index condition; Using where; Using filesort
1 row in set (0.00 sec)

order by limit 导致索引失效的原因是当查询字段与排序字段不同时,如果使用查询字段的索引,排序字段将无序。优化器认为排序操作昂贵,因此优先使用排序字段的索引

隐式转换

字段类型不一致或字符集不一致时自动隐式转换将导致索引失效。

SQL

SELECT
  id
FROM
  base_operating_report
WHERE
  yn = 1
  and ec_code = 42
order by
  inbound_time desc;

执行计划,显示索引失效全表扫描

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: base_operating_report
   partitions: NULL
         type: ALL
possible_keys: idx_ecCode_transferCode
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 36524
     filtered: 1.00
        Extra: Using where; Using filesort
1 row in set, 3 warnings (0.00 sec)

查看警告信息,显示隐式转换导致索引失效

mysql> show warnings \G
*************************** 1. row ***************************
  Level: Warning
   Code: 1739
Message: Cannot use ref access on index 'idx_ecCode_transferCode' due to type or collation conversion on field 'ec_code'
*************************** 2. row ***************************
  Level: Warning
   Code: 1739
Message: Cannot use range access on index 'idx_ecCode_transferCode' due to type or collation conversion on field 'ec_code'
*************************** 3. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `dms_uat`.`base_operating_report`.`id` AS `id` from `dms_uat`.`base_operating_report` where ((`dms_uat`.`base_operating_report`.`yn` = 1) and (`dms_uat`.`base_operating_report`.`ec_code` = 42)) order by `dms_uat`.`base_operating_report`.`inbound_time` desc
3 rows in set (0.00 sec)

表结构

# 索引信息
KEY `idx_ecCode_transferCode` (`ec_code`, `transfer_code`)

# 字段类型
`ec_code` varchar(64) DEFAULT NULL COMMENT '仓库编码'

优化方法:将参数中的数值类型转换成字符串

SELECT
  id
FROM
  base_operating_report
WHERE
  yn = 1
  and ec_code = '42'
order by
  inbound_time desc;

执行计划,显示索引生效。

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: base_operating_report
   partitions: NULL
         type: ref
possible_keys: idx_ecCode_transferCode
          key: idx_ecCode_transferCode
      key_len: 195
          ref: const
         rows: 443
     filtered: 10.00
        Extra: Using index condition; Using where; Using filesort
1 row in set, 1 warning (0.00 sec)

隐式转换导致索引失效的原因是字段上有函数,而函数并不一定是单调函数,因此会破坏索引本身的有序性

IN

SQL

select
  IFNULL(count(DISTINCT (awi.id)), 0)
from
  tc_attorney_waybill_info awi
where
  awi.is_delete = 0
  and awi.cur_transit_center_code in (
'2008' , '2052' , '2053' , '2054' , '2055' , '2056' , '2057' , '2058' , '2059' , '2061' , '2064' , '2069' , '2079' , '2084' , '2085' , '2094' , '2171' , '2201' , '2202' , '2207' , '2216' , '2258' , '2292' , '2301' , '2311' , '2324' , '2332' , '2334' , '2336' , '2349' , '2354' , '2355' , '2359' , '2367' , '2369' , '2373' , '2381' , '2385'
  )
  and awi.send_time >= '2022-10-20 00:00:00'
  and awi.send_time <= '2022-11-18 23:59:59'
  and awi.split_send_package_times > 0
  and awi.first_split_type = 1;

# 执行用时
1 rows in set (1.309 sec)

执行计划,显示索引失效全表扫描

慢 SQL 优化之索引的作用是什么?

trace,显示当前的索引中全表扫描的成本最低,因此索引失效。

				"analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "idx_transit_code_waybill_code",
                        "ranges": [
                          "2008 <= cur_transit_center_code <= 2008",
                          "2052 <= cur_transit_center_code <= 2052",
                          "2053 <= cur_transit_center_code <= 2053",
                          "2054 <= cur_transit_center_code <= 2054",
                          "2055 <= cur_transit_center_code <= 2055",
                          "2056 <= cur_transit_center_code <= 2056",
                          "2057 <= cur_transit_center_code <= 2057",
                          "2058 <= cur_transit_center_code <= 2058",
                          "2059 <= cur_transit_center_code <= 2059",
                          "2061 <= cur_transit_center_code <= 2061",
                          "2064 <= cur_transit_center_code <= 2064",
                          "2069 <= cur_transit_center_code <= 2069",
                          "2079 <= cur_transit_center_code <= 2079",
                          "2084 <= cur_transit_center_code <= 2084",
                          "2085 <= cur_transit_center_code <= 2085",
                          "2094 <= cur_transit_center_code <= 2094",
                          "2171 <= cur_transit_center_code <= 2171",
                          "2201 <= cur_transit_center_code <= 2201",
                          "2202 <= cur_transit_center_code <= 2202",
                          "2207 <= cur_transit_center_code <= 2207",
                          "2216 <= cur_transit_center_code <= 2216",
                          "2258 <= cur_transit_center_code <= 2258",
                          "2292 <= cur_transit_center_code <= 2292",
                          "2301 <= cur_transit_center_code <= 2301",
                          "2311 <= cur_transit_center_code <= 2311",
                          "2324 <= cur_transit_center_code <= 2324",
                          "2332 <= cur_transit_center_code <= 2332",
                          "2334 <= cur_transit_center_code <= 2334",
                          "2336 <= cur_transit_center_code <= 2336",
                          "2349 <= cur_transit_center_code <= 2349",
                          "2354 <= cur_transit_center_code <= 2354",
                          "2355 <= cur_transit_center_code <= 2355",
                          "2359 <= cur_transit_center_code <= 2359",
                          "2367 <= cur_transit_center_code <= 2367",
                          "2369 <= cur_transit_center_code <= 2369",
                          "2373 <= cur_transit_center_code <= 2373",
                          "2381 <= cur_transit_center_code <= 2381",
                          "2385 <= cur_transit_center_code <= 2385"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 1328061,
                        "cost": 1.59e6,
                        "chosen": false,
                        "cause": "cost"
                      },
                      {
                        "index": "idx_outstoretime",
                        "ranges": [
                          "0x99ae280000 <= send_time <= 0x99ae657efb"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 1597295,
                        "cost": 1.92e6,
                        "chosen": false,
                        "cause": "cost"
                      }
                    ],
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    }
                  }
                }
              }
            ]
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`tc_attorney_waybill_info` `awi`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 3194590,
                      "access_type": "scan",
                      "resulting_rows": 3.19e6,
                      "cost": 677027,
                      "chosen": true
                    }
                  ]
                },

优化方法:

•创建一个更多查询字段的联合索引,减少回表次数;

•缩小查询的时间范围,因为查询的数据量比较大,而且用到的字段比较多,导致回表成本高。

IN 导致索引失效的原因是符合条件的数据量过大导致回表成本高于全表扫描

分组字段无索引

提数,创建唯一键之前分组查询是否有重复数据。

SQL

select
  operate_id,
  waybill_code,
  private_call_id
from
  tos_resource.courier_call_out_record_0
group by
  operate_id,
  waybill_code,
  private_call_id
having
  count(*) > 1;

执行计划,显示全表扫描

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: courier_call_out_record_0
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1470107
     filtered: 100.00
        Extra: Using temporary; Using filesort
1 row in set, 1 warning (0.01 sec)

分组字段无联合索引,有两个索引覆盖三个分组字段,因此索引无法使用。

  KEY `courier_call_out_record_0_operate_id_IDX` (`operate_id`,`waybill_code`),
  KEY `courier_call_out_recourd_0_waybill_code` (`waybill_code`)

优化方法:给分组字段创建联合索引

alter table courier_call_out_record_0 
add index `courier_call_out_record_0_composite_key` (`operate_id`,`waybill_code`,`private_call_id`);

执行计划

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: courier_call_out_record_0
   partitions: NULL
         type: index
possible_keys: courier_call_out_record_0_composite_key
          key: courier_call_out_record_0_composite_key
      key_len: 907
          ref: NULL
         rows: 2230391
     filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.00 sec)

分组字段无联合索引导致全表扫描的原因是分组时需要先排序,因此只有当分组字段在同一个索引中时才可以保证有序

索引的优缺点

索引的代价

通过以上分析可以发现索引不是万能的,实际上有时候索引甚至会有副作用。

创建索引的代价可以分为两类:

•空间代价,索引需要占用磁盘空间,并且删除索引并不会立即释放空间,因此无法通过删除索引的方式降低磁盘使用率;

•时间代价,有的时候会发现创建索引后导致写入变慢,原因是每次数据写入后还需要对该记录按照索引排序。因此经常更新的列不建议创建索引。

可见,索引的优点是可以加快查询速度,缺点是占用内存与磁盘空间,同时减慢了插入与更新操作的速度。

因此,B+ Tree 适用于读多写少的业务场景,相对应的 LSM-Tree 适用于写多读少的业务场景,原因是每次数据写入对应一条日志追加写入磁盘文件,用顺序 IO 代替了随机 IO。

索引使用的建议

关于索引的使用有以下几点建议:

•建议给区分度高的字段创建索引;

•建议删除冗余索引,否则优化器可能使用 index_merge 导致选择到错误的索引;

•不建议使用强制索引,比如当数据量或统计信息发生变化时,强制索引不一定最优。

下面测试下与索引相关的两个操作:

•如果 SQL 中强制指定已删除的索引,SQL 执行会报错吗?

•如果删除字段,索引也会自动删除吗?

准备数据

mysql> create table t_index_drop like t_index;
Query OK, 0 rows affected (0.02 sec)

mysql> show create table t_index_drop \G
*************************** 1. row ***************************
       Table: t_index_drop
Create Table: CREATE TABLE `t_index_drop` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `age` int(11) DEFAULT '0',
  `name` varchar(10) DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `idx_age_name` (`age`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

如果 SQL 中强制指定已删除的索引,SQL 将直接报错,生产环境中遇到过相关案例。

mysql> alter table t_index_drop drop index idx_age_name;
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> select * from t_index_drop force index(idx_age_name);
ERROR 1176 (42000): Key 'idx_age_name' doesn't exist in table 't_index_drop'

如果删除字段,索引也会自动删除。

mysql> alter table t_index_drop add index idx_age(age);
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> show create table t_index_drop \G
*************************** 1. row ***************************
       Table: t_index_drop
Create Table: CREATE TABLE `t_index_drop` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `age` int(11) DEFAULT '0',
  `name` varchar(10) DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `idx_age_name` (`age`,`name`),
  KEY `idx_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

mysql> alter table t_index_drop drop column age;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> show create table t_index_drop \G
*************************** 1. row ***************************
       Table: t_index_drop
Create Table: CREATE TABLE `t_index_drop` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `idx_age_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

基于优化器选择索引有可能选错索引导致性能下降,而使用强制索引可能导致 SQL 执行直接报错。

结论

慢SQL的优化原则是减少数据访问量与减少计算操作,其中索引是数据库中用来提升性能的最常用工具。

索引是一种用于快速查找的一种排序的数据结构,基于以空间换时间的思想实现。

MySQL 中由存储引擎层实现索引,InnoDB 存储引擎中基于 B+ 树实现,因此每个索引都是一棵 B+ 树。

索引用于组织页,页用于组织行记录。

其中:

•每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,依赖行记录的Page Header中next_record属性实现,其中保存下一条记录相对于本条记录的地址偏移量;

•数据页之间组成一个双向链表,依赖数据页的File Header中FIL_PAGE_PREV和FIL_PAGE_NEXT属性实现,其中保存本页的上一个和下一个页的页号。

查询操作可以分为以下两步:

•定位到记录所在的页

•从所在的页中定位到具体的记录

对比没有索引与有索引时查询操作的性能差异:

•没有索引时每次查询都是全表扫描;

•有索引时从 B+ 树的根节点出发,一层一层向下搜索目录项,由于上层节点保存的都是下层节点的最小值,因此可以快速定位到数据可能所在的页。

关于索引失效的场景总结以下两点:

•索引的本质是有序的数据结构,因此破坏索引有序性的操作都有可能导致索引失效或部分生效;

•回表成本较高,因此优先使用覆盖索引,必须要回表时也需要控制回表的记录数,从而降低索引失效的风险。

参考教程

•《MySQL 是怎样运行的:从根儿上理解 MySQL》

•MySQL工具之innodb_ruby:探究InnoDB存储结构的利器

•你管这破玩意叫B+树?

作者:京东物流 张凯

来源:京东云开发者社区文章来源地址https://www.toymoban.com/news/detail-474177.html

到了这里,关于慢 SQL 优化之索引的作用是什么?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MySQL~索引的优缺点是什么?有哪些优化索引的方法?

    优点:提高查询记录的速度。 缺点: 需要占用空间,索引是一种用空间换时间的做法 创建索引和维护索引都需要消耗时间,会降低表的增删查改效率,因为每次进行增删查改,都需要对索引进行维护,需要消耗时间 经常作为查询条件的字段 ,如果需要同时查找多个字段,

    2024年02月16日
    浏览(46)
  • MySQL高级篇复盘笔记(一)【存储引擎、索引、SQL优化、视图、触发器、MySQL管理】

    ❤ 作者主页:欢迎来到我的技术博客😎 ❀ 个人介绍:大家好,本人热衷于 Java后端开发 ,欢迎来交流学习哦!( ̄▽ ̄)~* 🍊 如果文章对您有帮助,记得 关注 、 点赞 、 收藏 、 评论 ⭐️⭐️⭐️ 📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉 连接层

    2024年02月06日
    浏览(73)
  • 如何针对慢查询的sql进行优化?

    优化慢查询的 SQL 可以从多个方面入手,以下是一些常用的优化方式和示例: 在表中添加合适的索引可以显著提升查询效率。可以通过 EXPLAIN 命令来查看查询计划,判断是否使用了索引,如果没有使用索引,就需要考虑添加索引。 示例: 如果查询计划中 type 列为 ALL,说明没

    2024年02月15日
    浏览(50)
  • 【MySql】MySql索引的作用&&索引的理解

    【MySql】MySql索引的作用索引的理解 索引是与效率挂钩的,所以没有索引,可能会存在问题 索引:提高数据库的性能,索引是物美价廉的东西了。不用加内存,不用改程序,不用调sql,只要执行正确的 create index ,查询速度就可能提高成百上千倍。但是天下没有免费的午餐,

    2024年02月09日
    浏览(37)
  • 【后端面经】MySQL主键、唯一索引、联合索引的区别和作用

    目录 0. 简介 1. 主键 2. 唯一索引 3. 联合索引 4. 索引对数据库操作的影响 5. 其他索引 5.1 普通索引 5.2 全文索引 5.3 前缀索引 6. 总结 7. 参考资料 索引是一类特殊的 文件 ,用来存储检索信息,使数据库查找更加快速。 主键是一类特殊的唯一索引,选择某一列元素作为主键,用

    2024年02月09日
    浏览(44)
  • SQL查询优化---单表使用索引及常见索引失效优化

    系统中经常出现的sql语句如下: 优化后 建立索引前 索引后 如果系统经常出现的sql如下: 或者 那原来的idx_age_deptid_name 还能否正常使用? 如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。 如果系统经常出现的sql如下: 那么

    2024年02月08日
    浏览(57)
  • SQL性能优化-索引

    1)索引失效 索引分为单索、复合索引。 四种创建索引方式 create index index_name on user (name); create index index_name_2 on user(id,name,email); 2)查询语句较烂 3)关联查询太多join,sql设计不合理 4)服务器问题。 explain可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 是如何处理

    2024年01月21日
    浏览(46)
  • DB索引&B+树&SQL优化

    数据库的索引就像一本书的目录,查数据快人一步,快速定位,精准打击! 官方介绍索引是帮助MySQL高效获取数据的数据结构。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度。 一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往

    2024年04月28日
    浏览(28)
  • mysql织梦索引优化之MySQL Order By索引优化

    在一些情况下,MySQL可以直接使用索引来满足一个ORDER BY 或GROUP BY 子句而无需做额外的排序。尽管ORDER BY 不是和索引的顺序准确匹配,索引还是可以被用到,只要不用的索引部分和所有的额外的ORDER BY 字段在WHERE 子句中都被包括了。 使用索引的MySQL Order By 下列的几个查询都会

    2024年02月04日
    浏览(47)
  • 数据库优化:探索 SQL 中的索引

    推荐:使用 NSDT场景编辑器 助你快速搭建可编辑的3D应用场景 在一本书中搜索特定主题时,我们将首先访问索引页面(该页面位于该书的开头),并找到包含我们感兴趣的主题的页码。现在,想象一下在没有索引页的书中找到特定主题是多么不方便。为此,我们必须搜索书中

    2024年02月14日
    浏览(64)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包