前言
同一条 ORDER BY updated_time DESC,PolarDB 和 SelectDB 打出来的时间一模一样,前两行的先后是反的。第三行两边都在最后。
一开始怀疑同步把小数秒吃了,或者两个库的字段精度不一样。把类型和原始值对完,时间就是同一个秒。顺序不同,是相等键没有次序,两边打破平局的方式不一样。
问题现象
下面用一张订单表来说明。PolarDB 是 MySQL 8.0.18,SelectDB 的引擎是 Doris。select version() 在 SelectDB 上返回 5.7.99,这是它对外的 MySQL 协议版本,不是真的跑在 5.7 上。
select id, order_no, updated_time
from order_bill
where biz_no = 'BIZ-001'
order by updated_time desc;
PolarDB:
1002 SO-1002 2026-10-22 15:44:31
1003 SO-1003 2026-10-22 15:44:31
1001 SO-1001 2026-10-22 15:42:27
SelectDB:
1003 SO-1003 2026-10-22 15:44:31
1002 SO-1002 2026-10-22 15:44:31
1001 SO-1001 2026-10-22 15:42:27
1002 < 1003。分歧只在 15:44:31 那两行。再查一次 SelectDB,顺序不变。
时间是同一个值
information_schema.COLUMNS 里,两边的 updated_time 都是 datetime,DATETIME_PRECISION = 0。
select id,
date_format(updated_time, '%Y-%m-%d %H:%i:%s.%f') as updated_us,
unix_timestamp(updated_time) as updated_unix,
hex(updated_time) as updated_hex
from order_bill
where id in (1002, 1003);
%f 是 000000,unix_timestamp 相同,hex(updated_time) 也相同,就是秒级字符串 2026-10-22 15:44:31。秒以下没有被客户端藏起来的小数。SelectDB 上同样。
这两行的创建时间、更新时间、审核时间都是 15:44:31。另一个业务时间差了 2 秒,写进秒级字段之后收成了同一个值。SQL 也没有按那个字段排。
换索引,顺序就翻了
过滤列 biz_no 上有二级索引 idx_biz_no,updated_time 上有二级索引 idx_updated_time。不加 hint 时,EXPLAIN 走 idx_biz_no,不走 idx_updated_time。
select id, order_no, updated_time
from order_bill force index (idx_updated_time)
where biz_no = 'BIZ-001'
order by updated_time desc;
强制走 idx_updated_time 之后,PolarDB 也变成大 id 在前,和 SelectDB 一致。
时间没有高低。变的是平局交给谁来打破。
二级索引的叶子顺序
InnoDB 的二级索引叶子会带上主键。建表语句里只有一列,叶子上实际是两列。
idx_biz_no 的叶子顺序是 (biz_no, id)。按这个条件读出来是 id 升序:
1001 2026-10-22 15:42:27
1002 2026-10-22 15:44:31
1003 2026-10-22 15:44:31
原 SQL 要按 updated_time 排序,这列不在 idx_biz_no 里,所以还会 filesort。两行 updated_time 完全相等,filesort 把进入排序前的相对顺序留了下来。进入排序前就是上面这个 id 升序,所以默认结果是小 id 在前。
这个 id 升序是扫描顺序留下来的,不是 ORDER BY 的第二排序键。
idx_updated_time 的叶子顺序是 (updated_time, id)。同一秒里面,叶子上仍然是 id 升序。ORDER BY updated_time DESC 可以倒着扫这棵索引。MySQL 8.0.13 之后,EXPLAIN 的 Extra 里会看到 Backward index scan,并且没有 Using filesort。倒扫把索引里的两列一起倒过来,同一秒内大 id 在前。FORCE INDEX (idx_updated_time) 之后顺序翻过来,就是这个原因。
怎么看到叶子里的顺序
云上的 PolarDB 拿不到 .ibd,innodb_ruby、innblock 这种拆页工具用不上。optimizer_trace 也只看得到选了哪个 range,看不到这三行谁在前。
服务器自己顺着索引读,就是叶子顺序。条件是 EXPLAIN 的 Extra 里没有 Using filesort。
看 idx_biz_no,不要再写一个会触发 filesort 的 ORDER BY:
explain
select id, order_no, updated_time
from order_bill force index (idx_biz_no)
where biz_no = 'BIZ-001';
Extra 干净的话,结果就是 (biz_no, id) 的叶子顺序。
看 idx_updated_time 里同一秒的两行,把条件换成时间:
explain
select id, order_no, updated_time
from order_bill force index (idx_updated_time)
where updated_time = '2026-10-22 15:44:31'
and biz_no = 'BIZ-001';
没有 Using filesort 时,小 id 在前。再加上 ORDER BY updated_time DESC,出现 Backward index scan,大 id 在前。
想一条一条顺着叶子走,用 HANDLER:
handler order_bill open;
handler order_bill read idx_biz_no = ('BIZ-001');
handler order_bill read idx_biz_no next;
handler order_bill read idx_biz_no next;
handler order_bill close;
READ index = (值) 落在第一条匹配记录上,后面每次 NEXT 都是叶子的下一条。idx_updated_time 把等号换成 ('2026-10-22 15:44:31'),PREV 就是反方向。
SelectDB 为什么是大 id 在前
SelectDB 这张表 id 是唯一键,updated_time 和 biz_no 都不是键。没有 (biz_no, id) 这种扫描序可以留下来。
Doris 的排序用的是 pdqsort。比较结果相等时,不保留输入顺序。谁在前,取决于进排序算子之前的 tablet 扫描顺序,以及这次划分。当前数据上,结果是大 id 在前。数据不变时再查一次还是这个顺序,因为输入序列没变。
这不是 ORDER BY id DESC。compaction、均衡或者并行扫描顺序变了,平局可以变。
总结
ORDER BY updated_time DESC 只比较这一列。两行相等,SQL 不规定谁先谁后。
PolarDB 上现在看到的小 id 在前,是 idx_biz_no 的叶子顺序被 filesort 留下来的。优化器改走 idx_updated_time 的反向扫描,顺序就翻。FORCE INDEX 已经翻过一次。
SelectDB 这次大 id 在前,同样只是当前扫描加不稳定排序的结果。
要让两边次序一致,排序键得能分出胜负:
order by updated_time desc, id desc
补上 id 之后,两边都是 1003、1002、1001。
这和之前记过的 MySQL GROUP BY 隐式排序 是同一类问题。没写在 SQL 里的顺序,不要当成契约。