慢SQL原因分析之索引失效 | 京东物流技术团队

news2025/2/24 14:19:16

现象

最近收到一个慢sql工单,慢sql大概是这样:“select xxx from tabel where type = 1”。

咦,type字段明明有索引啊,为啥是慢sql呢?

原因

通过执行explain,发现实际上数据库执行了全表扫描,从而被系统判定为慢sql。这时有一定开发经验的同事会说:“字段区分度不够,这种字段作单独索引是没有意义的”。那么为什么会产生索引失效这种情况呢?索引失效都有哪些情况呢?

索引失效概括

下面总结了若干索引失效的情况:

不满足最左匹配

假如表中有个组合索引,idx_start_org_code_start_province_id_trans_type,它的索引顺序如下:

start_org_code,

start_province_id,

trans_type

当我们从第二个索引字段开始查询时就不会走索引:

因为索引是BTree结构的,不能跳过第一个索引直接走第二个索引

索引列上有计算

当我们用主键做条件时,走索引了:

而当id列上面有计算,比如:

可以看到走了全表扫描

索引列上有函数

有时候我们在某条sql语句的查询条件中,需要使用函数,比如:截取某个字段的长度:

你有没有发现,在使用该函数之后,该sql语句竟然走了全表扫描,索引失效了

字段类型不同

在sql语句中因为字段类型不同,而导致索引失效的问题,很容易遇到,可能是我们日常工作中最容易忽略的问题。

到底怎么回事呢?

我们看下表里的start_org_code字段,它是varchar字符类型的

在sql语句查询数据时,查询条件我们可以写成这样:

从上图中看到,该字段走了索引

但如果在写sql时,不小心把引号丢了:

咦,该sql语句居然变成全表扫描了,为什么索引失效了?

答:因为这个索引列是varchar类型,而传参的类型是int,mysql在比较两种不同类型的字段时会尝试把这两个转化为同一种类型,再进行比较。这样就可以理解为在字段上加了函数,根据上面分析,索引列加了函数会索引失效。

比较有意思的是,如果int类型的id字段,在查询时加了引号条件,却还可以走索引:

从图中看出该sql语句确实走了索引。int类型的参数,不管在查询时加没加引号,都能走索引。

这还科学吗?有没有王法了?

答:MySQL发现如果是int类型字段作为查询条件时,它会自动将该字段的传参进行隐式转换,把字符串转换成int类型。

MySQL会把上面列子中的字符串12348,转换成数字12348,所以仍然能走索引。

事实上,索引列上对字段做任何操作都会导致索引失效,因为mysq认为任何计算或者函数都会改变索引的实际效果,如果继续使用索引可能会造成结果不准确。

like左边包含%

这个相信有点编程经验的同学都知道,这里就不举例说明了。但是为什么索引会失效呢?

答:其实很好理解,索引就像字典中的目录。一般目录是按字母或者拼音从小到大,从左到右排序,是有顺序的。

我们在查目录时,通常会先从左边第一个字母进行匹对,如果相同,再匹对左边第二个字母,如果再相同匹对其他的字母,以此类推。

通过这种方式我们能快速锁定一个具体的目录,或者缩小目录的范围。

但如果你硬要跟目录的设计反着来,先从字典目录右边匹配第一个字母,这画面你可以自行脑补一下,你眼中可能只剩下绝望了,哈哈

列对比

假如我们现在有这样一个需求:过滤出表中某两列值相同的记录。例如:

索引失效了吧?惊不惊喜?

答:表里create_time和update_time都建了索引,单独查询某一字段时都会走索引。但如果把两个单独建了索引的列,用来做列对比时索引会失效。这其实和在索引列上加函数一个原理,MySQL认为索引无法满足需求。

or和in和exist和not in和not exist

这几个有异曲同工之处,就放一起说了。这里就不像上面几种情况100%不走索引了,而是有时候会走索引,有时候不走索引。到底走不走?成本计算说了算。

成本计算

查询优化器是 MySQL 的核心子系统之一,成本计算又是查询优化器的核心逻辑。

全表扫描成本作为参照物,用于和表的其它访问方式的成本做对比。任何一种访问方式,只要成本超过了全表扫描成本,就不会被使用。

基于全表扫描成本的重要地位,要讲清楚 MySQL 的成本计算逻辑,从全表扫描成本计算开始是个不错的选择。

全表扫描成本计算

定义

我们先来看一下Mysql源码里成本计算的定义:

class Cost_estimate {
 private:
  // cost of I/O operations
  double io_cost;
  // cost of CPU operations
  double cpu_cost;
  // cost of remote operations
  double import_cost;
  // memory used (bytes)
  double mem_cost;
  ......
}


从上面代码可以看到,MySQL 成本计算模型定义了四种成本:

  • IO 成本:从磁盘或内存读取数据页的成本。
  • CPU 成本:访问记录需要消耗的 CPU 成本。
  • 导入成本:这一项一直没被使用,先忽略。
  • 内存成本:这一项指的是占用内存字节数,计算 MRR(Multi Range Read)方式读取数据的成本时才会用到,也先忽略。

全表扫描的成本就只剩 IO 成本、CPU 成本这两项了

计算成本

我们先从整体计算公式开始,然后逐步拆解。

全表扫描成本 =io_cost+ 1.1 +cpu_cost+ 1

io_cost 后面的1.1是硬编码直接加到 IO 成本上的;cpu_cost 后面的1也是硬编码的,直接加到 CPU 成本上。代码里长这样:

int test_quick_select(...) {
  ......
  double scan_time =
    cost_model->row_evaluate_cost(static_cast<double>(records)) 
    + 1 /* cpu_cost 后面的 + 1 */; 
  Cost_estimate cost_est = table->file->table_scan_cost();
  
  // io_cost 后面的 + 1.1
  cost_est.add_io(1.1);
  ......
}


关于这两个硬编码的值,代码里没有注释为什么要加,不过它们是个固定值,不影响我们理解成本计算逻辑,先忽略它们。

io_cost =cluster_page_count*avg_single_page_cost。

cluster_page_count 是主键索引数据页数量,从表的统计信息中得到,在统计信息小节会介绍。

avg_single_page_cost 是读取一个数据页的平均成本,通过计算得到,公式如下:

avg_single_page_cost =pages_in_memory_percent 0.25 +pages_on_disk_percent 1.0**。

pages_in_memory_percent 是主键索引已经加载到 Buffer Pool中的叶结点占所有叶结点的比例,用小数表示(取值范围 0.0 ~ 1.0),例如:80% 表示为 0.8。数据页在内存中的比例小节会介绍具体计算逻辑。

pages_on_disk_percent 是主键索引在磁盘文件中的叶结点占所有叶结点的比例,通过1 - pages_in_memory_percent计算得到。

0.25是成本常数 memory_block_read_cost的默认值,表示从 Buffer Pool 中的一个数据页读取数据的成本。

1.0是成本常数io_block_read_cost的默认值,表示把磁盘文件中的一个数据页加载到 Buffer Pool 的成本,加上从 Buffer Pool 中的该数据页读取数据的成本。

cpu_cost = n_rows * 0.1

n_rows 是表中记录的数量,从表的统计信息中得到,在统计信息小节会介绍。

0.1是成本常数row_evaluate_cost的默认值,表示访问一条记录的 CPU 成本。

有了上面这些公式,我们通过一个具体例子走一遍全表扫描成本计算的过程。

假设一个表有 15228 条记录,主键索引数据页的数量为 739,主键索引数据页已经全部加载到 Buffer Pool(pages_in_memory_percent = 1.0),下面我们开始计算过程:

  • pages_on_disk_percent = 1 -pages_in_memory_percent(1.0) =0.0
  • avg_single_page_cost =pages_in_memory_percent(1.0) *0.25+pages_on_disk_percent(0.0) *1.0=0.25
  • io_cost =cluster_page_count(739) *avg_single_page_cost(0.25) =184.75。
  • cpu_cost =n_rows(15228) * 0.1 =1522.8。
  • 全表扫描成本 =io_cost(184.75) +1.1+cpu_cost(1522.8) +1=1709.55
统计信息

全表扫描成本计算过程中,用到了主键索引数据页数量、表中记录数量,这两个数据都来源 InnoDB 的表统计信息。

clustered_index_size就是主键索引数据页数量,n_rows是表中记录数量。

数据页在内存中的比例

avg_single_page_cost =pages_in_memory_percent 0.25 +pages_on_disk_percent 1.0**。

上面的公式用于计算读取一个数据页的平均成本,pages_in_memory_percent 是主键索引已经加载到 Buffer Pool 中的叶结点占所有叶结点的比例。

计算代码如下:

inline double index_pct_cached(const dict_index_t *index) {
  // 索引叶结点数量
  const ulint n_leaf = index->stat_n_leaf_pages;
  ......
  // 已经加载到 Buffer Pool 中的叶结点数量
  const uint64_t n_in_mem =
      buf_stat_per_index->get(index_id_t(index->space, index->id));
  // 已加载到 Buffer Pool 中的叶结点 [除以] 索引叶结点数量
  const double ratio = static_cast<double>(n_in_mem) / n_leaf;
  // 取值只能在 0.0 ~ 1.0 之间
  return (std::max(std::min(ratio, 1.0), 0.0));
}


InnoDB 在内存中维护了一个哈希表(buf_stat_per_index->m_store),key 是表名,value 是表的主键索引已经加载到 Buffer Pool 中的叶子结点数量。

每次从磁盘加载某个表的主键索引的一个叶子结点数据页到 Buffer Pool 中,该表在buf_stat_per_index->m_store中对应的 value 值就加一。

从 Buffer Pool 的 LRU 链表淘汰某个表的主键索引叶子结点时,该表在buf_stat_per_index->m_store中对应的 value 值就减一。

还有其它场景,buf_stat_per_index->m_store 中的 value 值也会发生变化,不展开了。

成本常数

memory_block_read_cost 和 io_block_read_cost 这两个成本常数从系统表mysql.engine_cost中读取,默认分别是1.0和0.25

索引成本计算

以如下sql为例

列resource_type的搜索条件是 BETWEEN 1 AND 2,形成的扫描区间就是[1,2]。**优化器规定,读取二级索引的一个扫描区间的IO成本,和读取一个页面的IO成本相同,无论它占用多少页面。(这个是规定,大家记住就好了)因此二级索引页的IO成本就是1.0。

接下来就是估算二级索引过滤后的记录数量了,也就是满足resource_type BETWEEN 1 AND 2的记录数量。MySQL是这样预估的:

  • 找到索引树中resource_type=1的第一条记录,称为该区间的最左记录,这个过程是极快的。
  • 找到索引树中resource_type=2的最后一条记录,称为该区间的最右记录,这个过程也是极快的。
  • 从最左记录向右最多读10个页面,如果读到了最右记录,则精确计算区间的记录数。
  • 如果读不到最右记录,说明中间记录比较多,则采用预估法。对10个页面中的记录数取平均值,用平均值乘以区间的页面数量即可。

索引页的Page Header部分有PAGE_N_RECS属性记录了页中的记录数,因此不用遍历每个页里的记录

又带来一个新的问题,如何计算这个区间的页面数量呢?还记得B+树的结构吗?该区间的第0层的叶子节点数虽然很多,难以统计,但是我们可以看它们的父节点啊,这两个索引页的目录项大概率是会在同一个父节点页中的,在父节点页中统计区间内有多少页面就非常容易了,其实就是统计两个目录项之间隔了多少个目录项记录。

这里,我们假设满足resource_type BETWEEN 1 AND 2的记录数是15000个,则二级索引记录的CPU成本是15000 * 0.2 + 0.01 = 3000.01

接下来就是这15000条记录回表的IO成本了,MySQL规定,每次回表的IO成本相当于读取一个页面的IO成本,二级索引过滤出的记录数量就是回表的次数。因此,回表的IO成本是15000 * 1.0 = 15000.0。

综上所述,使用索引的执行成本是:

  • IO成本:15000.0+1.0= 15001.0
  • CPU成本:3000.01
  • 总索引成本:15001.0+3000.01=18001.01

通过和全表扫描对比,孰优孰劣一目了然。这下是不是清楚多了?

小结

MySQL执行一条查询语句的流程是这样的,先找到所有可能用到的索引,然后计算全表扫描的成本,然后分别计算使用不同索引的成本,最终选择成本最低的方案来执行查询。这里说的成本其实是由IO成本和CPU成本组成的,对于InnoDB引擎来说,读取一个页的IO成本是1.0,读取一条记录并检测是否符合搜索条件的CPU成本是0.2。全表扫描的成本计算非常简单,根据表的统计数据即可预估出聚簇索引占用的页面数和表的总记录数。对于二级索引的辅助查询,除了过滤二级索引本身的IO成本+CPU成本,还有回表的IO成本+CPU成本,

作者:京东物流 刘海运

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1027833.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

excel的vlookup函数用法

vlookup函数功能&#xff1a;在表格的首列查找指定的数值&#xff0c;并返回表格当前行中指定列处的数值。 结构&#xff1a;VLOOKUP(查找值,查找区域,列序数,匹配条件) 解释&#xff1a;VLOOKUP(找谁,在哪里找,第几列,0或1) 说明&#xff1a; 1、第一参数&#xff1a;查找…

【Linux is not Unix】Linux前言

目录 二战军工的产物——第一台现代电子数字计算机ENIAC&#xff08;埃尼阿克&#xff09; Unix Linux Linux企业应用现状 如今计算机已经应用在我们生活的各个层面&#xff0c;像我们日常使用的笔记本是计算机的一类&#xff0c;可以解决我们生活中遇到的很多问题&#xff…

【数据分享】2023年全国乡镇(街道)点位数据(免费获取\shp格式\excel格式)

乡镇&#xff08;街道&#xff09;点位数据是我们各项研究中经常使用到的数据&#xff0c;在之前的文章中我们分享过2022年度的乡镇&#xff08;街道&#xff09;点位数据&#xff08;可查看之前推送的文章获悉详情&#xff09;。本次我们带来的是2023年度的全国范围的乡镇&…

Labelme分割标注软件

Labelme分割标注软件 1、环境配置与安装1.1 创建conda虚拟环境(建议)1.2 安装Labelme 2、简单使用2.1 创建label标签文件2.2 启动labelme2.3 打开文件/文件夹2.4 设置保存结果路径2.5 标注目标2.6 保存json文件格式 3 格式转换3.1 转换语义分割标签3.2 转换实例分割标签 相关重…

CUDA小白 - NPP(11) 图像处理 Comparison Operations

cuda小白 原始API链接 NPP GPU架构近些年也有不少的变化&#xff0c;具体的可以参考别的博主的介绍&#xff0c;都比较详细。还有一些cuda中的专有名词的含义&#xff0c;可以参考《详解CUDA的Context、Stream、Warp、SM、SP、Kernel、Block、Grid》 常见的NppStatus&#xf…

显示器有白点闪烁、间歇黑屏解决办法

问题描述 以上三张图片是不到一秒内通过手机视频拍摄显示器画面&#xff0c;可以看到第一张图大桥下和第二张图片右下角岛屿初均有红点闪烁。当触发黑屏时&#xff0c;显示器整体白点闪烁。并且时常黑屏&#xff0c;几秒后恢复。 解决办法 检查HDMI连接线是否脱落&#xff0c…

初识canvas

对于一个前端人员来说&#xff0c;canvas是必须掌握的技能之一。如果你想像画画一样在浏览器上作画&#xff0c;那么canvas就可以做你的画布。 接下啦我们就以画画的标准来初步认识下canvas 1.画布 画画的第一步你得有一张画纸或者画布&#xff0c;canvas标签就是我们的画布…

查看mysql 容量

SQL SELECT table_schema "database", sum( data_length index_length) / 1024 / 1024 /1024 "size in GB" FROM information_schema.TABLES GROUP BY table_schema;结果

CSS 之 grid 网格布局

一、简介 ​ display: grid;用于设置元素内部的布局类型为网格布局&#xff0c;其外显类型为块级元素。该类型的元素将内部分为行和列&#xff0c;划分成一个个单元格&#xff0c;并通过一系列相关属性控制单元格及其内容的布局和大小。 ​ 该属性值的主要应用场景为&#xf…

1297. 子串的最大出现次数

1297. 子串的最大出现次数 // 返回子串的最大出现次数&#xff1a;用hash表 // 子串中 不同字母的次数 < maxLetters && 子串长度> minSize && 子串长度 < maxSizeint maxFreq(char * s, int maxLetters, int minSize, int maxSize){}

【算法】矩阵快速幂优化动态规划

文章目录 知识讲解题目列表[矩阵快速幂] 题目列表&#x1f4d5;70. 爬楼梯解法1——线性DP解法2——矩阵快速幂 509. 斐波那契数1137. 第 N 个泰波那契数1220. 统计元音字母序列的数目解法1——线性DP解法2——矩阵快速幂优化DP 552. 学生出勤记录 II&#xff08;&#x1f6b9;…

Kafka 常见问题

文章目录 kafka 如何确保消息的可靠性传输Kafka 高性能的体现利用Partition实现并行处理利用PageCache 如何提高 Kafka 性能调整内核参数来优化IO性能减少网络开销批处理数据压缩降低网络负载高效的序列化方式 kafka 如何确保消息的可靠性传输 消费端弄丢了数据 唯一可能导致…

第N个数字

给你一个整数 n &#xff0c;请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …] 中找出并返回第 n 位上的数字。 我觉得这题是哪以理解的 看这个题解 func findNthDigit(n int) int {digit : 1start : 1count : 9for n > count {n - countdigitstart start …

这个发表在 Nature Genetics的水稻全基因组关联数据库 RHRD,很赞!!!

历经半个世纪的发展&#xff0c;杂交水稻育种取得了巨大的成就&#xff0c;培育出了大量的高产、优质、适应环境变化的品系。本数据库是一个综合性的杂交水稻数据库&#xff08;http://ricehybridresource.cemps.ac.cn/#/&#xff09;&#xff0c;涵盖了从1976年至2017年间发布…

【Unity】简单的深度虚化shader

【Unity】简单的深度虚化shader 实现效果 可以用于对地图场景边界的白模处理 实现方法 1.关键方法 UnityObjectToClipPos&#xff1a;将物体坐标转换为屏幕坐标 LinearEyeDepth&#xff1a;将屏幕坐标中的z值转换为实际的深度值 saturate&#xff1a;将值规范到0~1之间&am…

Java 消息策略的实现 - Kafak 是怎么设计的

这个也是开放讨论题&#xff0c;主要讨论下 Kafka 在消息中是如何进行实现的。 1_cCyPNzf95ygMFUgsrleHtw976506 21.4 KB 总结 这个题目的开发性太强了。 Kafka 可以用的地方非常多&#xff0c;我经历过的项目有 Kafka 用在消息处理策略上的。这个主要是 IoT 项目&#xff0c…

three.js中的3D模型分层显示(分类型显示);使用dat.gui控制three.js中的3D模型分层显示;dat.gui调用一次但是渲染了多个

效果如上&#xff0c;就是可以通过dat.gui控制3D模型中仅仅显示管线或者是仅仅显示除了管线之外的模型。 1.在模型导入的时候就按照类型&#xff08;分层的类别标识&#xff09; 区别开&#xff08;我这里是按照是否是管线&#xff09; 这里是new THREE.Object3D();必须的否则…

Python基础学习笔记3

深度学习实践 深度学习离不开编程 深度学习离不开数学分析&#xff08;高等数学&#xff09;、线性代数、概率论等知识&#xff0c;更离不开以编程为核心的动手实践。 Python编程语言 无论是在机器学习还是深度学习中&#xff0c;Python已经成为主导性的编程语言。而且&…

OJ练习第178题——收集树中金币

收集树中金币 力扣链接&#xff1a;2603. 收集树中金币 题目描述 给你一个 n 个节点的无向无根树&#xff0c;节点编号从 0 到 n - 1 。给你整数 n 和一个长度为 n - 1 的二维整数数组 edges &#xff0c;其中 edges[i] [ai, bi] 表示树中节点 ai 和 bi 之间有一条边。再给…

计算机视觉与深度学习-全连接神经网络-训练过程-欠拟合、过拟合和Dropout- [北邮鲁鹏]

目录标题 机器学习的根本问题过拟合overfitting泛化能力差。应对过拟合最优方案次优方案调节模型大小约束模型权重&#xff0c;即权重正则化(常用的有L1、L2正则化)L1 正则化L2 正则化对异常值的敏感性随机失活(Dropout)随机失活的问题 欠拟合 机器学习的根本问题 机器学习的根…