从ROWNUM到LIMIT:KES、Oracle与PostgreSQL的执行顺序差异解析


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

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

本文基于金仓数据库KingbaseES V9 /Oracle 19c/PostgreSQL15编写.

目录

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中:先限制行数,再执行去重

  1. 先取 10 行:扫描前 10 行物理记录(rowid 1-10)
  2. 后去重 :对这 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;

  1. 先去重 :对全表做 DISTINCT,得到所有不重复的 user_id
  2. 后取 10 行:对去重后的结果取前 10 个

结果:10 行(恰好 10 个不重复 user_id)


3.原因解析:ROWNUM 与 LIMIT 的执行顺序差异

3.1KES/Oracle:ROWNUM先于DISTINCT 生效

在 KES 和 Oracle 中,ROWNUM 是一个动态生成的伪列.它的赋值发生在数据读取阶段,早于 DISTINCTORDER 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 的执行优先级陷阱,本质上是不同数据库对行号伪列赋值时机的设计差异.关键要点回顾:

  1. KES / OracleROWNUM 赋值在 DISTINCT 之前------先截取,后去重,结果可能少于N行.
  2. PostgreSQLLIMIT 作用于最终结果------先去重,后截取,结果恰好N行.
  3. ROWNUM 阻断子查询提升 :引用 ROWNUM 的子查询,外部过滤条件无法下推,可能导致全表扫描.
  4. 最佳实践
    • 明确业务意图,选择正确的写法
    • 跨库兼容场景下,使用嵌套子查询或窗口函数
    • 避免将 DISTINCT + ROWNUM 作为"取 N 个不重复值"的手段

记住一条铁律:永远不要用 ROWNUM 去做你真正想做之外的事情.它的行为高度依赖于它在 SQL 中的位置和数据库引擎的实现细节.当你对执行顺序有一丝不确定时,窗口函数永远是最安全的选择.

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容的更新


每日心灵鸡汤: 人很少赢,但总有赢的时候!

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

相关推荐
2501_901006471 小时前
CSS如何实现多种颜色的线性渐变_使用linear-gradient()按方向和色标填色
jvm·数据库·python
2303_821287381 小时前
Golang怎么用embed嵌入SQL文件_Golang如何将SQL迁移文件嵌入Go程序统一管理【技巧】
jvm·数据库·python
m0_702036531 小时前
PHP怎么处理Eloquent Attribute Harmonization属性协调_Laravel解决数据冲突【教程】
jvm·数据库·python
蜘蛛小助理1 小时前
从 Excel 到多维表:蜘蛛表格如何解决传统数据库开发与维护痛点
数据库·人工智能·excel·数据库开发·多维表·多维表格·蜘蛛表格
iAm_Ike1 小时前
Redis怎样通过频道划分不同的日志级别
jvm·数据库·python
kexnjdcncnxjs1 小时前
CSS如何利用-nth-of-type(1)修改首个元素样式_通过位置约束精准修饰
jvm·数据库·python
dinglu1030DL1 小时前
Tailwind CSS如何实现鼠标悬停变色_使用hover-bg-blue-500类.txt
jvm·数据库·python
瀚高PG实验室1 小时前
xx000 can not wait without a pgproc
服务器·数据库·oracle·瀚高数据库
神明9311 小时前
Tailwind CSS如何实现鼠标悬停变色_使用hover-bg-blue-500类
jvm·数据库·python