先给结论:不是方向问题,也不是 pinned 选择性差,是「优化器 cost 偏差 + 大表 cardinality 抽样不精确」的叠加。
MySQL 8.0 的 B+Tree 反向扫描(reverse index scan)在结构上完全可以服务「等值前缀锁定后剩余列全 DESC」的排序——你的 (receiver_id ASC, pinned ASC, ts ASC) 反向扫就是 pinned DESC, ts DESC 序。你的对比实验 A(ORDER BY ts DESC 不 filesort)已经证明 reverse scan 机制本身工作正常。所以问题不在 B+Tree 能不能走,而在优化器选没选。
为什么优化器会选错(3.2 亿行 + 210GB 这个量级特有的坑):
- InnoDB 的 cardinality 抽样对大表不精确。
ANALYZE TABLE后,某些 receiver_id 的真实行数可能只有 1 万~10 万,但优化器估算成几百万。reverse scan 路径的 cost = 估算行数 × 回表代价,被拉得很高。 - filesort + LIMIT 50 的 priority queue 代价被低估。优化器假设排序是 O(n log n),但 MySQL 对 LIMIT 小值走 priority queue,实际是 O(n log 50)——cost 模型里没有这个修正。
- 两条路径 cost 接近时,优化器偏向 filesort(因为 filesort 路径对
ORDER BY列的顺序要求更宽松,通用性更高)。
验证步骤(按顺序做):
-- 1. 刷新统计信息
ANALYZE TABLE t_message;
-- 2. 看真实分布(关键!)
SELECT pinned, COUNT(*) FROM t_message
WHERE receiver_id = 10086 GROUP BY pinned;
-- 3. optimizer_trace 看 cost 差
SET optimizer_trace="enabled=on", optimizer_trace_max_mem_size=1000000;
SELECT id, sender_id, pinned, ts FROM t_message
WHERE receiver_id = 10086
ORDER BY pinned DESC, ts DESC LIMIT 50;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
重点看 trace 里 reverse scan 路径的 rows_examined_per_scan 和 cost。如果 reverse scan 的 rows 估算是真实值的 10 倍以上,就是 cardinality 抽样不精确。
推荐方案(按可落地性排序):
方案 A(最稳):拆两段等值查询 UNION
(SELECT id, sender_id, pinned, ts FROM t_message
WHERE receiver_id = 10086 AND pinned = 1
ORDER BY ts DESC LIMIT 20)
UNION ALL
(SELECT id, sender_id, pinned, ts FROM t_message
WHERE receiver_id = 10086 AND pinned = 0
ORDER BY ts DESC LIMIT 50)
ORDER BY pinned DESC, ts DESC LIMIT 50;
每段都是「等值 + 单列 DESC」,reverse scan 100% 命中,免 filesort。置顶段通常只有几条,cost 可忽略。
方案 B(如果坚持单 SQL):建降序索引
ALTER TABLE t_message ADD INDEX idx_recv_pin_ts_desc
(receiver_id, pinned DESC, ts DESC);
显式 DESC 索引让优化器不用走 reverse scan,cost 估算更准。代价是 210GB 表加一个索引,建议低峰期做。
方案 C(兜底):FORCE INDEX + 监控
SELECT /*+ SET_VAR(optimizer_switch='prefer_ordering_index=on') */ ...
MySQL 8.0.28+ 对 priority queue 的 cost 估算有修正,如果你还在 8.0.32,先确认下 optimizer_switch 里 prefer_ordering_index 是 on。
一个容易忽略的坑:3.2 亿行大表上,EXPLAIN 的 rows 字段对 receiver_id 这种高基数列的估算经常偏乐观 1~2 个数量级。不要信 EXPLAIN 的 rows,要看 optimizer_trace 里的 rows_estimation + rows_examined_per_scan,这两个才是优化器真正的决策依据。