联合索引是高效分页的基础设施,单列索引在ORDER BY + LIMIT场景下常因回表代价高而失效;必须按“过滤字段+排序字段+主键”顺序建联合索引,并配合覆盖索引与延迟关联或游标分页才能实现毫秒级响应。

联合索引本身不能“彻底解决”分页慢,但它是所有高效分页方案的基础设施——没建对,后面全白搭。
为什么ORDER BY + LIMIT必须配联合索引
单列索引(比如只在 create_time 上建索引)在 ORDER BY create_time LIMIT 100000, 20 场景下大概率失效。MySQL 会发现:排序能走索引,但后续回表取 * 太重,干脆放弃索引、改用 filesort + 全表扫描。
- 联合索引要把「过滤条件 + 排序字段 + 主键」按顺序打包,例如:
ALTER TABLE orders ADD INDEX idx_status_ctime_id (orders_status, create_time, id) - 其中
orders_status是高频WHERE条件,create_time是ORDER BY字段,id是主键——这样子查询SELECT id FROM orders WHERE orders_status = 1 ORDER BY create_time DESC LIMIT 100000, 20才能真正Using index - 如果顺序写反成
(id, create_time, orders_status),WHERE orders_status = ?就无法命中最左前缀,整个索引作废
覆盖索引 + 延迟关联:最通用的落地写法
这是线上系统最常被验证有效的组合,不依赖业务是否允许跳页,也不强求字段绝对唯一。
- 第一步:确保子查询只查索引列(通常是
id),且该查询能走覆盖索引:SELECT id FROM orders WHERE orders_status = 1 ORDER BY create_time DESC LIMIT 100000, 20
- 第二步:用
INNER JOIN关联原表取完整数据,避免IN(MySQL 不支持子查询中用LIMIT):SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders WHERE orders_status = 1 ORDER BY create_time DESC LIMIT 100000, 20) AS tmp ON o.id = tmp.id
- 注意
tmp.id列表长度别超max_allowed_packet,生产环境建议单次不超过 500 个id;若需更多,拆成多次查或换游标法
游标分页(书签法):性能最稳但有约束
当业务接受“只能下一页/上一页”,且排序字段天然单调(如 id 或带唯一约束的 create_time),这招能让查询耗时恒定在毫秒级。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 初始页查出最后一条记录的排序值:
SELECT id, create_time FROM orders WHERE orders_status = 1 ORDER BY create_time DESC, id DESC LIMIT 20
- 下一页用复合条件精准定位起点(避免
create_time重复导致漏行):SELECT * FROM orders WHERE orders_status = 1 AND (create_time
- 关键点:必须用
或>而非=单独判断时间;否则高并发插入同秒数据时会跳过部分记录
这几个细节一错,联合索引就等于没建
哪怕索引定义完全正确,以下任意一点都会让优化器绕开它:
-
WHERE条件用了函数,比如WHERE DATE(create_time) = '2024-01-01'→ 改成WHERE create_time >= '2024-01-01' AND create_time - 排序字段含大量
NULL值,且未在索引中显式声明NOT NULL或用IFNULL处理,MySQL 可能拒绝用该索引做排序 - 联合索引中某列是字符串类型,但查询时传入数字(如
user_id = 123而user_id是VARCHAR),触发隐式类型转换,直接失效 - 表里有上千万行且并发高,即使走对了索引,单次
JOIN回表也可能锁住多个页;这时得配合READ COMMITTED隔离级别或加缓存兜底
真正卡住人的从来不是“要不要建索引”,而是“建在哪几列、什么顺序、怎么查”。联合索引只是把路铺好,走不走得通,还得看 SQL 怎么写、数据怎么分布、边界条件怎么兜住。










