MySQL索引你用对了吗?

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 本文从遇到的问题出发,分析了tddl优化器、MySQL索引、分表拆分键的选择相关知识。

背景


最近由于应用需要进行多租户改造,对监控盯的较紧。发现了应用的一些问题,应用的hsf consumer成功率,不是一直都是100,偶尔出现99.99的情况,进一步查应用日志发现,是因为慢sql导致服务超时失败。
image.png
定位到具体语句


(
  select
    `iop_xxx_msg`.`id`,
    `iii_sss_msg`.`message_id`,
    `iii_sss_msg`.`title`,
    `iii_sss_msg`.`content`,
    `iii_sss_msg`.`id_seller`,
    `iii_sss_msg`.`id_user`,
    `iii_sss_msg`.`gmt_create`,
    `iii_sss_msg`.`gmt_modified`,
    `iii_sss_msg`.`is_read`,
    `iii_sss_msg`.`category`,
    `iii_sss_msg`.`sub_category`,
    `iii_sss_msg`.`description`,
    `iii_sss_msg`.`need_side_notice`,
    `iii_sss_msg`.`link_url`,
    `iii_sss_msg`.`btn_name`,
    `iii_sss_msg`.`gmt_create_l`,
    `iii_sss_msg`.`mobile_content`,
    `iii_sss_msg`.`tier`,
    `iii_sss_msg`.`requirement_id`,
    `iii_sss_msg`.`fk_template_id`,
    `iii_sss_msg`.`business_part`,
    `iii_sss_msg`.`business_id`
  from
    `iii_sss_msg_29` `iii_sss_msg`
  WHERE
    (
      (
        (
          `iii_sss_msg`.`gmt_create` >= '2023-07-24 00:00:00'
        )
        AND (
          `iii_sss_msg`.`gmt_create` < '2023-07-31 15:46:45.684'
        )
        AND (`iii_sss_msg`.`id_user` = 500173482096)
        AND (`iii_sss_msg`.`tier` IN ('S', 'A'))
        AND (
          `iii_sss_msg`.`sub_category` IN (1000305, 1000306, 1000501, 1000502)
        )
      )
      OR (
        (`iii_sss_msg`.`category` IN (10003, 10005))
        AND (
          `iii_sss_msg`.`gmt_create` >= '2023-07-24 00:00:00'
        )
        AND (
          `iii_sss_msg`.`gmt_create` < '2023-07-31 15:46:45.684'
        )
        AND (`iii_sss_msg`.`id_user` = ***)
        AND (
          `iii_sss_msg`.`sub_category` IN (1000305, 1000306, 1000501, 1000502)
        )
      )
    )
  order by
    `iii_sss_msg`.`gmt_create` desc
  limit
    0, 5
)union all ...

语句较为复杂,概括来讲sql语句的含义是找到七天内某个商家的某几个类目或者tier='S' 的最近的五条消息。由iii_sss_msg表根据创建时间分了31张表,路由规则如下:

<property name="tbRuleArray">
    <value>"iii_sss_msg_" + getCalendar(#gmt_create,1_date,31#).get(Calendar.DAY_OF_MONTH)</value>
  </property>

所以该查询连接了七张表iii_sss_msg_29、iii_sss_msg_30....(该分表规则极不合理,本文之后分析)


我们看看mybatis对应的xml文件是什么样的。

<select id="selectByQuery" resultMap="webMsgResultMap" parameterType="map" >
        select
        <include refid="Base_Column_List" />
        from iii_sss_msg
        where
        id_user = #{userId}
        <if test="startTime != null">
             and gmt_create <![CDATA[>=]]> #{startTime}
        </if>
        <if test="endTime != null">
           and gmt_create <![CDATA[<]]> #{endTime}
        </if>
       ...

发现mybatis对应的sql语句where条件的排序与我们最后看到的sql语句的并不一样。是有什么改变了这个语句?


TDDL优化器


我们的应用使用了tddl,在mybatis生成了sql后还会被tddl更改,tddl的工作流程如下,在Matrix层会对mysql进行解析与优化。(详细的tddl知识请读者自行查阅相关知识)

image.png


问题解答了,是tddl改变了这个语句,目的是优化查询。那目的达到了吗?我们继续进行分析,该表的索引信息如下:

image.png

发现该表有个名为idx_user的索引,该索引是gmt_create,id_user,category,sub_category四个字段的联合索引。这几个字段刚好都在上面慢sql的where条件中。看起来tddl调整sql语句的顺序是为了利用该索引。


我们用执行计划分析下该条语句,结果如下:

image.png

发现用的索引并不是idx_user(gmt_create,id_user,category,sub_category),而是idx_uer_query(id_user,category)。看起来tddl自作多情了。那为什么mysql选择的是idx_user_query(id_user,category)索引呢?


为了验证这个问题我们使用

force index(idx_user(gmt_create,id_user,

category,sub_category))强制使用idx_user索引。

image.png

对比两个执行计划发现,使用idx_uer_query(id_user,category)索引的扫描行数是13948,但是使用idx_user

(gmt_create,id_user,category,sub_category)

的索引的扫描行数是1552218,扫描行数是前者的100多倍!这个索引用了个寂寞,幸好mysql没听信tddl的谗言。那mysql怎么知道使用idx_user索引会更好?


mysql如何选择索引


选择索引是优化器的工作。而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。优化器主要会根据以下条件考虑:


1.查询语句中的条件:MySQL 会根据查询语句中的条件选择最合适的索引,以尽可能快地定位符合条件的行。如果查询条件包括多个列,那么可以考虑建立多列索引,以便在匹配时更高效。


2.索引的选择性(基数):MySQL 会根据索引的选择性来选择最合适的索引。选择性是指索引列中不同值的个数与表中记录总数的比值。选择性越高,索引的效率越高。


3.索引的大小和数据类型:索引的大小和数据类型也会影响索引的选择。较小的索引通常比较大的索引效率更高,而数据类型的不同也会影响索引的效率。


4.数据块的大小:MySQL 使用数据块(或称为页)来存储索引数据和表数据,数据块的大小也会影响索引选择。较小的数据块能够提高缓存的效率,并减少磁盘 I/O 操作的次数。


5.索引的覆盖度:如果一个查询可以使用覆盖索引来满足,则 MySQL 通常会优先选择使用覆盖索引。覆盖索引是指查询语句中需要的列都包含在索引中,不需要再到表中读取数据。


如果按照1、5点当然选择idx_user(gmt_create,id_user,category,sub_category)更好,但是第二点呢?我们使用:

SHOW INDEX FROM `iii_sss_msg_29`

分析两个索引的基数:

image.png

发现idx_user(gmt_create,id_user,category,sub_category)的gmt_create列的基数为99933,idx_uer_query(id_user,category)的id_user的基数为286528优于前者。


综合考虑,所以优化器选择idx_uer_query(id_user,category)索引。


MySQL会选错索引吗?


上面我们分析的两个索引的基数差距并不是很大(优化器也可能预估错误),然而idx_user(gmt_create,id_user,category,sub_category)索引在查询语句中的条件中的匹配度更高和使用索引idx_user(gmt_create,id_user,category,sub_category)可以避免排序(idx_user(gmt_create,id_user,category,sub_category)本身是索引,已经是有序的了,如果选择索引idx_user(gmt_create,id_user,category,sub_category)的话,不需要再做排序,只需要遍历),所以即使扫描行数多,也判定为代价更小。会有这样的情况发生吗?


多次执行,看看其他分表的执行计划,发现优化器这次使用了idx_user索引!tddl优化器和索引idx_user(gmt_create,id_user,category,sub_category)里应外合误使优化器选择了错误的索引。选择了一百多万的扫描行数的执行方案。

image.png

使用索引

idx_user(gmt_create,id_user,category,sub_category)需要执行995毫秒:

image.png

使用索引idx_uer_query(id_user,category)需要执行95毫秒:

image.png

这可能就是慢sql的原因了:tddl优化器选择了错误的索引。


索引该怎么建?


我们再来看看下面这个索引:

idx_user(gmt_create,id_user,category,sub_category)为什么建立这个索引?大概是因为业务需求,有以上的条件查询的语句较多。但是该索引合理吗?

image.png

上图是笔者画的idx_user(gmt_create,id_user,category,sub_category)索引的B+树结构图。可以看到B+树只维护第一列gmt_create的有序性,其他字段的有序性没有维护。由该表的分表规则可知iii_sss_msg_29的创建时间都在(2023-07-29 00:00:00-2023-07-29 23:59:59)之间。


结合之前的sql语句的where条件:

WHERE
    (
      (
        (
          `iii_sss_msg`.`gmt_create` >= '2023-07-24 00:00:00'
        )
        AND (
          `iii_sss_msg`.`gmt_create` < '2023-07-31 15:46:45.684'
        )

可知若使用该索引,idx_user所有的叶子结点都会扫描一遍!之前强制使用该索引查询时扫描行数一百多万,主键id索引的基数三百多万,两者是同一量级。


idx_user(gmt_create,id_user,category,sub_category)索引在上述sql的应用场景是不合理的,特别是在按创建时间的分表规则下。笔者将其改为idx_user(id_user,category,sub_category,gmt_create,is_read)(因为是否已读也常见于业务条件中,因此加上)。更改之后,再次查看sql语句的执行计划,发现这次优化器坚定的选择了idx_user(id_user,category,sub_category,gmt_create,is_read)索引。这次扫描行数只有一万多行。与idx_uer_query(id_user,category)索引相比,该索引还能减少回表次数。

image.png


mysql的索引不是刚建表的时候就能创建完全的。索引是为了加快查询数据,与业务场景强相关。所以索引要根据业务查询做相应的变化。不合理的索引反而会妨碍查询,误导优化器。


分表拆分键如何选择

iii_sss_msg表根据gmt_create进行分表,该拆分键是合理的吗?


拆分键的选择重点应该考虑:


1.能够最大限度的实现数据均匀分布到每个物理分表上去,即能够实现负载均衡实现均匀拆分其实最好的就是自增的主键取模。但是可能主键不是自增,甚至不是数字就不一定能够实现绝对的均匀分布了。


2.拆分键不可为空,必须要有索引。


3.尽可能的实现所有的查询sql的where语句后都能够带上这个字段,如果做不到也要有方式能够路由到具体的物理表。


4.要注意将相关联的一系列数据拆分到同一个表中。


查七天内某个商家的一些类目的消息是我们应用最常见的场景,但是iii_sss_msg消息表却根据gmt_create进行分表,频繁触发联表查询,每次查询要从七张表里面取数据,那如果以后要看一个月内的消息,那得从三十张表里面取数据。如此看来该拆分键极不合理。根据业务场景来看,按照id_user作为拆分键是更佳选择。(该改动较为费时,待后期排期解决QAQ)


总结

本文从遇到的问题出发,分析了tddl优化器、MySQL引、分表拆分键的选择相关知识,以下是知识总结:


1.tlld在Matrix层会对mysql进行解析与优化。


2.选择索引是优化器的工作。而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。优化器主要会根据以下条件考虑:查询语句中的条件、索引的选择性(基数)、索引的大小和数据类型、数据块的大小、索引的覆盖度。


3.mysql优化器存在选错索引的可能性。对于优化器误判的情况,可以在应用端用force index来强行指定索引,也可以通过修改语句来引导优化器,还可以通过增加或者删除索引来绕过这个问题。


4.拆分键的选择重点应该考虑:能够最大限度的实现数据均匀分布到每个物理分表上去、拆分键不可为空,必须要有索引、尽可能的实现所有的查询sql的where语句后都能够带上这个字段、要注意将相关联的一系列数据拆分到同一个表中。


以上知识如果想详细了解,可以查阅相关资料。





来源  |  阿里云开发者公众号
作者  |  
成诚




相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
6天前
|
存储 关系型数据库 MySQL
MySQL基础:索引
MySQL中的索引是一种数据结构,能大幅提升数据库查询效率和减少I/O成本,类似于书的目录帮助快速定位内容。其优势包括提高检索效率和降低排序成本,但会占用空间并影响更新表的效率。鉴于查询远多于更新,索引仍被推荐使用。索引分为多种类型,如B+树和哈希索引,其中B+树因其较低的高度和稳定的查询开销成为常用选择。创建和删除索引需谨慎,以免影响性能。
27 4
MySQL基础:索引
|
22天前
|
缓存 关系型数据库 MySQL
在Linux中,如何优化MySQL性能,包括索引优化和查询分析?
在Linux中,如何优化MySQL性能,包括索引优化和查询分析?
|
24天前
|
存储 关系型数据库 MySQL
MySQL bit类型增加索引后查询结果不正确案例浅析
【8月更文挑战第17天】在MySQL中,`BIT`类型字段在添加索引后可能出现查询结果异常。表现为查询结果与预期不符,如返回错误记录或遗漏部分数据。原因包括索引使用不当、数据存储及比较问题,以及索引创建时未充分考虑`BIT`特性。解决方法涉及正确运用索引、理解`BIT`的存储和比较机制,以及合理创建索引以覆盖各种查询条件。通过`EXPLAIN`分析执行计划可帮助诊断和优化查询。
|
26天前
|
SQL 存储 关系型数据库
mysql加索引真的会锁表吗?揭秘背后的技术细节与规避策略
【8月更文挑战第16天】在数据库管理中,添加索引能大幅提升查询效率。MySQL执行此操作时的锁定行为常引起关注。文章详细解析MySQL中索引添加时的锁定机制及其原理。不同存储引擎及SQL语句影响锁定策略:MyISAM需全表锁定;InnoDB提供更灵活选项,如使用`ALTER TABLE... LOCK=NONE`可在加索引时允许读写访问,尽管可能延长索引构建时间。自MySQL 5.6起,在线DDL技术可进一步减少锁定时间,通过`ALGORITHM=INPLACE`和`LOCK=NONE`实现近乎无锁的表结构变更。合理配置这些选项有助于最小化对业务的影响并保持数据库高效运行。
74 4
|
26天前
|
SQL JavaScript 关系型数据库
Mysql索引不当引发死锁问题
本文通过真实案例解析了MySQL在高并发环境下出现死锁的问题。数据库表`t_award`包含多个索引,但在执行特定SQL语句时遭遇索引失效,导致更新操作变慢并引发死锁。分析发现,联合索引`(pool_id, identifier, status, is_redeemed)`因`identifier`允许为空值而导致索引部分失效。此外,`pool_id`上的普通索引产生的间隙锁在高并发下加剧了死锁风险。为解决此问题,文中提出了调整索引顺序至`(pool_id, status, is_redeemed, identifier)`等方案来优化索引使用,进而减轻死锁现象。
|
29天前
|
缓存 NoSQL Redis
一天五道Java面试题----第九天(简述MySQL中索引类型对数据库的性能的影响--------->缓存雪崩、缓存穿透、缓存击穿)
这篇文章是关于Java面试中可能会遇到的五个问题,包括MySQL索引类型及其对数据库性能的影响、Redis的RDB和AOF持久化机制、Redis的过期键删除策略、Redis的单线程模型为何高效,以及缓存雪崩、缓存穿透和缓存击穿的概念及其解决方案。
|
11天前
|
SQL 关系型数据库 MySQL
SQL Server、MySQL、PostgreSQL:主流数据库SQL语法异同比较——深入探讨数据类型、分页查询、表创建与数据插入、函数和索引等关键语法差异,为跨数据库开发提供实用指导
【8月更文挑战第31天】SQL Server、MySQL和PostgreSQL是当今最流行的关系型数据库管理系统,均使用SQL作为查询语言,但在语法和功能实现上存在差异。本文将比较它们在数据类型、分页查询、创建和插入数据以及函数和索引等方面的异同,帮助开发者更好地理解和使用这些数据库。尽管它们共用SQL语言,但每个系统都有独特的语法规则,了解这些差异有助于提升开发效率和项目成功率。
70 0
|
12天前
|
SQL 关系型数据库 MySQL
深入探索MySQL索引策略
本文旨在深入探讨MySQL(8.0.26)数据库中索引的设计与优化方法。
|
2月前
|
存储 SQL 关系型数据库
(六)MySQL索引原理篇:深入数据库底层揭开索引机制的神秘面纱!
《索引原理篇》它现在终于来了!但对于索引原理及底层实现,相信大家多多少少都有了解过,毕竟这也是面试过程中出现次数较为频繁的一个技术点。在本文中就来一窥`MySQL`索引底层的神秘面纱!
187 5
|
19天前
|
SQL 算法 关系型数据库
MySQL索引看这篇就行
MySQL索引看这篇就行
24 0

热门文章

最新文章