通過動態 SQL 組裝 LIMIT offset, count,配合 PREPARE/EXECUTE 實現通用分頁,避免應用層拼接 SQL 風險。
DROP PROCEDURE IF EXISTS pr_pager;
DELIMITER //
CREATE PROCEDURE pr_pager(
IN p_table_name VARCHAR(1024), -- 表名 (可含 schema)
IN p_fields VARCHAR(1024), -- 查詢欄位,如 "id, name, email"
IN p_page_size INT, -- 每頁筆數
IN p_page_now INT, -- 當前頁碼 (1-based)
IN p_order_string VARCHAR(128), -- ORDER BY 子句,如 "ORDER BY created_at DESC"
IN p_where_string VARCHAR(1024), -- WHERE 子句,如 "WHERE status=1 AND type=2"
OUT p_out_rows INT -- 輸出:總行數
)
NOT DETERMINISTIC
SQL SECURITY DEFINER
COMMENT '通用分頁儲存過程'
BEGIN
DECLARE v_offset INT DEFAULT 0;
DECLARE v_limit_clause CHAR(64);
-- 計算 OFFSET
SET v_offset = (p_page_now - 1) * p_page_size;
SET v_limit_clause = CONCAT(' LIMIT ', v_offset, ', ', p_page_size);
-- 1. 計算總行數 (動態 SQL)
SET @count_sql = CONCAT('SELECT COUNT(*) INTO @total_rows FROM ', p_table_name, ' ', p_where_string);
PREPARE stmt_count FROM @count_sql;
EXECUTE stmt_count;
DEALLOCATE PREPARE stmt_count;
SET p_out_rows = @total_rows;
-- 2. 查詢分頁資料 (動態 SQL)
SET @data_sql = CONCAT('SELECT ', p_fields, ' FROM ', p_table_name, ' ', p_where_string, ' ', p_order_string, v_limit_clause);
PREPARE stmt_data FROM @data_sql;
EXECUTE stmt_data;
DEALLOCATE PREPARE stmt_data;
END//
DELIMITER ;
-- 第 1 頁,每頁 20 筆,按建立時間倒序,查詢啟用用戶
CALL pr_pager(
'users', -- 表名
'id, username, email, created_at', -- 欄位
20, -- 頁大小
1, -- 第 1 頁
'ORDER BY created_at DESC', -- 排序
'WHERE status = 1', -- 條件
@total_rows -- 輸出總行數
);
SELECT @total_rows; -- 獲取總筆數
CONCAT() 組裝 SQL 字符串PREPARE 解析、優化、生成執行計畫EXECUTE 執行DEALLOCATE PREPARE 釋放資源DECLARE v_xxx:區域變數,僅 BEGIN...END 內可見@xxx:用戶會話變數,跨語句可見 (用於 PREPARE 間傳值)OUT p_out_rows:輸出參數,返回給調用者| 問題 | 儲存過程現狀 | 進階優化方向 |
|---|---|---|
| 深分頁 OFFSET 大 | 直接 LIMIT 100000, 20 | 游標分頁:WHERE id > last_id ORDER BY id LIMIT 20 |
| COUNT(*) 全表掃描 | SELECT COUNT(*) | 資訊表估算、快取總數、觸發器維護計數表 |
| 動態 SQL 無法走查詢快取 | MySQL 8.0 已移除查詢快取 | 應用層快取 (Redis) 熱門分頁 |
| ORDER BY 無索引 | 可能 filesort | 強制要求調用者提供有索引支援的 ORDER BY |
function pager(PDO $pdo, string $table, string $fields, int $pageSize, int $pageNo,
string $where, string $order, array $whereParams = []): array {
// 白名單驗證:防止 SQL 注入
$allowedTables = ['users', 'orders', 'products'];
$allowedFields = ['id', 'username', 'email', 'status', 'created_at', 'updated_at'];
// ... 實際專案需完整白名單驗證邏輯 ...
$stmt = $pdo->prepare("CALL pr_pager(?, ?, ?, ?, ?, ?, @total_rows)");
$stmt->execute([$table, $fields, $pageSize, $pageNo, $order, $where]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->closeCursor();
$total = $pdo->query("SELECT @total_rows")->fetchColumn();
return [
'data' => $data,
'total' => (int)$total,
'page' => $pageNo,
'page_size' => $pageSize,
'total_pages' => ceil($total / $pageSize)
];
}
p_where_string 傳 "WHERE status=? AND type=?",參數另行綁定 (需改造 SP 支援)EXECUTE 權限,無 DROP/ALTER| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 儲存過程分頁 | 封裝邏輯、減少網路往返、權限控制 | 動態 SQL 風險、難除錯、版本控制弱 | 傳統單體應用、嚴格權限控制 |
| 應用層構建 SQL | 靈活、可測試、ORM 整合、版控友好 | 需自行防注入、多次網路往返 | 現代微服務、ORM 框架 |
| 游標分頁 (Keyset) | 深分頁 O(1)、效能穩定 | 不支援跳頁、需唯一排序鍵 | 無限滾動、大數據集、API 分頁 |
MySQL 分頁儲存過程展示了 **動態 SQL + 預處理** 的經典模式。現代架構傾向將分頁邏輯下沉到應用層 (配合 ORM/Query Builder),但理解儲存過程實現有助於維護舊系統、處理跨語言共享邏輯場景。**安全白名單、深分頁優化、游標分頁** 是三大必修課。