首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> Oracle 資料庫 >> Oracle 資料庫基礎

Oracle 分頁查詢最佳實踐:OFFSET/FETCH (12c+)、ROWNUM 傳統寫法與存儲過程封裝

     編輯:Oracle 資料庫基礎

現代標準寫法 (12c+)

SELECT * 
FROM employees
ORDER BY hire_date DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
  • OFFSET n ROWS:跳過前 n 筆
  • FETCH NEXT m ROWS ONLY:取後續 m 筆
  • 符合 ANSI SQL:2008,可攜性高、最佳化器支援佳

傳統 ROWNUM 寫法 (11g 及以下)

標準模式

SELECT * FROM (
  SELECT a.*, ROWNUM rn
  FROM (
    SELECT * FROM employees ORDER BY hire_date DESC
  ) a
  WHERE ROWNUM <= :offset + :limit
) WHERE rn > :offset;
  • 內層排序 → 中層賦予 ROWNUM → 外層篩選範圍
  • 關鍵:中層 WHERE ROWNUM <= upper_bound 讓最佳化器提早停止 (COUNT STOPKEY)

效能陷阱

-- ❌ 錯誤:外層排序導致全表掃描
SELECT * FROM (
  SELECT * FROM employees ORDER BY hire_date DESC
) WHERE ROWNUM BETWEEN 21 AND 30;

存儲過程封裝 (通用分頁元件)

CREATE OR REPLACE PACKAGE pkg_pagination AS
  TYPE ref_cursor IS REF CURSOR;
  
  PROCEDURE page_query (
    p_table       IN  VARCHAR2,       -- 表名 (含 Schema)
    p_where       IN  VARCHAR2 DEFAULT NULL,  -- WHERE 條件 (不含 WHERE)
    p_order_by    IN  VARCHAR2 DEFAULT NULL,  -- ORDER BY 子句
    p_page_num    IN  NUMBER  DEFAULT 1,      -- 第 N 頁 (1-based)
    p_page_size   IN  NUMBER  DEFAULT 20,     -- 每頁筆數
    p_total_rows  OUT NUMBER,                 -- 總筆數
    p_total_pages OUT NUMBER,                 -- 總頁數
    p_result      OUT ref_cursor              -- 結果游標
  );
END pkg_pagination;
/

CREATE OR REPLACE PACKAGE BODY pkg_pagination AS
  PROCEDURE page_query (...) IS
    v_sql      VARCHAR2(4000);
    v_cnt_sql  VARCHAR2(4000);
    v_offset   NUMBER;
    v_limit    NUMBER;
  BEGIN
    -- 參數驗證
    IF p_page_num < 1 THEN p_page_num := 1; END IF;
    IF p_page_size < 1 THEN p_page_size := 20; END IF;
    
    v_offset := (p_page_num - 1) * p_page_size;
    v_limit  := p_page_size;
    
    -- 總筆數
    v_cnt_sql := 'SELECT COUNT(*) FROM ' || p_table;
    IF p_where IS NOT NULL THEN
      v_cnt_sql := v_cnt_sql || ' WHERE ' || p_where;
    END IF;
    EXECUTE IMMEDIATE v_cnt_sql INTO p_total_rows;
    
    -- 總頁數
    p_total_pages := CEIL(p_total_rows / p_page_size);
    IF p_page_num > p_total_pages THEN
      p_page_num := p_total_pages;
      v_offset := (p_page_num - 1) * p_page_size;
    END IF;
    
    -- 分頁查詢 (12c+ 使用 OFFSET/FETCH)
    v_sql := 'SELECT * FROM ' || p_table;
    IF p_where IS NOT NULL THEN
      v_sql := v_sql || ' WHERE ' || p_where;
    END IF;
    IF p_order_by IS NOT NULL THEN
      v_sql := v_sql || ' ORDER BY ' || p_order_by;
    END IF;
    v_sql := v_sql || ' OFFSET ' || v_offset || ' ROWS FETCH NEXT ' || v_limit || ' ROWS ONLY';
    
    OPEN p_result FOR v_sql;
  END page_query;
END pkg_pagination;
/

-- 呼叫範例
VAR rc REFCURSOR
EXEC pkg_pagination.page_query('EMPLOYEES', 'department_id=10', 'hire_date DESC', 2, 10, :total_rows, :total_pages, :rc)
PRINT rc

進階優化技巧

Keyset Pagination (游標分頁) - 大資料集首選

-- 第一頁
SELECT * FROM employees 
WHERE department_id = 10 
ORDER BY hire_date DESC, employee_id DESC
FETCH FIRST 20 ROWS ONLY;

-- 後續頁 (記住上一頁最後一筆鍵值)
SELECT * FROM employees 
WHERE department_id = 10 
  AND (hire_date, employee_id) < (:last_hire_date, :last_emp_id)
ORDER BY hire_date DESC, employee_id DESC
FETCH FIRST 20 ROWS ONLY;
  • 無 OFFSET 掃描開銷,效能恆定
  • 需唯一排序鍵 (含 PK 避免重複值)
  • 不支援隨機跳頁,適合無限捲動/分頁器

物化視圖預分頁 (極大靜態資料)

CREATE MATERIALIZED VIEW mv_emp_paged
BUILD IMMEDIATE REFRESH FAST ON COMMIT
AS SELECT *, ROW_NUMBER() OVER (ORDER BY hire_date DESC) AS rn
FROM employees;

-- 查詢
SELECT * FROM mv_emp_paged WHERE rn BETWEEN 21 AND 30;

Java/JDBC 呼叫範例

String sql = "{call pkg_pagination.page_query(?,?,?,?,?,?,?,?)}";
CallableStatement cs = conn.prepareCall(sql);
cs.setString(1, "EMPLOYEES");
cs.setString(2, "department_id=10");
cs.setString(3, "hire_date DESC");
cs.setInt(4, 2);
cs.setInt(5, 10);
cs.registerOutParameter(6, Types.INTEGER);   // total_rows
cs.registerOutParameter(7, Types.INTEGER);   // total_pages
cs.registerOutParameter(8, OracleTypes.CURSOR);
cs.execute();

int totalRows = cs.getInt(6);
int totalPages = cs.getInt(7);
ResultSet rs = (ResultSet) cs.getObject(8);
while (rs.next()) { /* process */ }

方案選型指南

場景推薦方案理由
一般後台管理、報表OFFSET/FETCH 或 ROWNUM支援隨機跳頁、實作簡單
手機無限捲動、大資料集Keyset Pagination效能 O(1)、無深分頁效能衰退
極大靜態資料、高併發讀取物化視圖 + ROW_NUMBER預計算、查詢極快
動態複雜查詢 (多表 JOIN)ROWNUM 包裹子查詢相容舊版、可控制最佳化器
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved