動態 SQL(EXECUTE IMMEDIATE / DBMS_SQL)賦予 PL/SQL 運行時構建、執行任意 SQL 的能力,適用於表名/列名/條件/排序動態化場景。但動態 SQL 伴隨**難以靜態檢查、解析開銷大、綁定變量窺探導致計劃不穩、可讀性差**等缺點。本文以**通用分頁查詢**為例,展示動態 SQL 規範寫法、綁定變量最佳實踐、性能優化要點與替代方案對比。
CREATE OR REPLACE PROCEDURE sp_exec_dynamic_page (
i_tablename IN VARCHAR2, -- 表名,支持多表聯結別名,如 'employees e, departments d'
i_tablecolumn IN VARCHAR2, -- 查詢列,如 'e.employee_id, d.department_name',NULL 表示 *
i_where IN VARCHAR2, -- WHERE 條件,不含 WHERE 關鍵字,如 "d.department_name LIKE 'S%'"
i_ordercolumn IN VARCHAR2, -- ORDER BY 列,如 'd.department_name DESC'
i_pagesize IN NUMBER, -- 每頁行數
i_curpage IN NUMBER, -- 當前頁碼 (1-based)
o_rowcount OUT NUMBER, -- 總行數
o_pagecount OUT NUMBER, -- 總頁數
o_cursor OUT SYS_REFCURSOR -- 分頁結果集游標
) IS
v_start_rec NUMBER;
v_end_rec NUMBER;
v_pagesize NUMBER;
v_curpage NUMBER;
v_cols VARCHAR2(2000);
v_where VARCHAR2(2000);
v_order VARCHAR2(200);
v_cnt_sql VARCHAR2(32767);
v_sel_sql VARCHAR2(32767);
BEGIN
-- 1. 參數預處理與默認值
v_cols := NVL(i_tablecolumn, '*');
v_where := CASE WHEN i_where IS NOT NULL THEN ' WHERE 1=1 AND ' || i_where ELSE ' WHERE 1=1' END;
v_order := CASE WHEN i_ordercolumn IS NOT NULL THEN ' ORDER BY ' || i_ordercolumn ELSE '' END;
v_pagesize := NVL(i_pagesize, 10);
v_curpage := CASE WHEN i_curpage IS NULL OR i_curpage < 1 THEN 1 ELSE i_curpage END;
-- 2. 統計總行數(使用綁定變量防注入、利用游標共享)
v_cnt_sql := 'SELECT COUNT(*) FROM ' || i_tablename || v_where;
EXECUTE IMMEDIATE v_cnt_sql INTO o_rowcount;
-- 3. 計算總頁數
IF MOD(o_rowcount, v_pagesize) = 0 THEN
o_pagecount := o_rowcount / v_pagesize;
ELSE
o_pagecount := FLOOR(o_rowcount / v_pagesize) + 1;
END IF;
-- 4. 修正頁碼越界
IF v_curpage > o_pagecount THEN
v_curpage := o_pagecount;
END IF;
-- 5. 計算分頁邊界
v_start_rec := (v_curpage - 1) * v_pagesize + 1;
v_end_rec := v_curpage * v_pagesize;
-- 6. 構建核心查詢(內層:排序+截取 ROWNUM;外層:過濾 RN)
v_sel_sql :=
'SELECT * FROM (' ||
' SELECT e.*, ROWNUM rn FROM (' ||
' SELECT ' || v_cols || ' FROM ' || i_tablename || v_where || v_order ||
' ) e' ||
' WHERE ROWNUM <= :end_rec' ||
') WHERE rn >= :start_rec';
-- 7. 綁定變量執行,開啟游標返回
OPEN o_cursor FOR v_sel_sql USING v_end_rec, v_start_rec;
-- 8. 調試輸出(生產環境建議移除或改為 DEBUG 級別日誌)
DBMS_OUTPUT.PUT_LINE('Count SQL => ' || v_cnt_sql);
DBMS_OUTPUT.PUT_LINE('Page SQL => ' || v_sel_sql);
END;
/
| 決策點 | 方案 | 理由 |
|---|---|---|
| 表名/列名/條件/排序動態化 | 字符串拼接進 SQL 模板 | 識別符無法綁定,只能拼接。**嚴格驗證白名單**或來源可控 |
| 分頁邊界值 | 綁定變量 USING v_end_rec, v_start_rec | 避免硬解析、防注入、利用游標共享 |
| 分頁算法 | 雙層 ROWNUM (Oracle 11g-) | 經典穩定寫法;12c+ 可用 OFFSET ... FETCH NEXT 標準語法 |
| 總行數統計 | 單獨 COUNT(*) 查詢 | 簡單可靠;大表可考慮近似統計或物化視圖 |
| 結果集返回 | SYS_REFCURSOR 開啟游標 | 客戶端逐行獲取,避免中間物化大數據集 |
OPT_PARAM('_optim_peek_user_binds', 'FALSE') 會話級關閉窺探DBMS_SQL 手動綁定並設置 BIND_ARRAYDBMS_SQL.BIND_ARRAY / FORALL 批量 DML,減少上下文切換-- 靜態 SQL + 標準分頁,無需動態構建
SELECT *
FROM (
SELECT e.*, ROW_NUMBER() OVER (ORDER BY d.department_name DESC) rn
FROM employees e, departments d
WHERE d.department_name LIKE 'S%'
)
WHERE rn BETWEEN :start_rec AND :end_rec;
-- 或 12c 簡潔語法
SELECT *
FROM employees e, departments d
WHERE d.department_name LIKE 'S%'
ORDER BY d.department_name DESC
OFFSET :offset ROWS FETCH NEXT :page_size ROWS ONLY;
**優勢**:靜態解析、計劃穩定、優化器完整優化、可讀性高、無注入風險。**除非表名/列名確需動態,優先使用靜態 SQL + 綁定變量**。
SELECT 權限動態 SQL 是雙刃劍。**通用分頁場景下,Oracle 12c+ 標準 OFFSET/FETCH 或 ROW_NUMBER() 靜態 SQL 為首選**;確需動態表名/列名時,遵循「模板固定、值綁定、識別符白名單、最小權限、審計日誌」五原則。理解綁定變量窺探、硬解析開銷、游標共享機制,能寫出高性能、安全、可維護的動態 SQL 代碼。