所有分頁系統只需三個輸入:
$pageSize (如 10、20、50)$currentPage (從 URL $_GET['page'] 取得,預設 1)$totalRows (執行 SELECT COUNT(*) 取得)由此衍生:
$totalPages = ceil($totalRows / $pageSize)$offset = ($currentPage - 1) * $pageSizeSELECT * FROM table ORDER BY id DESC LIMIT $offset, $pageSize-- 格式:LIMIT [offset,] rows
-- offset 從 0 開始 (第 1 筆為 0)
-- 第 1 頁 (每頁 10 筆)
SELECT * FROM articles LIMIT 0, 10; -- 或 LIMIT 10
-- 第 2 頁
SELECT * FROM articles LIMIT 10, 10;
-- 第 3 頁
SELECT * FROM articles LIMIT 20, 10;
-- 通用模板
SELECT * FROM table ORDER BY id DESC LIMIT (page-1)*pageSize, pageSize;
LIMIT 1000000, 10 (深分頁) 會掃描前 100 萬行再丟棄,極慢。百萬級資料請用「游標分頁」或「延遲關聯」優化:SELECT * FROM table WHERE id > last_seen_id ORDER BY id LIMIT 10 (需前端配合傳遞最後一筆 ID)
Paginator (PHP 7.4+)<?php
// Paginator.php
class Paginator {
public int $currentPage = 1;
public int $pageSize = 15;
public int $totalRows = 0;
public int $totalPages = 0;
public int $offset = 0;
public string $urlPattern = '?page={page}'; // {page} 會被替換
public array $queryParams = []; // 額外查詢參數 ['keyword' => 'php', 'category' => 3]
public function __construct(int $pageSize = 15) {
$this->pageSize = $pageSize;
$this->currentPage = max(1, (int)($_GET['page'] ?? 1));
}
public function setTotalRows(int $totalRows): self {
$this->totalRows = max(0, $totalRows);
$this->totalPages = (int)ceil($this->totalRows / $this->pageSize);
$this->currentPage = min($this->currentPage, max(1, $this->totalPages));
$this->offset = ($this->currentPage - 1) * $this->pageSize;
return $this;
}
// 取得 LIMIT 子句字串
public function getLimitClause(): string {
return "LIMIT {$this->offset}, {$this->pageSize}";
}
// 產生分頁連結 URL
public function getPageUrl(int $page): string {
$params = array_merge($this->queryParams, ['page' => $page]);
return '?' . http_build_query($params);
}
// 判斷是否有上/下一頁
public function hasPrev(): bool { return $this->currentPage > 1; }
public function hasNext(): bool { return $this->currentPage < $this->totalPages; }
// 取得顯示的頁碼範圍 (如 1 ... 4 5 [6] 7 8 ... 20)
public function getPageRange(int $delta = 2): array {
$start = max(1, $this->currentPage - $delta);
$end = min($this->totalPages, $this->currentPage + $delta);
// 保證至少顯示 2*delta+1 頁 (若總頁數足夠)
if ($end - $start < 2 * $delta) {
if ($start == 1) $end = min($this->totalPages, $start + 2 * $delta);
elseif ($end == $this->totalPages) $start = max(1, $end - 2 * $delta);
}
return range($start, $end);
}
// 渲染 Bootstrap 5 風格分頁 HTML
public function render(): string {
if ($this->totalPages <= 1) return '';
$html = '';
return $html;
}
}
<?php
// ArticleController.php
require 'Paginator.php';
require 'pdo_connect.php'; // 回傳 $pdo
$pager = new Paginator(10); // 每頁 10 筆
// 1. 取得總筆數 (可加搜尋條件)
$where = "WHERE status = 1";
$params = [];
if (!empty($_GET['keyword'])) {
$where .= " AND title LIKE ?";
$params[] = "%{$_GET['keyword']}%";
$pager->queryParams['keyword'] = $_GET['keyword']; // 保留分頁連結參數
}
$stmt = $pdo->prepare("SELECT COUNT(*) FROM articles {$where}");
$stmt->execute($params);
$pager->setTotalRows((int)$stmt->fetchColumn());
// 2. 取得當頁資料
$stmt = $pdo->prepare("SELECT * FROM articles {$where} ORDER BY created_at DESC " . $pager->getLimitClause());
$stmt->execute($params);
$articles = $stmt->fetchAll();
// 3. 傳遞給視圖
include 'article_list_view.php';
article_list_view.php<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>文章列表</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="container py-4">
<h1 class="mb-4">文章列表</h1>
<!-- 搜尋表單 (保留分頁參數) -->
<form method="get" class="row g-2 mb-4">
<div class="col-auto">
<input type="text" name="keyword" class="form-control" placeholder="搜尋標題..." value="<?= htmlspecialchars($_GET['keyword'] ?? '') ?>">
</div>
<div class="col-auto">
<button class="btn btn-primary">搜尋</button>
</div>
</form>
<!-- 文章列表 -->
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead><tr><th>ID</th><th>標題</th><th>作者</th><th>建立時間</th></tr></thead>
<tbody>
<?php foreach ($articles as $art): ?>
<tr>
<td><?= $art['id'] ?></td>
<td><?= htmlspecialchars($art['title']) ?></td>
<td><?= htmlspecialchars($art['author_name'] ?? '-') ?></td>
<td><?= $art['created_at'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- 分頁導航 -->
<?= $pager->render() ?>
</body></html>
| 方案 | 原理 | 適用場景 | 前端配合 |
|---|---|---|---|
| 游標分頁 | WHERE id > last_id ORDER BY id LIMIT 10 | 無限滾動、時間軸、日誌 | 傳遞 last_id 而非 page |
| 延遲關聯 | 先查 SELECT id FROM t LIMIT offset, 10,再 SELECT * FROM t WHERE id IN (...) | 需保留跳頁功能的大表 | 維持 page 參數 |
| 覆蓋索引 | 建立 INDEX (status, created_at, id) 讓 COUNT 與 LIMIT 走索引 | 所有分頁查詢 | 無 |
| 預估總數 | EXPLAIN SELECT ... 取 rows 估值,或 Redis 緩存總數 | 極大表 (億級),精確總數非必須 | 顯示「約 10,000 筆」 |
分頁看似簡單,實則包含 **SQL LIMIT 機制、參數驗證、URL 狀態保持、深分頁效能、前端無障礙** 等多層面考量。本文提供的 Paginator 類別封裝了核心邏輯,支援 Bootstrap 5 輸出、搜尋參數保留、頁碼範圍智慧顯示,可直接納入專案或依需求擴充 (如 AJAX 分頁、游標分頁介面)。記住:**永遠驗證 $currentPage 範圍、使用預處理防注入、針對大表優化索引與查詢策略**,即能構建穩健高效的分頁系統。