在 Oracle 中實現高效分頁,核心策略是 **「資料庫層完成資料切片,應用層僅負責呈現」**。本文提供一套生產級的分頁存儲過程設計:使用 ROWNUM 偽欄位配合雙層子查詢,封裝於 Package 中,再由 Java (或任意語言) 透過 REF CURSOR 呼叫,實現高效能、低耦合的分頁架構。
Oracle 無 LIMIT/OFFSET,標準寫法為:
SELECT * FROM (
SELECT t1.*, ROWNUM rn FROM (
SELECT * FROM emp ORDER BY sale DESC
) t1 WHERE ROWNUM <= :v_end
) WHERE rn >= :v_begin;
ROWNUM <= 結束行 截取前 N 筆rn >= 起始行 取出當頁資料CREATE OR REPLACE PACKAGE pkg_pagination AS
TYPE ref_cursor IS REF CURSOR;
PROCEDURE get_page (
p_table_name IN VARCHAR2, -- 表名 (動態 SQL 需防注入)
p_page_size IN NUMBER, -- 每頁筆數
p_page_no IN NUMBER, -- 頁碼 (1-based)
p_total_rows OUT NUMBER, -- 總筆數
p_total_pages OUT NUMBER, -- 總頁數
p_result_set OUT ref_cursor -- 結果集游標
);
END pkg_pagination;
/
CREATE OR REPLACE PACKAGE BODY pkg_pagination AS
PROCEDURE get_page (
p_table_name IN VARCHAR2,
p_page_size IN NUMBER,
p_page_no IN NUMBER,
p_total_rows OUT NUMBER,
p_total_pages OUT NUMBER,
p_result_set OUT ref_cursor
) IS
v_sql VARCHAR2(32767);
v_begin NUMBER;
v_end NUMBER;
BEGIN
-- 參數防禦
IF p_page_size <= 0 OR p_page_no <= 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Page size and page no must be > 0');
END IF;
-- 計算總筆數 (動態 SQL)
v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;
EXECUTE IMMEDIATE v_sql INTO p_total_rows;
-- 計算總頁數
p_total_pages := CEIL(p_total_rows / p_page_size);
IF p_page_no > p_total_pages AND p_total_pages > 0 THEN
p_page_no := p_total_pages; -- 自動修正超界頁碼
END IF;
-- 計算分頁邊界
v_begin := (p_page_no - 1) * p_page_size + 1;
v_end := p_page_no * p_page_size;
-- 動態構建分頁查詢 (簡化版:假設固定 ORDER BY sale DESC)
v_sql := 'SELECT * FROM (
SELECT t1.*, ROWNUM rn FROM (
SELECT * FROM ' || p_table_name || ' ORDER BY sale DESC
) t1 WHERE ROWNUM <= ' || v_end || '
) WHERE rn >= ' || v_begin;
OPEN p_result_set FOR v_sql;
END get_page;
END pkg_pagination;
/
import java.sql.*;
import oracle.jdbc.*;
public class PaginationDemo {
public static void main(String[] args) throws SQLException {
String url = "jdbc:oracle:thin:@//host:1521/service";
try (Connection conn = DriverManager.getConnection(url, "user", "pwd");
CallableStatement cs = conn.prepareCall("{call pkg_pagination.get_page(?,?,?,?,?,?)}")) {
// 綁定參數
cs.setString(1, "EMP"); // p_table_name
cs.setInt(2, 10); // p_page_size
cs.setInt(3, 2); // p_page_no
cs.registerOutParameter(4, Types.INTEGER); // p_total_rows
cs.registerOutParameter(5, Types.INTEGER); // p_total_pages
cs.registerOutParameter(6, OracleTypes.CURSOR); // p_result_set
cs.execute();
// 讀取輸出參數
int totalRows = cs.getInt(4);
int totalPages = cs.getInt(5);
System.out.printf("Total: %d rows, %d pages%n", totalRows, totalPages);
// 處理 REF CURSOR
try (ResultSet rs = (ResultSet) cs.getObject(6)) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " | " +
rs.getString("ename") + " | " +
rs.getBigDecimal("sale"));
}
}
}
}
}
| 維度 | 建議 |
|---|---|
| SQL 注入防護 | 表名不可直接拼接。建議: 1. 白名單驗證表名 2. 或改用固定查詢視圖 + 參數化 WHERE 條件 |
| 大資料量深分頁 | ROWNUM 方案深分頁 (如第 10 萬頁) 效能急劇下降。 改用 Keyset Pagination (游標分頁): WHERE id > :last_seen_id ORDER BY id FETCH FIRST :size ROWS ONLY (12c+) |
| 動態排序 | 將 ORDER BY 欄位、方向參數化,動態拼接 v_sql (經白名單驗證) |
| 統計資訊快取 | 總筆數/總頁數若允許近似,可快取 5-10 分鐘,減少 COUNT(*) 全表掃描 |
| 連線池 | Java 端必須使用 HikariCP 等連線池,避免頻繁建立連線開銷 |
SELECT * FROM emp ORDER BY sale DESC
OFFSET :offset ROWS FETCH NEXT :page_size ROWS ONLY;
Oracle 12c 起支援 ANSI 標準分頁語法,執行計畫優於 ROWNUM 寫法,**強烈建議新專案直接採用**。
掌握「Package 封裝分頁邏輯 + REF CURSOR 回傳 + 應用層參數化呼叫」模式,即可構建高效、安全、可維護的 Oracle 分頁服務。