
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在数据库查询优化和 SQL 迁移过程中,分页查询 是一个非常常见却又容易踩坑的场景.不同数据库虽然都提供了限制返回行数的能力,但其背后的执行顺序和语义并不完全一致.尤其是在从 Oracle 迁移到 PostgreSQL,或者在 KES 这类兼容型数据库中编写 SQL 时,
ROWNUM与LIMIT的差异往往会直接影响查询结果的正确性.在Oracle中,ROWNUM是一个具有特殊语义的伪列,它并不是简单地在最终结果集上截取数据,而是在 SQL 执行过程中较早阶段就参与了行号分配.因此,如果不了解它与ORDER BY、子查询之间的执行关系,就很容易写出看似正确、实际结果却不符合预期的 SQL.而 PostgreSQL 中的LIMIT则更接近我们直观理解中的"结果集截取",通常是在排序、分组等操作完成之后,再限制最终返回的记录数量.这种执行顺序上的差异,使得同样的分页或取 Top-N 查询,在Oracle与PostgreSQL中可能需要采用不同的写法.本文将围绕 KES、Oracle 与 PostgreSQL 在行数限制语法上的执行顺序差异 展开分析,从ROWNUM和LIMIT的基本用法入手,对比它们在排序、分页、子查询场景下的行为差异,并结合示例说明为什么有些 SQL 在不同数据库中执行结果不同.通过本文,希望能够帮助大家更清楚地理解:ROWNUM和LIMIT不只是语法差异,更代表了不同数据库执行模型和优化逻辑的差异.掌握这些细节,不仅有助于避免分页查询错误,也能为后续进行 SQL 改写、数据库迁移和性能优化打下基础.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
本文基于金仓数据库KingbaseES V9 /Oracle 19c/PostgreSQL15编写.

目录
- [1.前言:一次看似"数据丢失"的 SQL 排查](#1.前言:一次看似"数据丢失"的 SQL 排查)
- 2.问题复现:同一条SQL,为什么返回行数不一样?
-
- [2.1 KES/Oracle中:先限制行数,再执行去重](#2.1 KES/Oracle中:先限制行数,再执行去重)
- [2.2 PostgreSQL中:先完成去重,再截取结果](#2.2 PostgreSQL中:先完成去重,再截取结果)
- [3.原因解析:ROWNUM 与 LIMIT 的执行顺序差异](#3.原因解析:ROWNUM 与 LIMIT 的执行顺序差异)
-
- [3.1KES/Oracle:ROWNUM先于DISTINCT 生效](#3.1KES/Oracle:ROWNUM先于DISTINCT 生效)
- [3.2 PostgreSQL:LIMIT作用于最终结果集](#3.2 PostgreSQL:LIMIT作用于最终结果集)
- [3.3 优化器影响:ROWNUM为什么会限制子查询改写?](#3.3 优化器影响:ROWNUM为什么会限制子查询改写?)
- [4.改写建议:如何避免 DISTINCT + ROWNUM 的语义偏差?](#4.改写建议:如何避免 DISTINCT + ROWNUM 的语义偏差?)
- 5.总结:限制行数之前,先弄清它限制的是哪一步
1.前言:一次看似"数据丢失"的 SQL 排查
同样一条 SQL,换个数据库跑,行数不一样了.这不是玄学,是执行优先级的锅.
上周,一位从 Oracle 迁移到金仓数据库 KES 的开发者在群里抛出一个问题:
"我的查询明明写了 ROWNUM <= 10,为什么返回的结果有时候是 7 行、8 行,就是不到10行?而且同样的SQL 在同事的 PostgreSQL 上跑,偏偏返回的就是10行."
他跑的 SQL 是这样的:
sql
SELECT DISTINCT user_id FROM access_log WHERE rownum <= 10;
access_log 表存储的是用户访问日志,同一个 user_id 可能出现在多行中.他的本意是"取前 10 个不重复的用户".但实际结果却让人困惑.
如果你也遇到过类似的问题,或者你正在从 Oracle 迁移到 KES / PostgreSQL,这篇文章将帮你彻底理清背后的执行优先级差异,避免在后续开发中踩同样的坑.
2.问题复现:同一条SQL,为什么返回行数不一样?
让我们用一个简单的数据集来复现这个现象.假设 access_log 表的前 15 行数据如下:
| rowid | user_id |
|---|---|
| 1 | A |
| 2 | A |
| 3 | B |
| 4 | C |
| 5 | A |
| 6 | D |
| 7 | E |
| 8 | B |
| 9 | F |
| 10 | G |
| 11 | H |
| 12 | C |
| 13 | I |
| 14 | J |
| 15 | K |
执行 SELECT DISTINCT user_id FROM access_log WHERE rownum <= 10; 时:
2.1 KES/Oracle中:先限制行数,再执行去重
- 先取 10 行:扫描前 10 行物理记录(rowid 1-10)
- 后去重 :对这 10 行做
DISTINCT,得到 A、B、C、D、E、F、G
结果:7 行(而非 10 行)
2.2 PostgreSQL中:先完成去重,再截取结果
PG 使用 LIMIT 而非 ROWNUM,等价 SQL 为 SELECT DISTINCT user_id FROM access_log LIMIT 10;:
- 先去重 :对全表做
DISTINCT,得到所有不重复的 user_id - 后取 10 行:对去重后的结果取前 10 个
结果:10 行(恰好 10 个不重复 user_id)
3.原因解析:ROWNUM 与 LIMIT 的执行顺序差异
3.1KES/Oracle:ROWNUM先于DISTINCT 生效
在 KES 和 Oracle 中,ROWNUM 是一个动态生成的伪列.它的赋值发生在数据读取阶段,早于 DISTINCT、ORDER BY 等操作.
执行顺序可以概括为:
全表扫描 → 逐行赋予 ROWNUM → 过滤 ROWNUM 条件 → DISTINCT 去重 → 返回结果
关键问题在于:ROWNUM <= 10 在去重之前就截断了数据.如果前 10 行物理记录中存在大量重复值,去重后的结果自然会少于10行.
用流程图表述:
原始 10 行: A A B C A D E B F G
↓ DISTINCT 去重
结果 7 行: A B C D E F G
3.2 PostgreSQL:LIMIT作用于最终结果集
PostgreSQL 的 LIMIT 作用于最终结果集.执行顺序为:
全表扫描 → DISTINCT 去重 → LIMIT 截取前 N 行 → 返回结果
这种语义更符合大多数开发者的直觉------"我要 10 个不重复的值".
3.3 优化器影响:ROWNUM为什么会限制子查询改写?
更深入地说,ROWNUM 的存在还会影响优化器的决策.在KES/Oracle中,当子查询内部引用了 ROWNUM 时,外部查询的过滤条件无法下推到子查询中(这一优化技术称为"子查询提升"或"Pull-up").
这意味着:
sql
SELECT * FROM (
SELECT DISTINCT user_id FROM access_log WHERE rownum <= 10
) t WHERE t.user_id = 'A';
在这条 SQL 中,WHERE t.user_id = 'A' 这个外部过滤条件无法被下推到子查询内部.优化器被迫先对子查询做全表扫描(取前10行),然后在外层做过滤.如果数据量很大,这可能导致不必要的性能损耗.
相比之下,如果将 ROWNUM 替换为 LIMIT,PostgreSQL 的优化器通常可以将外部条件下推,从而减少扫描范围.
4.改写建议:如何避免 DISTINCT + ROWNUM 的语义偏差?
4.1方案一:用嵌套子查询固定执行顺序(推荐)
如果你确实需要"先取 N 行,再去重"的 Oracle/KES 语义,但希望在 PG 上得到一致结果,使用嵌套子查询:
sql
-- KES / Oracle / PG 均可执行,行为一致
SELECT DISTINCT user_id FROM (
SELECT user_id FROM access_log WHERE rownum <= 10
) t;
或者在 PG 中:
sql
SELECT DISTINCT user_id FROM (
SELECT user_id FROM access_log LIMIT 10
) t;
4.2方案二:先明确业务目标:取前N行,还是取N个唯一值?
问自己一个问题:你的业务到底想要什么?
| 业务意图 | KES / Oracle 写法 | PG 写法 |
|---|---|---|
| 取前 N 行物理记录,然后去重 | SELECT DISTINCT ... WHERE rownum <= N |
用子查询 + LIMIT |
| 取 N 个不重复的值 | 嵌套子查询 或 ROW_NUMBER() |
SELECT DISTINCT ... LIMIT N |
大多数情况下,开发者的真实意图是后者------"我要 N 个不重复的值".在这种情况下,KES/Oracle 中的 DISTINCT + ROWNUM 组合其实是写错了.
4.3方案三:使用窗口函数实现更精确的取数控制
如果你需要对排序、去重、截断的顺序有完全精确的控制,使用窗口函数是最可靠的方式:
sql
-- 先按 user_id 分组,取每个 user_id 的最小 rowid,然后取前 10 个
SELECT user_id FROM (
SELECT user_id,
ROW_NUMBER() OVER (ORDER BY MIN(rowid)) AS rn
FROM access_log
GROUP BY user_id
) t WHERE rn <= 10;
这种写法在所有数据库中行为一致,且语义最为明确.
5.总结:限制行数之前,先弄清它限制的是哪一步
DISTINCT + ROWNUM 的执行优先级陷阱,本质上是不同数据库对行号伪列赋值时机的设计差异.关键要点回顾:
- KES / Oracle :
ROWNUM赋值在DISTINCT之前------先截取,后去重,结果可能少于N行. - PostgreSQL :
LIMIT作用于最终结果------先去重,后截取,结果恰好N行. - ROWNUM 阻断子查询提升 :引用
ROWNUM的子查询,外部过滤条件无法下推,可能导致全表扫描. - 最佳实践 :
- 明确业务意图,选择正确的写法
- 跨库兼容场景下,使用嵌套子查询或窗口函数
- 避免将
DISTINCT + ROWNUM作为"取 N 个不重复值"的手段
记住一条铁律:永远不要用 ROWNUM 去做你真正想做之外的事情.它的行为高度依赖于它在 SQL 中的位置和数据库引擎的实现细节.当你对执行顺序有一丝不确定时,窗口函数永远是最安全的选择.

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容的更新
每日心灵鸡汤: 人很少赢,但总有赢的时候!
读到一个话题:自己拼命备考,万一没考上,岂不是浪费了时间和精力.一位上岸的博主分享了自己的感受:备考时,我也问过自己这样的问题,如果没考上,那之前所有努力不也白费了吗?后来偶然读到《杀死一只知更鸟》让我醍醐灌顶,勇敢就是当你还没开始时就知道自己注定会输,但还是义无反顾的往前走,并且无论发生什么都坚持到底,你很少能赢,但总有赢的时候.你会经历一段艰难的时光,低谷,内耗,甚至自我怀疑,但没关系,沉淀自有意义,终会迎来豁然开朗的一刻.当你发现还有书可以读,还有试可以考,在某种程度上来讲也是一种幸福,说明你还有机会,请你一而再再而三的救自己于水火之中,纵使知道好运难以降临到我身上,但我也不愿意放弃努力的机会,我想这就是备考的意义.
