ROWNUM 是 Oracle 最常被誤解的偽欄位。本文系統剖析其「從 1 開始、逐行遞增、WHERE 階段過濾」的核心機制,揭示 ROWNUM = n (n>1)、ROWNUM > n 失效根因,提供標準分頁寫法、子查詢技巧,並介紹 12c 引入的 ANSI 標準 OFFSET ... FETCH 寫法。
1, 2, 3...。WHERE 子句在 ROWNUM 分配 之前 評估(邏輯上),但實際執行計畫中 ROWNUM 過濾推送至存取路徑。ROWNUM = 1 或 ROWNUM <= N 能直接在 WHERE 使用;ROWNUM = 2、ROWNUM > 5 永遠回傳 0 列。-- 建表測試
CREATE TABLE student (id CHAR(6), name VARCHAR2(100));
INSERT INTO student VALUES ('200001', '張一');
INSERT INTO student VALUES ('200002', '王二');
INSERT INTO student VALUES ('200003', '李三');
INSERT INTO student VALUES ('200004', '趙四');
COMMIT;
-- (1) ROWNUM = 1 正常
SELECT rownum, id, name FROM student WHERE rownum = 1;
-- 回傳第一列
-- (2) ROWNUM = 2 失效
SELECT rownum, id, name FROM student WHERE rownum = 2;
-- 回傳 0 列!因為第一列 rownum=1 不滿足 =2 被過濾,第二列永遠沒機會變成 2
-- (3) ROWNUM > 2 失效
SELECT rownum, id, name FROM student WHERE rownum > 2;
-- 回傳 0 列!同理,第一列 rownum=1 不滿足 >2 被過濾,後續列永遠無法獲得 rownum>2
SELECT *
FROM (
SELECT a.*, ROWNUM rn
FROM (
SELECT id, name
FROM student
ORDER BY id -- 必須先排序,再賦予 ROWNUM
) a
WHERE ROWNUM <= :end_row -- 先截取前 N 列
)
WHERE rn >= :start_row; -- 再過濾起始列
關鍵點:內層 ORDER BY → 中層 ROWNUM <= end → 外層 rn >= start。別名 rn 必須在中層賦予,外層才能引用。
SELECT *
FROM (
SELECT id, name, ROWNUM rn
FROM student
ORDER BY id
)
WHERE rn BETWEEN :start_row AND :end_row;
同樣依賴「先排序再賦 ROWNUM」的執行順序。
SELECT id, name
FROM student
ORDER BY id
OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY;
OFFSET 0 ROWS(相容舊邏輯)。FETCH FIRST n ROWS ONLY 簡寫。WINDOW NOSORT STOPKEY,效能與雙層子查詢相當。| 陷阱 | 現象 | 解法 |
|---|---|---|
| DISTINCT + ROWNUM | SELECT DISTINCT ... WHERE ROWNUM <= 10 可能不穩定 | 子查詢先 DISTINCT 再賦 ROWNUM |
| GROUP BY + ROWNUM | 同 DISTINCT | 子查詢先 GROUP BY |
| UNION ALL + ROWNUM | 各子查詢各自從 1 開始編號 | 外層統一賦 ROWNUM |
| 並行查詢 + ROWNUM | 並行執行時 ROWNUM 分配順序不確定 | 加 /*+ NO_PARALLEL */ 或用 OFFSET FETCH |
| ROWNUM 在 SELECT 清單中 | 顯示的是分配順序,非最終過濾後順序 | 子查詢賦別名後外層 SELECT 別名 |
ORDER BY 欄位務必有索引,避免全表排序。OFFSET 1000000 FETCH 10 仍需掃描前 100 萬列;改用「游標分頁」(記住上一頁最大 ID,WHERE id > :last_id ORDER BY id FETCH 10)。ROWNUM 的「先分配後過濾」機制是理解所有分頁查詢行為的鑰匙。掌握雙層子查詢模式與 12c OFFSET FETCH 新標準,並配合索引與游標分頁策略,即可構建高效、正確、可維護的分頁查詢方案。