使用 ROWNUM 分頁時,常遇到**跨頁資料重複**或**資料遺漏**現象。根因在於 **排序鍵不唯一** 導致相同排序值的行在分頁邊界處順序不穩定。本文深度剖析機制並給出徹底解決方案。
-- 第 1 頁 (1~10)
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (SELECT * FROM t ORDER BY status) a WHERE ROWNUM <= 10
) WHERE rn >= 1;
-- 第 2 頁 (11~20)
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (SELECT * FROM t ORDER BY status) a WHERE ROWNUM <= 20
) WHERE rn >= 11;
若 status 有大量重複值 (如 'A' 佔 80%),Oracle 排序演算法**不保證穩定性** (非 Stable Sort),兩次查詢中 status='A' 的行順序可能不同,導致第 10~11 行邊界資料重複或遺漏。
原則:ORDER BY 子句**必須包含唯一鍵** (Primary Key 或 Unique Index 欄位) 作為最後一層排序依據。
SELECT * FROM (
SELECT a.*, ROWNUM rn
FROM (SELECT * FROM t ORDER BY status, id) a -- id 為 PK
WHERE ROWNUM <= :max_row
) WHERE rn >= :min_row;
SELECT * FROM (
SELECT a.*, ROWNUM rn
FROM (SELECT * FROM t ORDER BY status, ROWID) a
WHERE ROWNUM <= :max_row
) WHERE rn >= :min_row;
ROWID 物理唯一,但跨遷移/重組後會變更,**不適合長期分頁游標**,僅作臨時方案。
若業務排序為多欄位 (status, create_date) 仍可能重複,**必須再加 PK**。
ORDER BY status, create_date, id -- 確保完全唯一
-- 連續執行兩次相同分頁查詢,比較 ID 集合是否完全一致
WITH page1 AS (SELECT id FROM (SELECT id, ROWNUM rn FROM (SELECT id FROM t ORDER BY status, id) WHERE ROWNUM <= 20) WHERE rn >= 11),
page2 AS (SELECT id FROM (SELECT id, ROWNUM rn FROM (SELECT id FROM t ORDER BY status, id) WHERE ROWNUM <= 20) WHERE rn >= 11)
SELECT CASE WHEN COUNT(*) = 0 THEN '穩定' ELSE '不穩定-有差異' END AS result
FROM (
SELECT id FROM page1 MINUS SELECT id FROM page2
UNION ALL
SELECT id FROM page2 MINUS SELECT id FROM page1
);
/*+ FIRST_ROWS(n) */ Hint 引導 NL + 索引路徑。OFFSET ... FETCH,語義相同但更清晰。ALTER TABLE MOVE, ALTER INDEX REBUILD) 會改變 ROWID,**嚴禁依賴 ROWID 做長期分頁**。