解决MySQL索引存储顺序和order by不一致的问题

根据指定的字段排序来显示结果,是我们写应用时最常见的需求之一了,比如一个交易系统中,按照交易时间倒序显示交易记录。相信你听说过这样的建议:如果有order by的需求,给需要排序的字段加上索引,就可以避免数据库排序操作。

所谓数据库排序操作,是指数据库在执行过程中,先将满足条件的数据全部读出来,放入内存中,再执行快排,这个内存就是sort_buffer。如果临时数据量比sort_buffer大, 就要把数据放入临时文件,然后做外部排序,这个排序过程的消耗是比较大的。

所谓避免数据库排序操作,是指执行过程中不需要快排或外部排序。

为什么加上索引就可以避免排序呢?如果索引存储顺序和order by不一致,还需要排序吗? 如果是联合索引呢?
今天我们就来说一说,建了索引以后,order by是怎么执行的以及怎么优化。

为了便于说明,我创建一个简单的表,这个表里,除了主键索引id外,还有一个联合索引ab。你可以在文稿中看到这个表的定义。

create table `t` (
    `id` int(11) NOT NULL,
    `a`  int(11) NOT NULL,
    `b`  int(11) NOT NULL,
    `c`  int(11) NOT NULL,
    primary key (`id`),
    key `ab` (`a`, `b`) 
) engine = InnoDB;

我们来看看不同的业务需求下,SQL语句怎么写,以及在MySQL里是怎么执行的。

单字段排序

一个简单的需求是将这个表的数据,按照a的大小倒序返回。你的SQL语句可以这么写:

select * from t order by a desc;

我们来看看这个联合索引ab的结构。

图1

可以看出,在这个索引上,数据存储顺序是:按照a值递增,对于a值相同的情况,按照b值递增。

因此上面这个语句的执行流程就是:

  • 从索引ab上,取最右的一个记录,取出主键值ID_Z;
  • 根据ID_Z到主键索引上取整行记录,作为结果集的第一行;
  • 在索引ab上取上一个记录的左边相邻的记录;
  • 每次取到主键id值,再到主键索引上取到整行记录,添加到结果集的下一行;
  • 重复步骤3、4,直到遍历完整个索引。

可以看出,这个流程中并不涉及到排序操作。我们也可以用explain语句来验证这个结论。下图是这个语句的explain的结果,可以看到,Extra字段中没有Using filesort字样,说明这个语句执行过程中,不需要用到排序。

图2

组合字段排序

根据上面的分析,我们再来看看下面这个语句:

select * from t order by a desc, b desc;

这个语句的意思是,按照a值倒序,当a的值相同时按照b值倒序。
你一定发现了,这个语句的执行逻辑和执行结果,跟前面的语句是一模一样的,因此也不需要排序。
倒序不需要排序,正序呢?正序的语句是这么写的:

select * from t order by a, b;

显然,这个语句也是不需要排序的,执行流程上,只需要先取ab索引树最左边的节点,然后向右遍历即可。

到这里我们可以小结一下:
InnoDB索引树以任意一个叶节点为起始点,可以向左或向右遍历;
如果语句需要的order by顺序刚好可以利用索引树的单向遍历,就可以避免排序操作。

Descending Indexes

接下来我们来看一种不满足”单向遍历“的场景。

select * from t order by a, b desc;

这个语句要求查询结果中的记录排序顺序是:按照a值正序,对于相同的a值,按照b值倒序。
由于不满足单向遍历的要求,因此只能选择使用排序操作。
下图是这个语句explain的结果。


图3

extra字段中Using filesort表示使用了排序。

你一定想到了,如果可以让InnoDB在构建索引ab的时候,相同的a里面,b能够从大到小排序,就又可以满足单向遍历的要求了。在MySQL5.7及之前的版本是不支持这么创建索引的,在8.0版本中支持了这个功能,官方名称是Descending Indexes。在8.0版本中,我们可以把索引ab的定义做个修改。

create table `t` (
    `id` int(11) NOT NULL,
    `a`  int(11) NOT NULL,
    `b`  int(11) NOT NULL,
    `c`  int(11) NOT NULL,
    primary key (`id`),
    key `ab` (`a`, `b` desc) 
) engine = InnoDB;

我们将索引ab的定义做了修改,在字段b后面加上desc,表示对于相同的a值,字段b按照倒序存储。这个表对应的索引ab的结构图如下。


image
图4

这样从左到右遍历这个索引的时候,就刚好满足a正序,然后b逆序的要求。

Descending Indexes可以避免这种情况下的排序操作,语句的执行性能自然就提升了。

应用优化

前面说过,Descending Indexes这个功能是在MySQL 8.0才支持的。那如果你的生产环境上使用的还是低于8.0的版本,有没有不需要排序的方法呢?

Of cause.

假设我们现在的需求就是在MySQL 5.7版本下,要求按照”a值正序,然后b值逆序”的顺序,返回所有行a和b的值。

首先,为了避免数据库排序,我们直接执行下面这个语句:

select a,b from t order by a, b;

当然,这个语句返回的结果集是不满足业务要求的,但是我们知道,对于相同的a值,b值是有序递增的,我们要把这个数据特点利用起来。

执行这个语句后,应用端的逻辑改造如下:
构造一个空栈(stack),栈中的节点可以保存数据行;
读入第一行,入栈;
读入下一行,
a.如果新一行中a值与上一行相同,将新一行入栈;
b.如果新一行中a值与上一行不同,则将栈中的所有数据行依次出栈并输出,直到栈清空;然后新一行入栈。
重复步骤3直到遍历完整个索引,将栈中的所有数据行依次出栈并输出,直到栈清空。

下图是用第一个图中的示例数据,执行上面的流程的效果图。


图5

可以看到,这个过程中数据库端没有使用排序,在应用端也没有使用排序。
这个过程需要在应用端构造一个栈,需要临时内存。当然这个内存并不是凭空多出来的,因为如果不使用这个方法,就只能在MySQL端排序,这个内存就会在MySQL里创建,也就是sort_buffer。

相比之下,使用应用端的内存还是比使用MySQL的内存好些,也算是这个方案的另一个优点。

总结

本文讲述了MySQL在有索引的情况下,处理order by请求的执行过程,也介绍了Descending Indexes的应用背景。

Descending Indexes是MySQL 8.0才支持的特性。在数据库不支持一些特性的时候,也可以考虑通过应用端的协作来实现业务需求。方案优化并不一定只是数据库的优化,综合考虑系统中各个模块的特性,可以增强我们解决问题的灵活性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容