MySQL order by limit排序字段有重复值导致排序随机的情况

问题描述

最近线上出现了一个问题,查询用户券数据时,发现分页返回有重复值,最后定位到是因为order by(排序)字段有大量重复值导致的。

-- 有数据再第一页和第二页重复出现了
select id from AAA where user_id='123'  ORDER BY invalid_tm desc  limit  0,10;
-- 第二页  
select id from AAA  where user_id='123'  ORDER BY invalid_tm desc  limit 10,10;

很明显,这是由于order by 排序导致的,那么mysql order by排序底层是采用什么机制呢?

order by排序

MySQL支持二种方式的排序,Index和FileSort。

  • Index排序:索引排序,即我们通常为了查询建立的索引,也就是说,Mysql能为排序和查询使用相同的索引;
  • FileSort:文件排序,当不能使用Index排序时,使用文件排序。FileSort排序有两种方法,堆排序和快速排序。

Index排序使用场景

  • where + order by 场景
    如果where 条件字段满足最左前缀,且order by字段也在组合索引字段中,则使用组合索引;
  • 只有order by场景
    order by中字段满足组合索引字段排序,则使用组合使用,
  • order by id(主键)
    不管有没有where条件,或者where 字段有没有索引,只要order by id,都会使用Index 排序,而不会使用FileSort

其余情况下,使用FileSort排序,可以通过EXPLAIN中的Extra来判断使用哪种排序方式。

image.png
image.png

FileSort排序使用场景

在不能使用Index索引排序的时候,使用FileSort排序,FileSort排序有两种方式:

  • 堆排序:在排序量不大的情况下,使用堆排序

  • 快速排序:在需要大量排序的情况下,使用快排

从执行计划中只能看出来是使用Index排序,还是FileSort排序。而使用堆排序还是快排,是Mysql根据待排序数据量的大小进行切换具体根据函数check_if_pq_applicable进行判定的。

有一个简单的判断,如果使用的是order by limit n ,且在数据量不大的情况下(数据可以在内存中加载),使用的是堆排序;
而当n到了一个数量级的时候会切换成快排,具体使用那种算法是优化器通过函数check_if_pq_applicable进行判定的。
同时,如果没有limit时,就算数据量小,使用的也是快排(这个结论是从下面的参考文章中得出的)。

在大量排序的情况下快速排序是有优势的,而堆排序使用优先队列只完成少量的排序是有优势的,因为它根本不需要排序完成只排序你需要的数据量就可以了,MySQL认为快速排序的速度是堆排序的3倍。

快速排序和归并排序

问题释疑

从导致问题的sql,我们可以判断使用的是FileSort排序中的堆排序。那么为啥堆排序会出问题呢?

因为快速排序和堆排序是不稳定的排序算法,也就是对于重复值是不能保证顺序的。而直接利用索引的话其返回数据是稳定的,因为索引的B+树叶子结点的顺序(就是聚簇索引的顺序)是唯一且一定的。
因此在这种不稳定的算法情况下上面的查询出现了不一样的结果,归根结底就是使用索引避免排序和堆排序对于重复值的处理上是不同的。

order by limit排序时,根据取值大小创建相应容量的堆,即
order by limit 0,10;——创建一个10容量的堆,然后取前10
order by limit 10,10 ——创建一个20容量的堆,然后取10到20

我们可以在java中模拟一下mysql内存中堆排序的过程,

package FileSort;
import java.util.Date;
import java.util.PriorityQueue;

/**
 * @version 1.0
 * @date 2020/11/7 14:19
 */
public class MysqlFileSort {

    public static void main(String[] args) {
        PriorityQueue<UserInfo> queue = new PriorityQueue();
        Date date = new Date();
        UserInfo req0 = new UserInfo();
        req0.setId(202847L);
        req0.setInvalidTm(date);

        UserInfo req1 = new UserInfo();
        req1.setId(202848L);
        req1.setInvalidTm(date);

        UserInfo req2 = new UserInfo();
        req2.setId(202849L);
        req2.setInvalidTm(date);

        UserInfo req3 = new UserInfo();
        req3.setId(202850L);
        req3.setInvalidTm(date);

        UserInfo req4 = new UserInfo();
        req4.setId(202851L);
        req4.setInvalidTm(date);

        UserInfo req5 = new UserInfo();
        req5.setId(202852L);
        req5.setInvalidTm(date);

        UserInfo req6 = new UserInfo();
        req6.setId(202853L);
        req6.setInvalidTm(date);

        UserInfo req7 = new UserInfo();
        req7.setId(202854L);
        req7.setInvalidTm(date);

        UserInfo req8 = new UserInfo();
        req8.setId(202855L);
        req8.setInvalidTm(date);

        UserInfo req9 = new UserInfo();
        req9.setId(202856L);
        req9.setInvalidTm(date);

        UserInfo req10 = new UserInfo();
        req10.setId(202857L);
        req10.setInvalidTm(date);

        UserInfo req11 = new UserInfo();
        req11.setId(202858L);
        req11.setInvalidTm(date);

        UserInfo req12 = new UserInfo();
        req12.setId(202859L);
        req12.setInvalidTm(date);

        UserInfo req13 = new UserInfo();
        req13.setId(2028560L);
        req13.setInvalidTm(date);

        UserInfo req14 = new UserInfo();
        req14.setId(202861L);
        req14.setInvalidTm(date);

        UserInfo req15 = new UserInfo();
        req15.setId(202862L);
        req15.setInvalidTm(date);

        UserInfo req16 = new UserInfo();
        req16.setId(202863L);
        req16.setInvalidTm(date);

        UserInfo req17 = new UserInfo();
        req17.setId(202864L);
        req17.setInvalidTm(date);

        UserInfo req18 = new UserInfo();
        req18.setId(202865L);
        req18.setInvalidTm(date);

        queue.add(req0);queue.add(req1);queue.add(req2);queue.add(req3);queue.add(req4);
        queue.add(req5);queue.add(req6);queue.add(req7);queue.add(req8);queue.add(req9);

        //limit 0,10——创建一个10容量的堆,然后取前10
//        queue.remove();
//        queue.add(req10);
//        queue.remove();
//        queue.add(req11);
//        queue.remove();
//        queue.add(req12);
//        queue.remove();
//        queue.add(req13);
//        queue.remove();
//        queue.add(req14);
//        queue.remove();
//        queue.add(req15);
//        queue.remove();
//        queue.add(req16);
//        queue.remove();
//        queue.add(req17);
//        queue.remove();
//        queue.add(req18);

        //limit 10,20 ——创建一个20容量的堆,然后取10到20
        queue.add(req10);
        queue.add(req11);
        queue.add(req12);
        queue.add(req13);
        queue.add(req14);
        queue.add(req15);
        queue.add(req16);
        queue.add(req17);
        queue.add(req18);
        while(!queue.isEmpty()) {
            UserInfo e=queue.remove();
            System.out.print(e+" ");
        }
    }
}
public class UserInfo implements Comparable<UserInfo>{

    private Long id;
    private Date invalidTm;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getInvalidTm() {
        return invalidTm;
    }

    public void setInvalidTm(Date invalidTm) {
        this.invalidTm = invalidTm;
    }

    @Override
    public int compareTo(UserInfo o) {
        if(this.invalidTm.after(o.invalidTm)) {
            return 1;
        }else if(this.invalidTm==o.invalidTm) {
            return this.invalidTm.compareTo(o.invalidTm);
        }else {
            return -1;
        }
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "id=" + id +
                ", invalidTm=" + invalidTm +
                '}';
    }
}

问题解决

从上面的介绍我们知道,是因为排序字段重复导致的,那么解决办法就是再加一个排序字段(通常是主键id),使其排序唯一,就可以解决问题了。

对于这种情况,mysql官网有详细的介绍(https://dev.mysql.com/doc/refman/5.7/en/limit-optimization.html),感兴趣的小伙伴可以自己去看下。

image.png

参考文章:
https://www.jianshu.com/p/8c2154872f83
https://www.cnblogs.com/lamp01/p/10770172.html

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