一、索引与慢查询

聊一聊索引和慢查询,经常遇到的一个问题:一个SQL语句使用了索引,为什么还是会记录到慢查询日志之中?

为了说明,创建一个表t,该表3个字段,一个主键索引,一个普通索引

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB; insert into t values (1, 1, 1), (2, 2, 2);

首先MySQL判断一个语句是不是慢查询语句,用的是语句执行时间,它把语句执行时间跟long_query_time这个系统参数做比较,如果语句执行时间比long_query_time还大,就会把这个语句记录到慢查询日志里。

long_query_time这个参数它的默认值是10s,在生产上我们不会设置这么大的值,一般会设置1s,对于一些对延迟比较敏感的业务,会设置一个比1还小的值,而对于语句是否使用了索引,它的意思是语句执行过程中有没有用到表的索引。

具体到表象中是explain一个语句的时候,输出结果里面key的值不是NULL,图1就是执行 explain select * from t; 的结果。可以看到key这一列显示的是NULL。图2就是执行explain select * from t where id = 2的结果,这里key显示的是PRIMARY,就是我们常说的使用了主键索引。图3就是执行select a from t 的结果,这里key这一列显示的是a,表示使用了a这个索引。可以看到图2和图3的结果里key的字段都不是NULL,而实际上图3是扫描了整个索引树a。

这个示例的表里面只有两行,那如果有100万行呢,有100万行的时候图2的语句还是可以执行很快,但是图3就肯定慢了,如果是更极端的情况,比如如果这个数据库上CPU压力非常地高,那可能第二个语句的执行时间也会超过long_query_time,会记录到慢查询日志里面,所以如果简单地回答这个问题,是否使用索引只是表示了一个SQL语句的执行过程,而是否记录到慢查询日志中是由它的执行时间决定的,而这个执行时间可能会受各种外部因素的影响,也就是说是否使用索引和是否记录慢查询之间没有必然的联系

二、索引的过滤性

如果我们再深层次的看这个问题其实它还潜藏着一个问题需要澄清就是,什么叫做使用了索引。我们知道InnoDB是索引组织表,所有的数据都是存储在索引树上面的,比如表t,这个表它包含了两个索引,一个主键索引一个普通索引a,在InnoDB里数据是放在主键索引里的。我们来看一下这个表的数据示意图,可以看到数据都放在主键索引上,如果从逻辑上说,所有的在InnoDB表上的查询,都至少用了一个索引,现在有一个问题:如果执行explain select * from t where id > 0; 这个语句有用上索引吗?



现在我们来看看这个语句的explain的结果,在输出结果里,key这里显示的是PRIMARY,其实从数据上你是知道的这个语句一定是做了全表扫描,但是优化器认为,这个语句的执行过程中,需要根据主键索引定位到第一个满足id>0的值,也算用到了索引。所以你看,即使explain结果里面写了key不是NULL,实际上也可能是全表扫描的,因此InnoDB里面只有一种情况叫做没有使用索引,那就是从主键索引的最左边的叶节点开始,向右扫描整个索引树,也就是说,没有使用索引并不是一个准确的描述,你可以用全表扫描来表示一个查询遍历了整个主键索引树。也可以用全索引扫描来说明,像select a from t这样的查询,它扫描了整个普通索引树。而像select * from t where id = 2; 这样的语句才是我们平时说的使用了索引,它表示的意思是我们使用了索引的快速搜索功能,并且有效的减少了扫描行数。

那么除了全索引扫描,还有哪些是使用了索引但是执行速度不够快的例子呢,这就要说到索引的过滤性,假设你现在维护了一个表,这个表记录了全中国人的基本信息,然后你现在要查出年龄在10到15岁之间的小朋友的姓名和基本信息,那么你的语句会这么写,select * from t_people where age between 10 and 15;你一看这个语句一定要在age字段上建索引了,否则就是个全表扫描。但是你会发现在age上建了索引以后,这个语句还是执行慢,因为满足这个条件的数据有超过1亿行。我们来看看建立了这个索引以后这个表的组织结构图,这个语句的执行流程是这样的。从索引age上用树搜索,取到第一个age等于10的记录,得到它的主键ID的值,根据ID的值去主键索引取整行的信息,作为结果集的一部分返回,在索引age上向右扫描,取下一个ID值,到主键索引上取整行信息,作为结果集的一部分返回,重复上面的步骤直到碰到第一个age>15的记录。你看这个语句,虽然它用了索引,但是它扫描超过了一亿行,而上面select * from t;这个语句虽然没有用索引,但其实也只扫描了两行。

所以你现在知道了,当我们讨论有没有使用索引的时候,其实我们关心的是扫描行数,对于一个大表,不止要有索引,索引的过滤性还要足够好,像刚才这个例子age这个索引它的过滤性就不够好。在设计表结构的时候,我们要让索引的过滤性足够好,也就是区分度足够高。那么过滤性好了,是不是表示查询的扫描行数就一定少呢,我们再来看一个例子。

三、索引的扫描行数

如果这个t_people表上有一个索引是姓名、年龄的联合索引,那这个联合索引的过滤性应该不错,如果你的执行语句是select * from t_people where name = '张三' and age = 8; 就可以在一个索引上快速找到第一个姓名是张三并且年龄是8岁的小朋友,当然这样的小朋友就该不多,因此向右扫描的行数很少,查询效率就很高,但是查询的过滤性和索引的过滤性可并不一定是一样的。如果现在你的需求是查出所有名字第一个字是张并且年龄是8岁的所有小朋友,你的语句会怎么写呢?你的语句要这么写:select * from t_people where name like '张%' and age = 8; 在MySQL5.5和之前的版本中,这个语句的执行流程是这样的。首先从联合索引树上找到第一个姓名字段是张开头的记录,取出主键ID,然后到主键索引上,根据ID取出整行的值,判断年龄字段是否等于8如果是就作为结果集的一行返回,如果不是就丢弃,我们把根据ID到主键索引上查找整行数据这个动作称为回表,在联合索引上向右遍历,并重复做回表和判断的逻辑直到碰到联合索引树上名字第一个字不是张的记录为止。你可以看到这个执行过程里面最耗费时间的步骤就是回表,假设全国名字第一个字是张的人有8000万,那么这个过程就要回表8000万次,在定位第一行记录的时候,只能使用索引和联合索引的最左前缀,称为最左前缀原则。那你可以看到这个执行过程它的回表次数特别多,性能不够好,那有没有优化的方法呢?有的在MySQL5.6版本引入了index condition pushdown的优化,我们来看看这个优化的执行流程。

首先从联合索引树上找到第一个姓名字段是张开头的记录,判断这个索引记录里面年龄的值是不是8,如果是就回表,取出整行数据作为结果集的一部分返回,如果不是就丢弃。在联合索引树上向右遍历,并判断年龄字段后根据需要做回表,直到碰到联合索引树上名字的第一个字不是张的记录为止。这个过程跟上面过程的差别是在遍历联合索引的过程中,将年龄等于8这个条件下推到索引遍历的过程中,减少了回表的次数,假设全国名字第一个字是张的人里面朋100万个是8岁的小朋友,那么这个查询过程中,在联合索引里要遍历8000万次,而回表只需要100万次。可以看到,index condition pushdown 优化的效果还是很不错的,但是这个优化,还是没有绕开最左前缀原则的限制,因此在联合索引里,还是要扫描8000万行,那有没有更进一下的优化方法呢?我们可以把名字的第一个字,和年龄做一个联合索引来试试,这里可以用到MySQL 5.7引入的虚拟列来实现,对应的修改表结构的SQL语句是这么写的。

alter table t_people add name_first varchar(2) generated always as
(left(name, 1)), add index (name_first, age); CREATE TABLE `t_people` (
`id` int(11) DEFAULT NULL,
`name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`info` varchar(255) DEFAULT NULL,
`name_first` varchar(2) GENERATED ALWAYS AS (left(`name`, 1)) VIRTUAL,
KEY `name_first` (`name_first`, `age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

上图是这个DDL语句的执行效果,首先它在t_people上创建一个字段叫name_first虚拟列,然后给name_first和age上创建一个联合索引,并且让这个虚拟列的值,总是等于name字段的前两个字节,虚拟列在插入数据的时候,不能指定值,在更新的时候也不能指定修改,它的值会根据定义自动生成,在name字段修改的时候,也会自动修改,有了这个新的联合索引,我们再找名字第一个字是张并且年龄是8的小朋友的时候,这个SQL语句就可以这么写:select * from t_people where name_fist = '张' and age = 8; 这样这个语句的执行过程,就只需要扫描联合索引的100万行并回表100万次。这个优化的本质是创建了一个更紧凑的索引来加速了查询的过程。

四、小结

今天介绍了索引的基本结构和一些查询优化的基本思路,现在我们知道了:

1、使用索引和慢查询没有必然联系,使用索引的SQL也有可能是慢查询语句;

2、检查一个查询语句的执行效率最终要看的是扫描行数,我们查询优化的过程往往就是减少扫描行数的过程;

3、使用虚拟列和联合索引来提升复杂查询的执行效率。

为什么 SQL 语句使用了索引,但却还是慢查询?的更多相关文章

  1. SQL语句技巧_索引的优化_慢查询日志开启_root密码的破解

    1.正则表达式的使用 regexp例:select name,email from t where email regexp '@163[.,]com$'使用like方式查询selct name,em ...

  2. 三,mysql优化--sql语句优化之索引一

    1,需求:如何在一个项目中,找到慢查询的select,mysql数据库支持把慢查询语句,记录到日志中.供程序员分析.(默认不启用此功能,需要手动启用) 修改my.cnf文件(有些地方是my.ini) ...

  3. FreeSql (二十七)将已写好的 SQL 语句,与实体类映射进行二次查询

    有时候,我们希望将写好的 sql 语句,甚至是存储过程进行查询,虽然效率不高(有时候并不是效率至上). 巧用AsTable var sql = fsql.Select<UserX>() . ...

  4. SQL语句调优 - 索引上的数据检索方法

    如果一张表上没有聚集索引,数据将会随机的顺序存放在表里.以dbo.SalesOrderDetail_TEST为例子.它的上面没有聚集索引,只有一个在SalesOrderID上的非聚集索引.所以表格的每 ...

  5. sql语句学习及索引学习,未完待续,补充增删改查

    1,查询出last_name 为 'Chen' 的 manager 的信息.  select * fromwhere employee_id = ( selectfrom employees wher ...

  6. MySQL优化(五) SQL 语句的优化 索引、explain

    一.索引 1.分类 (1)主键索引:当一张表的某个字段设置为主键时,该字段就是主键索引: (2)唯一索引:索引列中的值必须是唯一的,但是允许为空值(可以存在多个null): (3)普通索引:基本索引类 ...

  7. 数据库基础知识详解四:存储过程、视图、游标、SQL语句优化以及索引

    写在文章前:本系列文章用于博主自己归纳复习一些基础知识,同时也分享给可能需要的人,因为水平有限,肯定存在诸多不足以及技术性错误,请大佬们及时指正. 11.存储过程 ​ 存储过程是事先经过编译并存储在数 ...

  8. 四,mysql优化——sql语句优化之索引二

    1,在什么列适合添加索引 (1)较频繁的作为查询条件字段应该添加索引 select * from emp where empid = 2; (2)唯一性太差的字段不适合添加索引,即时频繁作为查询条件. ...

  9. SQL语句中的单引号处理以及模糊查询

    为了防止程序SQL语句错误以及SQL注入,单引号必须经过处理.有2种办法: 1.使用参数,比如SELECT * FROM yourTable WHERE name = @name; 在C#中使用Sql ...

随机推荐

  1. github账号&文章选题

    ----------------------------------------------------------- https://github.com/yanpanjiao     github ...

  2. 多条命令同时执行的包concurrently

    npm i concurrently use "script":{ "client:build": "webpack --config build/w ...

  3. Java和JavaScript(函数)参数传递是按值传递还是按引用传递?

    结论:Java和JavaScript的所有(函数)参数传递都是按值传递! 1.什么是函数参数的传递是按引用传递? 什么是引用?这个概念多见于C++中,在C++中,引用解释为变量的别名. 1 #incl ...

  4. 计算机编码规则之:Base64编码

    目录 简介 Base64和它的编码原理 Base64的变体 Base64的编码细节 总结 简介 我们知道计算机中的文件可以分为两种,一种是人肉眼可读的文本类文件,一种是肉眼不可读的二进制文件.一般来说 ...

  5. uniapp报错:Browserslist: caniuse-lite is outdated. Please run next command `npm update`

    uni-app的编译器是基于npm的,依赖了众多包括mpvue.webpack在内的npm库,这些库又引用了一个三方库caniuser-lite.caniuser-lite这个库的代码里有个浏览器兼容 ...

  6. JDK内置锁深入探究

    一.序言 本文讲述仅针对 JVM 层次的内置锁,不涉及分布式锁. 锁有多种分类形式,比如公平锁与非公平锁.可重入锁与非重入锁.独享锁与共享锁.乐观锁与悲观锁.互斥锁与读写锁.自旋锁.分段锁和偏向锁/轻 ...

  7. python学习-Day5

    目录 今日学习内容详解 流程控制理论 程序执行流程分类: 重点小知识* 分支结构(if 语法) 单 if 分支 if 与 else 分支 if 与 elif 与 else 分支 if嵌套 练习 循环结 ...

  8. 漏洞复现:使用Kali制作木马程序

    漏洞复现:使用Kali制作木马程序 攻击机:Kali2019 靶机:Win7 64位 攻击步骤: 1.打开Kali2019和Win7 64位虚拟机,确定IP地址在一个网段 2.确定好IP地址,进入Ka ...

  9. CSS预编译器

    零.CSS预编译器 CSS预处理器是指对生成CSS前的某一语法的处理.CSS预处理器用一种专门的编程语言,进行Web页面样式设计,然后再编译成正常的CSS文件,供项目使用 CSS预处理器为CSS增加一 ...

  10. 公众号走走看看——js

    1.数字转换字符串/字符串转换数字 2.短循环 3.性能测试(执行时间) 4.交换值 5.合并数组(IE不兼容) 6.数组去重 7.判断给定参数是否是数字 8.获取最大最小值.取随机数(arr.len ...