你写过 `SELECT * FROM posts` 这样的查询吗?大概率写过。但你可能没意识到,这条 SQL 在 PostgreSQL 里**不保证返回顺序**——数据库可以按任何顺序给你结果,今天按 id 排,明天可能按物理存储排,后天升级个版本又变了。可现实是,几乎所有代码和测试都在默默依赖这个"碰巧"的顺序:分页接口第二页和第一页出现重复数据、测试偶尔失败、线上偶发诡异 Bug……这类问题你没法用 grep 搜出来,因为代码里根本没有"排序"两个字。

这篇文章的作者来自 Shopify 的 Rails 基础设施团队,他们干了一件很"混沌工程"的事:**故意把每条不带 ORDER BY 的查询结果随机打乱,然后跑全量测试,看什么会挂**。思路很简单——既然数据库不承诺顺序,那我们就主动把"最坏情况"提前暴露出来,而不是等生产环境随机抽奖。

他们在 Rails、Django 和 Gitea 三个项目上都试了,每个都真挖出了值得修的问题。Rails 这边,Evil Martians 已经把这个开关合进了主干,`config.active_record.shuffle_unordered_selects`,预计 8.2 版本发布。另外他们还开源了一个 PostgreSQL 扩展叫 **pg_disorder**,可以在数据库层面做同样的事。

为什么顺序依赖是个隐形炸弹

关系数据库的 SQL 标准里,没有 ORDER BY 的查询结果顺序就是未定义的。但工程上,数据库总会按某种顺序返回——通常是物理存储顺序、索引扫描顺序、或者并行执行时的某种调度顺序。问题在于,这个顺序**不是契约**,它随时可能变。

最常见的翻车场景是分页。很多人写分页直接 `LIMIT 10 OFFSET 20`,不带 ORDER BY。在数据量小、单机部署的时候,顺序稳定,看起来没问题。但一旦数据量上来,数据库换成并行扫描,或者主从切换,顺序就变了——第一页和第二页可能出现同一条数据,也可能漏掉某条。用户看到的是"刷新一下,内容变了",你排查半天发现 SQL 没写错,就是没排序。

更隐蔽的是测试。测试里经常有"查出来第一条应该是什么"的断言,如果查询没排序,这个断言就是碰运气。CI 跑 100 次挂 1 次,你以为是偶发网络问题,实际上是顺序漂移。这类 Bug 最恶心的地方在于:**它不报错,只是偶尔给你错误的结果**,而且极难复现。

随机打乱:把混沌提前到开发期

作者的做法很直接:在测试环境里,把每个不带 ORDER BY 的查询结果**随机打乱**,然后跑测试。这样,任何依赖隐含顺序的代码都会立刻暴露——测试挂了,你就能看到是哪里在赌顺序。

这本质上是混沌工程的思想:与其等系统在不可控的时候出问题,不如主动注入故障,提前发现脆弱点。只不过这次故障注入的目标是数据库的行序。

具体实现上,Rails 的开关是在 ActiveRecord 层做的:拦截所有不带 ORDER BY 的查询,在 SQL 末尾追加一个随机排序(比如 `ORDER BY random()`),然后执行。这样应用层代码无感,但测试环境的行为就变得"不友好"了——任何隐含顺序依赖都会让测试失败。

Django 和 Gitea 那边也类似,只不过实现位置不同。Gitea 是 Go 写的,直接在数据库访问层做拦截。作者说三个项目都挖出了真问题,比如某个列表接口依赖了插入顺序、某个统计查询假设了分组顺序等。

pg_disorder:在数据库层面做同样的事

如果你不用 Rails 8.2,或者想对**所有**应用生效(包括那些直接连数据库的脚本、定时任务),可以用他们开源的 PostgreSQL 扩展 **pg_disorder**。

这个扩展的原理是在 PostgreSQL 的查询执行层做钩子:检测到 SELECT 语句没有 ORDER BY,就自动给结果集加一个随机扰动。这样不管谁发的查询,只要没显式排序,返回顺序就是乱的。

用法很简单,装好扩展后执行 `CREATE EXTENSION pg_disorder;`,然后可以按会话或按全局开启。它有几个可配置项,比如只对特定表生效、或者只对特定用户生效,方便你在灰度环境先试。

什么时候该用、什么时候别用

这个工具最适合用在**测试环境和 CI**。跑一遍全量测试,把隐藏的顺序依赖全部暴露出来,然后逐个修掉——要么加 ORDER BY,要么改断言。修完之后,你的代码就对顺序完全不敏感了,将来数据库升级、数据量增长、架构调整,都不会再被这类问题偷袭。

生产环境不建议开,因为随机排序会破坏索引利用,性能会明显下降。它的定位是"开发期体检工具",不是生产特性。

另外要注意,打乱顺序只能暴露"依赖顺序"的 Bug,不能暴露"假设唯一性"的问题——比如你假设某列没有重复值但实际有,这类问题得靠约束和唯一索引来保证。

一个值得记住的教训:**凡是没写 ORDER BY 的查询,都是潜在的定时炸弹**。与其等它炸,不如主动拆弹。这个思路不仅适用于 PostgreSQL,MySQL、SQLite 也一样——标准里都没承诺顺序。下次你写查询的时候,多问自己一句:这个结果我真的不在乎顺序吗?如果不在乎,那测试里就别断言顺序;如果在乎,就老老实实写 ORDER BY。

这个"主动注入随机性来暴露脆弱依赖"的手法,其实可以推广到很多场景:比如随机打乱 API 返回的字段顺序、随机改变定时任务的执行顺序,都能帮你发现那些"碰巧能跑"的代码。混沌工程不一定要搞多复杂的故障注入,有时候一个随机排序就够了。

阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://evilmartians.com/chronicles/dont-bet-on-select-order-shuffle-your-rows-see-what-folds