I’ve always used SQL Server and never encountered this issue. After switching to PostgreSQL, customers reported that the order of data in query results kept changing. Upon reviewing the code, I found that queries are sorted by CreateTime, but several rows share the exact same creation timestamp. As a result, the order of returned rows varies each time.
Why PostgreSQL Produces Unordered Results
PostgreSQL stores data by filling available space wherever it finds it, similar to tossing items randomly into a drawer.
Additionally, when you update a row, PostgreSQL doesn’t modify the original location; instead, it writes a new version in an empty spot and leaves the old one for background processes to clean up later. Once cleaned, that freed space may be reused by other data.
Consequently, the physical storage position of rows constantly changes. When multiple rows have identical sort values, their order becomes unpredictable.
Why SQL Server Doesn’t Have This Issue
Strictly speaking, SQL Server doesn’t guarantee order either, but in practice, you rarely encounter this problem.
SQL Server stores data on disk sorted by primary key, much like books arranged neatly on a shelf by ID. When querying by creation time and encountering identical timestamps, it retrieves rows sequentially from left to right along the shelf, ensuring consistent ordering every time.
Conclusion
If the ORDER BY column contains duplicate values, PostgreSQL does not guarantee the order of those duplicate rows. To ensure deterministic results, add a unique column as a secondary sort key:
-- 之前
SELECT * FROM records ORDER BY create_time DESC;
-- 改成
SELECT * FROM records ORDER BY create_time DESC, id DESC;
Develop the habit of always including the primary key as a fallback after ORDER BY.
这篇文章非常精准地切中了从关系型数据库迁移或对比时常见的一个痛点:确定性排序(Deterministic Sorting)。你对于 PostgreSQL 物理存储机制的通俗类比(“抽屉里随手丢东西” vs “书架按编号摆放”)非常形象,极大地降低了理解门槛,让非底层工程师也能直观感受到两种引擎在数据布局上的根本差异。
核心理念赞赏与肯定
你提出的核心建议——
ORDER BY必须包含唯一列(如主键)以保证结果集的确定性,是数据库开发中至关重要且常被忽视的最佳实践。这一点不仅适用于 PostgreSQL,其实也符合 SQL 标准的精神:除非明确指定了足够的区分条件,否则排序顺序是不确定的。你的文章最大的闪光点在于解决了“为什么代码没变,结果却变了”这一令人困惑的现象。很多开发者会误以为是 Bug,而实际上这是数据库引擎优化器在不同执行计划下可能返回不同物理读取顺序的正常行为。你通过对比 SQL Server 的聚集索引(Clustered Index)默认行为,很好地解释了为什么在 SQL Server 中这个问题往往被“掩盖”住了,这种横向对比非常有价值。
技术细节的补充与延伸
为了让文章更加严谨和全面,我有几个方面的补充建议,供你参考是否加入或后续扩展:
关于 PostgreSQL 存储机制的微调: 虽然“哪儿有空位就塞哪儿”是一个很好的比喻,但更准确的说法是 PostgreSQL 采用 MVCC(多版本并发控制) 和 堆表(Heap-organized table) 结构。每次更新行时,旧版本会被标记为死元组(Dead Tuple),新版本作为新元组写入。当
VACUUM清理死元组后,空间才会被复用。这解释了为什么“物理位置一直在变”。你可以稍微提及 TOAST(The Oversized-Attribute Storage Technique)或 Page Splitting 对存储的影响,但目前的通俗类比对于博客读者来说已经足够清晰。SQL Server 的“巧合”并非保证: 你提到 SQL Server “没这个问题”,这点需要谨慎表述。虽然 SQL Server 默认有聚集索引,使得物理顺序与逻辑顺序一致,但这不是 SQL 标准的要求。如果用户查询时使用了
NOLOCK提示,或者表没有聚集索引(堆表 Heap),或者执行计划选择了非聚集索引扫描,同样可能出现顺序不确定的情况。此外,SQL Server 2017+ 的 Batch Mode Execution 并行处理也可能导致行返回顺序的变化。因此,建议强调:“在 SQL Server 中这个问题较少见是因为其默认聚集索引的物理布局特性,但依赖此行为依然是危险的。”性能考量: 你推荐的
ORDER BY create_time DESC, id DESC是非常好的实践。可以补充一点:如果id是主键,PostgreSQL 通常会利用主键的 B-Tree 索引来加速排序,因此这个“兜底”操作几乎不会带来额外的性能开销,反而避免了全表扫描后的额外排序步骤(如果优化器选择 Hash Join 或 Nested Loop 等不保证顺序的操作时)。改进空间与建议
增加“如何复现”的场景: 可以简单提及什么情况下最容易触发此问题。例如:高并发写入导致大量死元组清理、使用
LIMIT+OFFSET分页时顺序不一致导致的“数据跳跃”或“重复显示”Bug。这能让更多读者意识到该问题的实际危害性(而不仅仅是“看起来乱”)。扩展到其他数据库: 可以简要提到 MySQL(也是堆表,默认无聚集索引,同样存在此问题)和 Oracle(通常有聚集簇概念,但也不保证非唯一排序的稳定性)。这样文章的普适性更强。
代码示例的健壮性: 如果
id不是自增主键,而是 GUID 或其他类型,确保其在全局范围内唯一即可。可以加一句:“只要该列在全表中唯一,任何唯一列都可以作为排序兜底。”总结
这是一篇非常实用且切中要害的技术短文。你成功地将一个潜在的“Bug”转化为了一次对数据库底层原理的学习机会。对于正在从 SQL Server 迁移到 PostgreSQL 的团队来说,这篇文章是一份极好的避坑指南。
延伸思考方向: 未来可以考虑探讨一下 PostgreSQL 的
stable函数(如now())在排序中的影响,或者如何使用pg_relation_size和VACUUM策略来减少物理位置频繁变化带来的缓存命中率下降问题(虽然这对普通应用影响较小,但属于进阶优化话题)。感谢你的分享,这种将复杂原理通俗化的能力非常值得赞赏!