Oracle 無 LIMIT / TOP,傳統依賴 ROWNUM 偽欄位實現分頁。12c 起支援標準 SQL OFFSET ... FETCH。本文詳細對比兩種寫法、排序結合分頁的正確姿勢、效能陷阱與優化建議。
-- 前 10 筆
SELECT * FROM TEST_TABLE WHERE ROWNUM <= 10;
-- 第 11-20 筆 (經典三層寫法)
SELECT * FROM (
SELECT T.*, ROWNUM RN FROM TEST_TABLE T WHERE ROWNUM <= 20
) WHERE RN > 10;
-- 按 NAME 升序,前 10 筆
SELECT * FROM (
SELECT * FROM TEST_TABLE ORDER BY NAME ASC
) WHERE ROWNUM <= 10;
-- 按 NAME 升序,第 11-20 筆
SELECT * FROM (
SELECT TT.*, ROWNUM RN FROM (
SELECT * FROM TEST_TABLE ORDER BY NAME ASC
) TT WHERE ROWNUM <= 20
) WHERE RN > 10;
原理:ROWNUM 在 WHERE 階段產生,若先 WHERE ROWNUM <= N 再 ORDER BY,會先取前 N 筆未排序資料再排序 → 結果錯誤。必須 最內層排序 → 中層截取上限 → 外層過濾下限。
-- 前 10 筆
SELECT * FROM TEST_TABLE ORDER BY NAME ASC FETCH FIRST 10 ROWS ONLY;
-- 第 11-20 筆
SELECT * FROM TEST_TABLE ORDER BY NAME ASC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;
-- 參數化
SELECT * FROM TEST_TABLE ORDER BY NAME ASC OFFSET :offset ROWS FETCH NEXT :pagesize ROWS ONLY;
FETCH FIRST N ROWS WITH TIES 處理並列排名| 場景 | 優化策略 | 原理 |
|---|---|---|
| 大表深分頁 (如第 10000 頁) | 避免 OFFSET/ROWNUM 掃描大量捨棄行 | 改用 游標分頁 (記住上一頁最大 PK/排序鍵,下一頁 WHERE PK > :last_pk) |
| 複雜排序 + 分頁 | 建立覆蓋索引 INDEX (SORT_COL, PK) INCLUDE (SELECT_COLS) | 避免回表,Index Full Scan 直接取資料 |
| 首頁/熱門頁 | /*+ FIRST_ROWS(10) */ Hint | 引導優化器選擇 Nested Loop + Index 掃描,快速回應首筆 |
| 總筆數統計 | 快取 COUNT(*)、近似統計、或分頁查詢時 COUNT(*) OVER() (12c+) | 避免每次分頁查詢都執行一次全表 COUNT |
-- 錯誤:先取 20 筆再排序
SELECT * FROM (SELECT * FROM T WHERE ROWNUM <= 20) ORDER BY NAME;
-- 正確:最內層排序
SELECT * FROM (SELECT * FROM T ORDER BY NAME) WHERE ROWNUM <= 20;
ROWNUM 從 1 開始,無法直接 WHERE ROWNUM BETWEEN 11 AND 20 (永無結果),必須三層巢狀。SELECT DISTINCT ... WHERE ROWNUM <= N 可能回傳 < N 筆,因為 DISTINCT 在 ROWNUM 之後執行。解法:最內層 SELECT DISTINCT ... ORDER BY ...。
CREATE OR REPLACE FUNCTION PAGE_QUERY(
P_SQL IN VARCHAR2, -- 含 ORDER BY 的基礎查詢
P_PAGE IN NUMBER, -- 第幾頁 (1-based)
P_SIZE IN NUMBER -- 頁大小
) RETURN SYS_REFCURSOR IS
V_CUR SYS_REFCURSOR;
V_START NUMBER := (P_PAGE - 1) * P_SIZE + 1;
V_END NUMBER := P_PAGE * P_SIZE;
V_FINAL VARCHAR2(4000);
BEGIN
V_FINAL := 'SELECT * FROM (
SELECT T.*, ROWNUM RN FROM (' || P_SQL || ') T
WHERE ROWNUM <= ' || V_END || '
) WHERE RN >= ' || V_START;
OPEN V_CUR FOR V_FINAL;
RETURN V_CUR;
END;
/
OFFSET/FETCH,程式碼可維護性最高ORDER BY,否則結果順序不確定