记一次线上分页排序乱序问题

Mysql 分页排序出现跨页重复数据问题

1. 触发版本

MySQL 8.0

2. 问题描述

优化列表分页查询接口时,将默认的无排序条件查询修改为按照时间(create_at)条件排序,由于 order by 与 limit 混用时未加唯一限定排序条件,导致分页结果中出现了跨页重复的数据。

3. 问题复现

3.1 复现数据脚本

mysql 复制代码
-- 1. 创建测试表
DROP TABLE IF EXISTS test_pagination;
CREATE TABLE test_pagination (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    status VARCHAR(20),
    created_at DATETIME,
    updated_at DATETIME
);

-- 2. 创建存储过程
DELIMITER $$

DROP PROCEDURE IF EXISTS generate_pagination_test_data$$
CREATE PROCEDURE generate_pagination_test_data(
    IN total_rows INT,          -- 总记录数
    IN batch_size INT,          -- 批次大小(相同时间的记录数)
    IN start_date VARCHAR(19),  -- 开始时间
    IN end_date VARCHAR(19)     -- 结束时间
)
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE batch_count INT DEFAULT 0;
    DECLARE batch_time DATETIME;
    DECLARE start_time DATETIME;
    DECLARE end_time DATETIME;
    DECLARE total_seconds INT;
    DECLARE interval_seconds INT;
    DECLARE total_batches INT;
    DECLARE current_batch INT DEFAULT 0;

    -- 所有变量声明
    SET start_time = STR_TO_DATE(start_date, '%Y-%m-%d %H:%i:%s');
    SET end_time = STR_TO_DATE(end_date, '%Y-%m-%d %H:%i:%s');
    SET total_seconds = TIMESTAMPDIFF(SECOND, start_time, end_time);

    -- 计算总批次数和批次时间间隔
    SET total_batches = CEIL(total_rows / batch_size);
    SET interval_seconds = FLOOR(total_seconds / total_batches);
    IF interval_seconds < 1 THEN SET interval_seconds = 1; END IF;

    -- 设置初始批次时间
    SET batch_time = start_time;
    SET current_batch = 0;

    -- 性能优化设置
    SET autocommit = 0;
    SET unique_checks = 0;
    SET foreign_key_checks = 0;

    -- 开始生成数据
    WHILE i < total_rows DO
        -- 如果批次计数器为0,表示开始新批次
        IF batch_count = 0 THEN
            -- 为这个批次生成一个时间
            SET batch_time = DATE_ADD(start_time,
                INTERVAL current_batch * interval_seconds +
                FLOOR(RAND() * 60) SECOND  -- 加一些随机偏移
            );
            SET current_batch = current_batch + 1;
        END IF;

        -- 插入记录(同批次记录时间相同)
        INSERT INTO test_pagination (status, created_at, updated_at)
        VALUES (
            CASE
                WHEN RAND() < 0.6 THEN 'active'
                WHEN RAND() < 0.9 THEN 'pending'
                ELSE 'deleted'
            END,
            batch_time,
            batch_time
        );

        -- 更新计数器
        SET i = i + 1;
        SET batch_count = batch_count + 1;

        -- 如果达到批次大小,重置批次计数器
        IF batch_count >= batch_size THEN
            SET batch_count = 0;
        END IF;

        -- 每5000条提交一次
        IF i % 5000 = 0 THEN
            COMMIT;
        END IF;
    END WHILE;

    -- 最终提交
    COMMIT;

    -- 恢复设置
    SET autocommit = 1;
    SET unique_checks = 1;
    SET foreign_key_checks = 1;

END$$

DELIMITER ;

-- 3. 调用存储过程生成数据
-- 生成1万条数据,每批次7条相同时间,时间从2026-01-01到2026-01-01
CALL generate_pagination_test_data(
    10000,      -- 总记录数
    7,          -- 批次大小(相同时间的记录数)
    '2026-01-01 00:00:00',
    '2026-01-01 23:59:59'
);

-- 4. 清理存储过程
DROP PROCEDURE IF EXISTS generate_pagination_test_data;

3.2 复现条件

  1. 执行分页查询 (第3页, 每页10条)

    mysql 复制代码
    select * from test_pagination order by created_at limit 20, 10
  1. 执行分页查询(第4页, 每页10条)

    mysql 复制代码
    select * from test_pagination order by created_at limit 30, 10

结果中 ID 为 31 的数据再次出现,存在跨页数据重复问题。

4. 问题分析

4.1 问题溯源

与前端联调时发现分页数据重复、主键 ID 顺序不稳定,与未加排序条件前的结果不一致,定位到核心问题为ORDER BY + LIMIT的使用方式不正确所导致。

4.2 核心原理

MySQL 5.6+ 对 ORDER BY ... LIMIT n 场景做了优化

  • 若排序无法利用索引有序特性,会启用优先队列(Priority Queue) 进行排序优化。不再对全量数据排序,而是维护一个大小为limit n的堆结构,遍历数据时仅将符合条件的数据插入堆中,遍历完成后直接取堆内数据返回。
  • 当排序字段(created_at)存在重复值时,MySQL 无法保证相同时间的记录选取顺序固定,对相同时间的记录随机保留最终导致分页结果乱序、重复。

4.3 历史逻辑分析

迭代前未显式指定ORDER BY条件时,MySQL 会按聚簇索引的物理存储顺序 进行隐式排序(看似无重复),实际开发过程中中不应该依赖该特性,所有排序查询都需要显式指定ORDER BY条件。

4.4 文档资料

来源:MySQL 8.0 官方文档 - ORDER BY 优化

核心内容:

If you combine LIMIT *row_count* with ORDER BY, MySQL stops sorting as soon as it has found the first row_count rows of the sorted result, rather than sorting the entire result. If ordering is done by using an index, this is very fast. If a filesort must be done, all rows that match the query without the LIMIT clause are selected, and most or all of them are sorted, before the first row_count are found. After the initial rows have been found, MySQL does not sort any remainder of the result set.

One manifestation of this behavior is that an ORDER BY query with and without LIMIT may return rows in different order, as described later in this section.

5. 解决方案

ORDER BY的排序条件末尾补充具有唯一性的排序字段(主键 ID、唯一索引等),确保相同排序字段值的记录排序顺序固定,彻底解决分页重复问题。

5.1 优化后的式例

mysql 复制代码
-- 优化前
SELECT * FROM test_pagination ORDER BY created_at LIMIT 20, 10;

-- 优化后
SELECT * FROM test_pagination ORDER BY created_at, id LIMIT 20, 10;

5.2 优化原则

使用排序字段 + 唯一特性字段组合排

  • 使用created_at字段排序满足业务侧按时间展示需要。
  • 时间相同的记录按照主键值进行二次排序,避免分页乱序。

6. 避坑指南

  1. 所有分页查询的ORDER BY条件必须包含唯一字段(主键 / 唯一索引),避免分页乱序;
  2. 避免依赖 MySQL 隐式排序特性(无ORDER BY时按照物理存储顺序排序),显式指定排序条件;
相关推荐
henujolly20 小时前
go学习day two
后端
AI袋鼠帝20 小时前
腾讯这只小程序Agent🦀,帮我找到了最强日程、文件、知识管理姿势
后端
努力的小郑20 小时前
突发!Claude Code 51万行源码全网裸奔:一场史诗级“开源”事故,国内大厂笑麻了
前端·后端·ai编程
HashTang20 小时前
Claude Code 源码中 REPL.tsx 深度解析:一个 5005 行 React 组件的架构启示
前端·后端·ai编程
thatway198921 小时前
ARM TFM-1介绍及代码下载运行适配
后端
千寻girling1 天前
不知道 Java 全栈 + AI 编程有没有搞头 ?
前端·人工智能·后端
小码哥_常1 天前
Spring Boot 实现网络限速:让流量“收放自如”
后端
johnrui1 天前
SpringBoot-JdbcTemplate
java·spring boot·后端
Victor3561 天前
MongoDB(72)如何创建用户和角色?
后端
Victor3561 天前
MongoDB(71)如何启用MongoDB身份验证?
后端