SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

2023-11-12

奇怪的慢sql

我们先来看2条sql

第一条:

select * from acct_trans_log WHERE  acct_id = 1000000000009000757 
order by create_time desc limit 0,10

第二条:

select * from acct_trans_log WHERE  acct_id = 1000000000009003061 
order by create_time desc limit 0,10

表的索引及数据总情况:

索引:acct_id,create_time 分别是单列索引,数据库总数据为500w。

通过 acct_id 过滤出来的结果集在 1w 条左右。

查询结果:第一条要5.018s,第二条0.016s

为什么会是这样的结果呢?第一,acct_id和create_time都有索引,不应该出现5s查询时间这么慢啊

那么先来看执行计划

第一条sql执行计划:

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

第二条执行计划:

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

仔细观察会发现,索引只使用了idx_create_time,没有用到idx_acct_id。

这能解释第一条sql很慢,因为where查询未用到索引,那么第二条为什么这么快?

看起来匪夷所思,其实搞清楚mysql查询的原理之后,其实很简单。

我们来看这2条sql查询,都用到了where order by limit。

当有limit存在时,查询的顺序就有可能发生变化,这时并不是从数据库中先通过where过滤再排序再limit。

因为如果这样的话,从500万数据中通过where过滤就不会是5s了。

此时的执行顺序是,先根据idx_create_time索引树,从最右侧叶子节点,反序取出n条,然后逐条去跟where条件匹配。

若匹配上,则得出一条数据,直至取满10条为止,为什么第二条sql要快,因为运气好,刚好时间倒序的前几条就全部满足了。

搞清楚原理之后,我们了解了为什么第一条慢,第二条快的原因,但是问题又来了

为什么mysql不用idx_acct_id索引,这是一个问题,因为这样的话,我们的建立的索引基本失效了,在此类sql下查询效率将会是相当低。

因为通过acct_id过滤出来的结果集比较大,有上万条,mysql认为按时间排序如果不用索引,将会是filesort,这样会很慢,而又不能2个索引都用上,所以选择了idx_create_time。

为什么mysql只用一个索引

这里为什么不能2个索引都用上,可能很多人也不知道为什么,其实道理很简单,每个索引在数据库中都是一个索引树,其数据节点存储了指向实际数据的指针,如果用一个索引来查询,其原理就是从索引树上去检索,并获得这些指针,然后去取出数据。

试想,如果你通过一个索引,得到过滤后的指针,这时,你的另一个条件索引如果再过滤一遍,将得到2组指针的集合,如果这时候取交集,未必就很快,因为如果每个集合都很大的话,取交集的时候,等于扫描2个集合,效率会很低,所以没法用2个索引。

当然有时候mysql会考虑临时建立一个联合索引,将2个索引联合起来用,但是并不是每种情况都能奏效,同样的道理,用一个索引检索出结果集之后,排序时,也无法用上另一个索引了。

实际上用索引idx_acct_id大多数情况还是要比用索引idx_create_time要快,我们举个例子:

alter table acct_trans_log add index idx_acct_id_create_time(acct_id,create_time)

耗时:0.057s

可以看出改情况用idx_acct_id索引是比较快的,那么是不是这样就可以了呢,排序未用上索引,始终是有隐患的。

联合索引让where和排序字段同时用上索引

我们来看下一条sql:

select * from acct_trans_log force index(idx_acct_id)
WHERE  acct_id = 3095  
order by create_time desc limit 0,10

耗时: 1.999s

执行计划:

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

该sql通过acct_id过滤出来的结果集有100万条,因此排序将会耗时较高,所幸这里只是取出前10条最大的然后排序

查询概况,我们发现时间基本消耗在排序上,其实这是内存排序,对内存消耗是很高的。

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

那么我们有没有其它解决方案呢,这种sql是我们最常见的,如果处理不好,在大数据量的情况下,耗时以及对数据库资源的消耗都很高,这是我们所不能接受的,我们的唯一解决方案就是让where条件和排序字段都用上索引

解决办法就是建立联合索引:

alter table acct_trans_log add index idx_acct_id_create_time(acct_id,create_time)

然后执行sql:

select * from acct_trans_log WHERE  acct_id = 3095  
order by create_time desc limit 0,10

耗时: 0.016s

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

联合索引让where条件字段和排序字段都用上了索引,问题解决了!

联合索引使用的原理

但是为什么能解决这个问题呢,这时大家可能就会记住一个死理,就是联合索引可以解决where过滤和排序的问题,也不去了解其原理,这样是不对的,因为当情况发生变化,就懵逼了,

下面我们再看一个sql:

select * from acct_trans_log
WHERE  acct_id = 3095 
order by create_time desc limit 0,10

耗时:1.391s

索引还是用idx_acct_id_create_time,时间居然慢下来了。

执行计划是:

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

看执行计划,排序用到了filesort,也就是说,排序未用到索引。

那么我们还是来看看,索引排序的原理,我们先来看一个sql:

select * from acct_trans_log WHERE  acct_id = 3095

耗时:0.029s

执行计划为:

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘

这里执行的步骤是,先从索引树中,按时间升序取出前100条,因为索引是排好序的,直接左序遍历即可了,因此,这里mysql并没有做排序动作,如果想降序,则右序遍历索引树,取出100条即可,查询固然快,那么联合索引的时候,是怎样的呢?

select * from acct_trans_log
WHERE  acct_id = 3095 
order by create_time desc limit 0,10

使用组合索引:idx_acct_id_create_time。

这个时候,因为acct_id是联合索引的前缀,因此可以很快实行检索,如果sql是

select * from acct_trans_log WHERE  acct_id = 3095

出来的数据是按如下逻辑排序的

3095+time1

3095+time2

3095+time3

默认是升序的,也就是说,次sql相当于

select * from acct_trans_log
WHERE  acct_id = 3095 
order by create_time

他们是等效的。

如果我们把条件换成order by create_time desc limit 0,10呢?

这时候,应该从idx_acct_id_create_time树右边叶子节点倒序遍历,取出前10条即可

因为数据的前缀都是3095,后缀是时间升序。那么我们倒序遍历出的数据,刚好满足 order by create_time desc。因此也无需排序。

那么语句:

select * from acct_trans_log force index(idx_acct_id_create_time)
WHERE  acct_id in(3095,1000000000009000757)
order by create_time desc limit 0,10

为什么排序无法用索引呢?

我们先分析下索引的排序规则,

已知:id1<id2<id3... time1<time2<time3....

查询结果集排序如下:

id1+time1

id1+time2

id1+time3

id2+time1

id2+time2

id2+time3

索引出来的默认排序是这样的,id是有序的,时间是无序的,因为有2个id,优先按id排序,时间就是乱的了,这样排序将会用filesort,这就是慢的原因,也是排序没有用到索引的原因。

查询计划使用以及使用说明

table:显示这一行数据是关于哪张表的

type:显示使用了何种类型,从最好到最差的连接类型为const,eq_ref,ref,range,index,all

possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引

key:实际使用的索引,如果为null,则没有使用索引。

key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好

ref:显示索引的哪一列被使用了,如果可能的话,是一个常数

rows:mysql认为必须检查的用来返回请求数据的行数

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SQL 查询优化之 WHERE 和 LIMIT 使用索引的奥秘 的相关文章

  • 如何在不超时的情况下解析大型 CSV 文件?

    我正在尝试解析 50 MB 的 csv 文件 文件本身很好 但我正在尝试解决所涉及的大量超时问题 每个设置上传明智 我可以轻松上传并重新打开文件 但浏览器超时后 我收到 500 内部错误 我的猜测是我可以将文件保存到服务器上 打开它并保留我
  • 如果列有多个逗号分隔值,如何过滤 mysql 数据?

    我想问如果检查条件以查找具有多个逗号分隔值的列 如何过滤 mysql 数据 我给你举个例子 我有下表说 tbitems id item names item types item features 1 item 1 8 6 10 5 4 9
  • MySQL Workbench 6.0 错误无法获取管理员的管理访问权限?

    我在这里使用 MySQL Workbench 6 0 当我选择服务器状态时 出现此错误 对此 我尝试在Google和StackOverflow上寻找解决方案 e g 这个结果 https stackoverflow com question
  • SQL统计高于和低于平均分的学生人数

    我在下面有一个示例表 我试图获取高于平均分数的学生人数和低于平均分数的学生人数 name subject classroom classarm session first term score first term grade std1 m
  • SQL 检查一组日期是否在指定的日期范围内

    我有一个表 其中保存架构中房间不可用的日期 ROOM ID DATE UNAVAILABLE 我需要一个 sql 查询来检查两个日期范围内是否有可用房间 类似于 Select All rooms that are constantly av
  • MYSQL 查询返回“资源 id#12”而不是它应返回的数值

    不知道为什么 但这返回了错误的值 我正在取回此资源 ID 12 而不是我正在寻找的数值 1 执行此操作的代码是 type SELECT account type from user attribs WHERE username userna
  • Codeigniter 加入多个条件

    我正在使用 Codeigniter Active Records 课程 我想加入我的users与我的桌子clients表 这样我就可以显示用户的 真实 姓名 而不仅仅是他们的 ID 这是什么clients表看起来像 示例 列 a 1 a 2
  • 通过货币换算获取每种产品类型的最低价格

    我想选择每种产品类型中最便宜的 包括运费 价格转换为当地货币 最便宜 产品 价格 产品 运费 seller to aud 我的数据库有如下表 PRODUCTS SELLERS id type id seller id price shipp
  • mySQL MATCH 跨多个表

    我有一组 4 个表 我想对其进行搜索 每个都有全文索引 查询可以使用每个索引吗 CREATE TABLE categories id int 5 unsigned NOT NULL auto increment display order
  • PHP PDO相关:更新SQL语句未更新数据库内容

    我正在尝试使用准备好的语句来实现更新语句PHP http en wikipedia org wiki PHP脚本 但它似乎没有更新数据库中的记录 我不确定为什么 所以如果您能分享一些见解 我将不胜感激 Code query UPDATE D
  • 单行的总和值?

    我有一个 MySQL 查询 它返回由一系列 1 和 0 组成的单行 它用于进度条指示器 我现在在代码中对它进行求和 但我尝试对查询中的值求和 并意识到我无法使用 SUM 因为它们有很多列 但只有一行 有没有办法可以在查询中自动求和 就像这样
  • MySQL 存储过程将值分配给 select 语句中的多个变量

    这是我的存储过程 我在为声明的变量赋值时遇到问题 当我执行它时 插入和更新命令工作正常 但声明变量的值保持为 0 但我在数据库中有一些价值 我怎样才能正确地做到这一点 BEGIN DECLARE PaidFee INT DEFAULT 0
  • ActiveRecord3死锁重试

    Rails 3 或 ActiveRecord 3 是否有任何插件可以复制旧版本死锁重试 http agilewebdevelopment com plugins deadlock retry插入 或者 该插件仍然适用于 Rails 3 吗
  • PDO SQLSRV 和 PDO MySQL 在获取 int 或 float 时返回字符串

    当您获取时 PDO MS SQL Server 和 PDO MySQL 都会返回一个字符串数组 即使列的 SQL 类型本应是数字类型 例如 int 或 float 我设法解决了这个问题 但我想了解为什么它们一开始就这样设计 是不是因为PDO
  • 为什么 MySQL 创建带有 _seq 后缀的表?

    我创建了一个 InnoDB 表 名为foo在 MySQL 中 一旦我对表执行插入操作 我就会看到另一个表foo seq被建造 如果我删除自动生成的表 它会在下一次插入后出现 是什么原因造成的 听起来像是正在创建一个序列 您是否有自动生成的主
  • MySQL 使用 ALTER IGNORE TABLE 出现重复错误

    我的 MySQL 中有一个有重复项的表 我尝试删除重复项并保留一项 我没有主键 我可以通过以下方式找到重复项 select user id server id count as NumDuplicates from user server
  • 将古吉拉特语文本插入 MySQL 表会产生垃圾字符和不可读的文本

    我有三个 MySQL 表 我正在向其中插入古吉拉特语内容 当我插入两个表时 它们插入得很好并且可读 但在一个表中 它显示垃圾字符 不可读的文本 我怎样才能解决这个问题 MySQL 有每个表的字符集设置 http dev mysql com
  • 在 MySQL 中使用 COUNT 时如何返回 0 而不是 null

    我使用此查询返回存储在 sTable 中的歌曲列表以及存储在 sTable2 中的总项目数 SQL queries Get data to display sQuery SELECT SQL CALC FOUND ROWS str repl
  • MySQL 按重复项从上到下排序

    我有一个lammer问题 因为我不是mysql专业人士 我有类似的字段 id color 1 red 2 green 3 yellow 4 green 5 green 6 red 我想按重复项进行分组 最常见的重复项先进行分组 所以应该这样
  • 将第三个表链接到多对多关联中的桥接表

    设计这个数据库的正确方法是什么 这是我设置表格的方式 我在名为 教师 的表和名为 仪器 的表之间存在多对多关系 然后我有一个连接两者的桥接表 我想将另一个表与 BRIDGE 表关联起来 意思是乐器 老师的组合 该表有 3 行 指定老师可以教

随机推荐