《MySQL实战45讲》——学习笔记18 “索引失效、函数/隐式转换“【建议收藏】

news2025/8/7 11:25:29

本文介绍几种MYSQL中索引失效的典型SQL用法,包括对索引字段使用函数、索引字段隐式类型转换(如字符串转数值类型,实质上也是使用CAST函数)、索引字段隐式字符编码转换(如utf8mb4字符集与utf8字符集的等值判断,实质上也是使用CONVERT函数);

并且,有的时候你可能发现explain执行计划中使用了索引,但是SQL的性能依然很慢,这时就要观察下rows判断是否走了全表扫描;并不是说查询走了索引就一定快;

案例一:查询条件中对索引字段显示的使用函数

假设有一个交易系统,其中交易记录表tradelog包含交易流水号(tradeid)、交易员id(operator)、交易时间(t_modified)等字段,其中交易时间和流水号字段都加了索引;这个表的建表语句如下:

mysql> CREATE TABLE `tradelog` (
  `id` int(11) NOT NULL,
  `tradeid` varchar(32) DEFAULT NULL,
  `operator` int(11) DEFAULT NULL,
  `t_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `tradeid` (`tradeid`),
  KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

假设,现在已经记录了从2016年初到2018年底的所有数据,运营部门有一个需求是:要统计发生在所有年份中7月份的交易记录总数;这个逻辑看上去并不复杂,你的SQL语句可能会这么写:

mysql> select count(*) from tradelog where month(t_modified)=7;

由于t_modified字段上有索引,于是你就很放心地在生产库中执行了这条语句,但却发现执行了特别久,才返回了结果;DBA同事的解释可能是:如果对字段做了函数计算,就用不上索引了,这是MySQL的规定

现在分析下原因,下面是这个t_modified索引的示意图,方框上面的数字就是month()函数对应的值;

如果你的SQL语句条件用的是where t_modified='2018-7-1’的话,引擎就会按照上面绿色箭头的路线,快速定位到t_modified='2018-7-1’需要的结果;实际上,B+树提供的这个快速定位能力,来源于同一层兄弟节点的有序性;

但是,如果计算month()函数的话,你会发现这颗B+树的"有序性"被破坏了;例如当传入month(t_modified)=7的时候,在树的第一层就不知道该怎么办了——他不知道接下来往哪个方向找;也就是说,对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能,转而走全索引扫描(遍历整个索引树)

需要注意的是,"放弃走树搜索功能"并不意味着优化器放弃使用这个索引

在这个例子里,尽管优化器放弃了树搜索功能,但它遍历索引树获取t_modified字段,既可以选择遍历主键id索引,也可以选择遍历索引t_modified;优化器对比索引大小后发现,索引t_modified更小(覆盖索引),遍历这个索引比遍历主键索引来得更快;因此最终还是会选择索引t_modified;

使用explain命令查看一下这条SQL语句的执行结果:

key="t_modified"表示的是,使用了t_modified这个索引;因为测试表数据中插入了10万行数据,这里统计rows=100335,说明这条语句扫描了整个索引的所有值;Extra字段的Using index,表示的是使用了覆盖索引,因为仅需要全表扫描t_modified字段;

结论就是——由于在t_modified这个索引字段加了month()函数操作,导致了全索引扫描;

根据上述结论,可以在语句中不对索引字段做函数运算,而是使用范围查询,如下;

mysql> select count(*) from tradelog where
    -> (t_modified >= '2016-7-1' and t_modified<'2016-8-1') or
    -> (t_modified >= '2017-7-1' and t_modified<'2017-8-1') or 
    -> (t_modified >= '2018-7-1' and t_modified<'2018-8-1');

不过优化器在个问题上确实有“偷懒”行为——即使是对于不改变有序性的函数,也不会考虑使用索引;比如,对于 select * from tradelog where id+1=10000 这个SQL语句,这个加1操作并不会改变有序性,但是MySQL优化器还是不能用id索引快速定位到9999这一行;所以,需要你在写SQL语句的时候,手动改写成 where id=10000-1 才可以;

案例二:对索引字段隐式的使用类型转换函数

先看下下面的语句,交易编号 tradeid 这个字段上本来就有索引,但是 explain 的结果却显示,这条语句需要走全表扫描;

mysql> select * from tradelog where tradeid=110717;

实际上,tradeid 的字段类型是 varchar(32),而输入的参数110717却是整型,所以这里一定需要做类型转换

那么,现在这里就有两个问题:

(1)数据类型转换的规则是什么?
(2)为什么有数据类型转换,就需要走全索引扫描?

先来看第一个问题,尝试执行 select '10'>9的结果:结果是1,也就是说这里"将字符串转成了数值",也就是执行到"tradeid=110717"时MySQL将索引字段tradeid转成了数值型做比较;(字符串比较大小是逐位从高位到低位逐个比较(按ascii码),"10"的第一位字符"1"的ascii比"9"小,所以如果是数值转字符串那么结果应该为0)

所以,上述的语句对于优化器来说,相当于:

mysql> select * from tradelog where  CAST(tradid AS signed int) = 110717;

也就是说,这条语句触发了我们上面说到的规则:对索引字段做函数操作,优化器会放弃走树搜索功能;举个例子,反过来,假如主键id的类型是int,如果执行下面这个语句,是否会导致全表扫描呢?

select * from tradelog where id="83126";

答案是——会走索引当字符串和数字比较时会把字符串转化为数字,所以这条语句里的隐式转换不会应用到索引字段上,而是作用于where等号后面的变量"83126"上,所以可以走索引;

案例三:对索引字段隐式的使用字符编码转换

假设系统里还有另外一个表 trade_detail,用于记录交易的操作细节;这个表与上面的交易日志表 tradelog 通过交易流水号关联;

mysql> CREATE TABLE `trade_detail` (
  `id` int(11) NOT NULL,
  `tradeid` varchar(32) DEFAULT NULL,
  `trade_step` int(11) DEFAULT NULL, /*操作步骤*/
  `step_info` varchar(32) DEFAULT NULL, /*步骤信息*/
  PRIMARY KEY (`id`),
  KEY `tradeid` (`tradeid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这时候,如果要查询 tradelog 表中id=2的交易记录的所有操作步骤信息,SQL语句可以这么写:

select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; /*语句Q1*/

看下这条语句的执行计划:

分析这个explain结果:

(1)第一行显示优化器会先在交易记录表tradelog上查到id=2的行,这个步骤用上了主键索引,rows=1表示只扫描一行;
(2)第二行key=NULL,表示没有用上交易详情表trade_detail上的tradeid索引,进行了全表扫描;

在这个执行计划里,是从tradelog表中取tradeid字段,再去trade_detail表里查询匹配字段;因此,我们把tradelog称为驱动表,把trade_detail称为被驱动表,把tradeid称为关联字段;接下来,我们看下这个explain结果表示的执行流程:

图中第3步不符合我们的预期;因为表trade_detail里tradeid字段上是有索引的,我们本来是希望通过使用tradeid索引能够快速定位到等值的行;但这里却走了全表扫描;

如果你去问DBA同学,他们可能会告诉你:因为这两个表的字符集不同,一个是utf8,一个是utf8mb4,所以做表连接查询的时候用不上关联字段的索引

utf8是utf8mb4的子集,utf8mb4在utf8支持的字符上做了扩展,所以当这两个类型的字符串在做比较的时候会做隐式的字符类型转换——MySQL内部的操作是先把utf8字符串转成utf8mb4字符集(如果utf8mb4转成utf8会损失精度),再做比较;

因此,在执行上面这个语句的时候,需要将被驱动数据表trade_detail里的索引字段traideid一个个地转换成utf8mb4编码,再跟驱动表的关联字段的值做比较;也就是说,实际上这个语句等同于下面这个写法:

 /*$L.tradeid.value 指tradelog表的tradeid字段值*/
select * from trade_detail  where CONVERT(traideid USING utf8mb4)=$L.tradeid.value; 

CONVERT() 函数可以把输入的字符串转成utf8mb4字符集;这就再次触发了我们上面说到的原则:对被驱动表的索引字段做了函数操作,优化器放弃走树搜索功能

作为对比验证,现在执行另外一个需求,“查找trade_detail表里id=4的操作,对应的操作者是谁”,再来看下这个语句和它的执行计划;

mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;

这个语句里trade_detail表成了驱动表,explain结果的第二行显示,这次的查询操作用上了被驱动表tradelog里的索引(tradeid),扫描行数是1;跟我们上面的结论对上了,这里被驱动表的tradeid字段是utf8mb4,无需做字符类型转换,而是where=后面的值做了隐式字符转换,即:

/*$D.tradeid.value 指trade_detail 表的tradeid字段值*/
select operator from tradelog where traideid =CONVERT($D.tradeid.value USING utf8mb4);

这里的CONVERT函数是加在输入参数上的,这样就可以用上被驱动表的traideid索引;

理解了原理以后,就可以用来优化这条语句了:

(1)修改表结构,让字符集一致,都换成utf8mb4;

alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null;

(2)但如果数据量比较大,或者业务上暂时不能做这个DDL的话,那就只能采用修改SQL语句的方法了;在这个场景下,tradeid字段从utf8mb4强转uft8不会异常,但是其他场景可能在转换时发生异常,需要注意;

mysql> select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2; 

小结

1. 对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能;函数操作可以是显示的,也有隐式的,如索引字段的类型/字符集转换;

2. MySQL 的优化器确实有“偷懒”的嫌疑,即使简单地把 where id+1=1000 改写成 where id=1000-1 就能够用上索引快速查找,它也不会主动做这个语句重写;

3. explain显示用上了索引不一定就执行快,因为他可能还是需要全表扫描,只不过扫描的字段刚好被索引覆盖,因此选了索引而非走主键索引树;除了要关注key,还要关注扫描行数rows;

下篇文章:待定

本章参考:18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?-极客时间

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

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

相关文章

Leetcode101:对称二叉树

原文链接&#xff1a;101. 对称二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;r…

日志异常检测准确率低?一文掌握日志指标序列分类

背景 目前&#xff0c;日志异常检测算法采用基于时间序列的方法检测异常&#xff0c;具体为&#xff1a;日志结构化->日志模式识别->时间序列转换->异常检测。异常检测算法根据日志指标时序数据的周期性检测出历史新增、时段新增、时段突增、时段突降等多种异常。 然…

那么多优秀的自动化测试工具,而你只知道Selenium?

如今&#xff0c;作为一名软件测试工程师&#xff0c;几乎所有人都需要具备自动化测试相关的知识&#xff0c;并且懂得如何去利用工具&#xff0c;来为企业减少时间成本和错误成本。这是为什么呢&#xff1f; 在以前&#xff0c;测试人员一般都只需要扮演终端用户&#xff0c;…

【云原生 | Kubernetes 系列】--Gitops持续交付 ArgoCD 部署与概念

1. Pull Pipeline 使用Pull Pipeline可以不在集群以外的地方保存凭据,避免凭据泄露的风险 工作模式: 两个Git仓库. 程序代码 程序员维护 分支策略 Hotfix Feature Develop Release Master 部署配置 基于OAM管理 Updater CI流水线交付了新的Image Operator Spec 期望状态 …

UE5笔记【一】安装、第一个关卡:光线、原点、平面;光线、天空、云朵;内容抽屉;运动控制;

第一步&#xff1a;安装UE5 下载Epic Games客户端。 下载EpicGames客户端&#xff0c;完成注册账户。 然后选择【库】、选择【引擎版本】后面的加号【】、然后点击【下拉箭头】选择你要安装的版本。点击【安装】。 特别提醒&#xff1a;请找一块特别大的硬盘存储空间&#xf…

prometheus+alertmanager 企业微信告警

一、应用安装启动 1、软件准备 alertmanager-0.24.0.linux-amd64.tar.gz blackbox_exporter-0.22.0.linux-amd64.tar.gz node_exporter-1.4.0.linux-amd64.tar.gz prometheus-2.40.0-rc.0.linux-amd64.tar.gz 2、配置文件 cat /data/prometheus/prometheus.yml #根据自己情况…

liunx集成jmeter进行压测实践

首先liunx环境需要部署jdk 1,获取jmeter免安装包&#xff1a;点击我获取免安装包 2,获取jmeter-manger工具&#xff0c;用于生成报告&#xff0c;日志等 点击我获取工具 3,在服务器上新建一个文件夹存放jmeter&#xff0c;推荐在/usr/local/下面&#xff0c;我这里由于权限问…

微服务真的过时了吗?Twitter员工因为微服务被马斯克解雇了

马斯克吐槽微服务 最近Twitter因为马斯克收购而大幅裁员的消息闹得可谓是沸沸扬扬&#xff0c;但是最近马斯克吐槽Twitter内部服务200个RPC的新闻更是闹得不可开交&#xff0c;并且和员工在twitter上的争论更是因为了程序猿界的广泛转发和评论 微服务真的是一个糟糕的设计吗&a…

如何处理海量数据文件以及大文件数据查找

目录 一.处理海量整数文件 ①问&#xff1a;假定有40亿个无符号整数&#xff0c;判断某数据是否在其中&#xff1f; ②问&#xff1a;假定有40亿个无符号整数&#xff0c;找到只出现一次的数据&#xff0c;两次&#xff0c;三次...&#xff1f; ③问&#xff1a;两个文件各…

[ Linux ] 动静态库 手把手教你写一个自己的库

目录 静态库与动态库 生成 发布动静态库 形成发布静态库 形成发布动态库 一个makefile同时生成动静态库 如何使用动静态库 使用静态库 正确做法&#xff1a; 使用动态库 运行动态库 为什么动态库运行时有找库的步骤 静态库与动态库 静态库&#xff08;.a&#xff0…

聊一聊作为高并发系统基石之一的缓存,会用很简单,用好才是技术活

大家好&#xff0c;又见面了。 在服务端开发中&#xff0c;缓存常常被当做系统性能扛压的不二之选。在实施方案上&#xff0c;缓存使用策略虽有一定普适性&#xff0c;却也并非完全绝对&#xff0c;需要结合实际的项目诉求与场景进行综合权衡与考量&#xff0c;进而得出符合自…

Spring Security认证之用户定义

本文内容来自王松老师的《深入浅出Spring Security》&#xff0c;自己在学习的时候为了加深理解顺手抄录的&#xff0c;有时候还会写一些自己的想法。 在前面的案例中&#xff0c;我们登陆的用户信息是基于配置文件来配置的&#xff0c;其本质上是基于内存来实现的。但是在实际…

(5)多机器人集群编队策略

系列文章目录 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 TODO:写完再整理 文章目录系列文章目录前言一、多机器人集群编队策略1、多机器人协同作业冲突问题&#xff08;1&#xff09;介绍&#xff08;2&#xff09;多机器人系统…

学会Spring Cloud微服务架构绝活,渣本也能进大厂

微服务架构是互联网很热门的话题&#xff0c;是互联网技术发展的必然结果。它提倡将单一应用程序划分成一组小的服务&#xff0c;服务之间互相协调、互相配合&#xff0c;为用户提供最终价值。虽然微服务架构没有公认的技术标准和规范或者草案&#xff0c;但业界已经有一些很有…

C++程序设计--第三章内容

提前声明&#xff1a; 本文内容为华北水利水电大学研究生C课程&#xff0c;如有 侵权请告知&#xff0c;作者会予以删除 1.函数 函数作用 —— 任务划分&#xff1b;代码重用定义形式 类型 函数名 &#xff08; 形式参数表&#xff09;{语句序列}调用形式 函数名&#x…

数据结构:树

文章目录一.树的概念二.树的相关概念三.树的表示一.树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下…

『Java』数组中值得说的那些事

文章目录前言一、数组的创建及初始化&#x1f333;1、数组的创建&#x1f333;2、数组的初始化&#x1f351;&#xff08;1&#xff09;动态初始化&#x1f351;&#xff08;2&#xff09;静态初始化二、数组的使用&#x1f333;1、数组中元素访问&#x1f333;2、遍历数组&…

python3 爬虫(初试牛刀)

此文章仅供学习交流使用 在学习爬虫之前&#xff0c;把最近对于 VMware 的网络学习做个总结 接下来&#xff0c;步入正题&#xff01; 分析 Robots 协议 禁止所有爬虫访问任何目录的代码&#xff1a; User-agent: * Disallow:/允许所有爬虫访问任何目录的代码&#xff1a; …

2022年新版Pycharm通过project interpreter国内镜像源设置

2022年新版Pycharm通过project interpreter国内镜像源设置解决方案速览一、国内镜像源列表二、pycharm访问project interpreter解决方案速览 File->Settings->project interpreter-> -> Available Packages将options打勾&#xff0c;并输入-i https://pypi.tuna.…

Gvim显示行号、最大化、字号、主题等常用配置修改

Gvim的设置分两种&#xff1a;1. 临时设置&#xff0c;2. 永久设置&#xff0c;本文只关注永久设置的情况。 配置Gvim只需修改Gvim配置文件即可&#xff0c;Linux中&#xff0c;配置文件的地址是~/.vimrc&#xff0c;若没有该文件则创建即可&#xff0c;我们可以直接输入gvim …