updated_time 一样排序不一样:PolarDB 与 SelectDB 排查记录

MySQL PolarDB SelectDB 数据库

前言

同一条 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 里的顺序,不要当成契约。