• Mysql 索引上做计算为什么会导致索引失效


    一、数据准备

    创建 employee 表

    CREATE TABLE `employee` (
      `id` int NOT NULL AUTO_INCREMENT COMMENT '主键 id',
      `name` varchar(32) NOT NULL DEFAULT '' COMMENT '姓名',
      `age` int NOT NULL DEFAULT '0' COMMENT '年龄',
      `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
      `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
      PRIMARY KEY (`id`),
      KEY `idx_name` (`name`),
      KEY `idx_age` (`age`),
      KEY `idx_hire_time` (`hire_time`)
    ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

    employee 表中 name 字段为 varchar 类型, age 字段为 int 类型,hire_time 为 timestamp 类型,同时针对字段 name、age、hire_time 分别创建了 3 个单列索引 idx_name、idx_age、idx_hire_time ,现在我们为 employee 表插入几条数据

    二、SQL 执行计划分析

    数据准备好了之后我们就可以开始分析了

    下面先看两条 Sql,分析一下这些 Sql 会不会走索引

    select * from employee where name = '1';
    select * from employee where age = 2;
    

    我们很自然的可以想到,name 是字符串类型,传递的参数也是字符串,age 是数字类型,传递的也是数字,传递的参数类型与索引字段类型完全匹配,所以它们一定是会走索引的

    通过 explain 关键字看一下执行计划

    果然上面两个 Sql 是能使用到索引的,那么继续看一下这条 Sql

    select * from employee where hire_time = '20220513193801';

    hire_time 字段的类型是 timestamp,而我们传递的参数是字符串 '20220513193801',参数类型和索引字段类型并不匹配,那么这样的情况会使用到索引吗?

    在 Mysql 中如果字段类型和参数类型不一致,并且字段类型和参数类型中存在字符串,那么 Mysql 都会把字符串转换成其它类型,从而保证字段和参数的类型一致,例如上面的 Sql 就会把字符串 '20220513193801' 转换成时间类型,然后再根据条件进行筛选过滤

    通过查询可以推断出字符串 '20220513193801' 会被转成时间格式 2022-05-13 19:38:01,然后再根据条件去数据库中查找符合条件的数据记录

    转换只发生在参数上,并没有在索引字段上进行 显示/隐式 的转换,所以索引不会失效

    继续分析一下下面的两条 Sql,它们会用到索引吗?

    select * from employee where age = '1';
    select * from employee where name = 0;
    

    我们大多数情况下都会认为 age 是数值类型,传递的参数是字符串 '1', name 是字符串类型,传递的参数却是数值类型的 123,索引字段类型和参数类型不匹配,所以它们不会走索引,真的是这样吗,我们可以使用 explain 验证一下

    第一条 Sql 走了索引,而第二条 Sql 却没有走索引,为什么呢,下面我们就来分析一下

    先看第一条 Sql

    select * from employee where age = '1';
    

    前面我们提到过,当有数据库字段类型和传递的参数类型不一致的时候,如果包含字符串,Mysql 默认会将字符串转成可以让 = 号两边匹配的上的类型

    age 是数值类型, '1' 是字符串类型,那么字符串 '1' 会转成数字类型,这样 = 号两边的类型就相符了,如果将字符串 '1' 转成数值类型,转换之后的数值类型的值是多少呢?

    • 如果是数值类型的字符串,那么转成数值类型之后的值就是对应的数值本身

    例如 '6' 转换成 6、'12' 转换成 12

    • 如果字符串不是以数字开头,转成数值类型之后,数值类型的值为 0 

    例如 'x13x' 转换成 0、'abc' 转换成 0 

    • 如果字符串是以数字开头,转成数值类型的时候会将开头的数值截取下来,直到截取到非字符串为止,截取下来的数字就是转换后的结果

    例如 '1a4' 转换成 1、'16xx14' 转换成 16

    age 字段是数值类型,虽然传递的参数是字符串类型,但是转换只发生在参数上,基于 age 字段构建的索引 B+ 树并没有发生任何变化,只需要将参数对应的字符串转化成数字类型的值,然后使用数字类型的值去 age 索引树上查找即可,索引并不会失效

    继续看第二条 Sql

    select * from employee where name = 123;
    

    前面已经分析了,如果包含字符串的参数类型和字段类型不匹配,Mysql 会将字符串转换成其它类型,以保证字段类型和参数类型相一致

    name 字段是字符串, 123 是数值类型,所以这一条 Sql 会将 employee 表中 name 列的所有值先转成数值类型,然后再进行匹配过滤

    可以验证一下

    上面的 Sql 为什么能筛选到 5 条记录呢,那是因为 Mysql 会将 name 列的所有值先转成数值类型

    '0'、'alis'、'dani'、'jean'、'owen' 这些字符串都会转成 int 类型的 0 

    '1' 转成 1

    '2' 转成 2

    '3' 转成 3

    所以上面的 Sql 能匹配到 5 条记录

    接下来分析一下,为什么 name 列不能用到索引 idx_name

    先看一下基于 name 字段建立的正常索引树是什么样子的 (黄色的是主键 id)

    条件是数字类型的 123 ,而基于 name 字段的索引树是字符串类型的,假如你想要利用索引去搜寻,必然要先将索引树上的字符串转成数字类型,然后再根据数字类型的参数 123 去整颗索引树上进行查找比对,找出符合条件的数据,否则数字类型的 123 怎么可能去字符串类型的索引树上去搜寻结果呢

    基于上面的要求,我们就来转换一下索引树

    '0'、'alis'、'dani'、'jean'、'owen' 这些字符串都会转成 int 类型的 0 

    '1' 转成 1

    '2' 转成 2

    '3' 转成 3

    那么原先的索引树就会变成如下这个样子(黄色的是主键 id)

    转换的过程会存在以下的问题

    1、完全转换一颗这样的 B+ 树是十分消耗性能的,因为如果表数据过大的情况下,索引树也是很庞大的

    2、转换后的 B+ 树有可能不是一颗符合规范的 B+ 树,例如上图转换过后的 B+ 树,叶子节点的 key 并不是有序的(key  0<-->1<-->2<-->3<-->0<-->0<-->0<-->0 并不是有序的),既然不是一颗合格的 B+ 树,那么你就不能利用 B+ 树的特性去快速查找了,如果你想把它变成一颗合格的 B+ 树,你又要做很多额外的操作,那么这个成本开销是很大的,显然不现实

    3、如果其它的 Sql 也用到了 idx_name 这个索引,转换后的 B+ 树就会影响到其它使用了该索引的 Sql

    所以 Mysql 底层认为,与其这样还不如全表扫描呢,这样效率反而更高一些,Mysql 根据自身维护的一套成本计算规则认定全表扫描的成本更低,所以输出执行计划的时候 type 列的值就是 ALL

    那么既然 Mysql 认为全表扫描成本更低,而你又认为使用索引的成本更低,如果你强制使用索引(force index),会怎么样呢?

    即使你想强制使用 idx_name 索引,Mysql 也不会这么做

    同理,在索引字段上显示的使用函数,或者对索引字段进行运算也会使索引失效

    三、总结

    1、在索引字段对应的参数上 显式、隐式转换不会影响到索引树的结构,索引不会失效

    2、对索引字段进行显示、隐式转换都会让索引失效,因为你想利用索引的话必然要先改变索引树的结构,这是一个很高的成本,与其是这样,还不如全表扫描

    3、为了避免出现各种意想不到的问题,一定要保证索引字段的类型和条件中参数的类型一致

    四、题外话

    分享一个生产环境发生的慢 SQL 问题

    我们生产环境有一张集团总商户表,里面有一个索引字段 first_agent_id,类型是 int 类型

    前端在传递参数的时候没有对输入框做任何限制,既可以输入数字也可以输入字符串

    我们后台在实体类中定义了一个字段 private String firstAgentId,用来接收前台传递的参数

    当前端输入数字 123456 的时候,可以通过实体类进行转换,变成字符串类型,Sql 中的条件变成了 first_agent_id = '123456' ,由于 fisrt_agent_id 是 int 类型,参数是字符串 '123456',所以字符串会先转换成 int 类型,也就等价于 first_agent_id = 123456,索引不会失效,查找性能也是比较高的

    问题就出在,当前端输入的是一些杂乱的字符串,例如  'afsdfsd ' 的时候,Sql 中的条件变成了 first_agent_id = 'afsdfsd ' ,字符串 'afsdfsd ' 转换成 int 类型的 0, Sql 的条件就变成了 first_agent_id = 0,而我们这张集团商户表中存在 130 万条 first_agent_id = 0 的过期商户,这 130 万过期商户就很无辜的被查出来了,后面使用 in(商户号1,商户号2........) 操作的时候,整个数据库都炸了

     

  • 相关阅读:
    通过Eclipse生成可运行的jar包
    消息队列原理概念扫盲
    为mutable类型的容器(array,set等)添加kvo,有点麻烦,供参考和了解下吧
    iOS archive(归档)的总结 (序列化和反序列化,持久化到文件)
    http相关概念在iOS中的使用介绍
    AutoLayout技术选型和应用
    addChildViewController相关api深入剖析
    SymmetricDS 完全配置安装手册
    决策树之 C4.5 算法
    决策树之 ID3 算法
  • 原文地址:https://www.cnblogs.com/xiaomaomao/p/16267857.html
Copyright © 2020-2023  润新知