${escapeHtml(item.title)}
${escapeHtml(item.excerpt)}
分頁是 Web 應用處理大量數據的標準模式。核心邏輯只需三個參數:每頁條數(PageSize)、當前頁碼(CurrentPage)、總記錄數。本文從 SQL LIMIT 原理到完整分頁類實現、前端渲染、效能優化全鏈路講解。
-- MySQL LIMIT 語法:LIMIT offset, rows
-- offset = (當前頁 - 1) * 每頁條數
-- 第 1 頁(10 條/頁)
SELECT * FROM articles LIMIT 0, 10;
-- 第 2 頁
SELECT * FROM articles LIMIT 10, 10;
-- 第 N 頁通用模板
SELECT * FROM table ORDER BY id DESC LIMIT (page-1)*pageSize, pageSize;
<?php
// 1. 獲取參數
$page = max(1, (int)($_GET['page'] ?? 1));
$pageSize = 10;
// 2. 獲取總記錄數(必要,用於計算總頁數)
$countSql = "SELECT COUNT(*) AS total FROM articles";
$total = (int)$pdo->query($countSql)->fetchColumn();
// 3. 計算總頁數
$pageCount = (int)ceil($total / $pageSize);
$page = min($page, max(1, $pageCount)); // 修正越界頁碼
// 4. 查詢當前頁數據
$offset = ($page - 1) * $pageSize;
$sql = "SELECT * FROM articles ORDER BY created_at DESC LIMIT ?, ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$offset, $pageSize]);
$rows = $stmt->fetchAll();
// 5. 構建分頁鏈接
$links = '';
if ($page > 1) {
$links .= '首頁 ';
$links .= '上一頁 ';
}
for ($i = max(1, $page-2); $i <= min($pageCount, $page+2); $i++) {
$links .= $i == $page ? "$i " : "$i ";
}
if ($page < $pageCount) {
$links .= "下一頁 ";
$links .= "尾頁";
}
// 6. 輸出
foreach ($rows as $row) { /* 顯示數據 */ }
echo "$links";
?>
<?php
class Paginator {
public int $pageNo = 1;
public int $pageSize = 20;
public int $totalCount = 0;
public int $pageCount = 0;
public int $offset = 0;
public array $items = [];
public bool $hasPrev = false;
public bool $hasNext = false;
public string $jsCallback = 'loadPage'; // 前端 JS 回調函數名
public function __construct(
int $totalCount,
int $pageSize = 20,
int $pageNo = 1,
array $items = [],
string $jsCallback = 'loadPage'
) {
$this->totalCount = $totalCount;
$this->pageSize = max(1, $pageSize);
$this->pageNo = max(1, $pageNo);
$this->items = $items;
$this->jsCallback = $jsCallback;
$this->calculate();
}
private function calculate(): void {
$this->pageCount = max(1, (int)ceil($this->totalCount / $this->pageSize));
$this->pageNo = min($this->pageNo, $this->pageCount);
$this->offset = ($this->pageNo - 1) * $this->pageSize;
$this->hasPrev = $this->pageNo > 1;
$this->hasNext = $this->pageNo < $this->pageCount;
}
/** 生成頁碼陣列:[1, 2, -1, 10] -1 代表省略號 */
public function getPageNumbers(): array {
$list = [];
$pc = $this->pageCount;
$pn = $this->pageNo;
if ($pc <= 9) {
$list = range(1, $pc);
} elseif ($pn <= 4) {
$list = array_merge(range(1, 5), [-1, $pc]);
} elseif ($pn > $pc - 4) {
$list = array_merge([1, -1], range($pc - 4, $pc));
} else {
$list = array_merge([1, -1], range($pn - 2, $pn + 2), [-1, $pc]);
}
return $list;
}
/** 渲染 HTML 分頁條(Bootstrap 5 風格) */
public function render(): string {
$html = "";
return $html;
}
}
// 使用範例
$pageNo = (int)($_GET['page'] ?? 1);
$pageSize = 15;
$items = Article::getPage($pageNo, $pageSize); // 返回當前頁數據陣列
$total = Article::getTotalCount(); // 返回總數
$paginator = new Paginator($total, $pageSize, $pageNo, $items);
echo $paginator->render();
?>
<script>
function loadPage(pageNo) {
fetch(`api/articles?page=${pageNo}&size=15`)
.then(r => r.json())
.then(data => {
renderArticles(data.items); // 渲染列表
document.getElementById('pagination').innerHTML = data.paginationHtml; // 更新分頁條
window.history.pushState({}, '', `?page=${pageNo}`); // 更新 URL
});
}
function renderArticles(items) {
const html = items.map(item => `
${escapeHtml(item.title)}
${escapeHtml(item.excerpt)}
`).join('');
document.getElementById('article-list').innerHTML = html;
}
function escapeHtml(str) {
return str.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
</script>
當 OFFSET 很大時(如第 10000 頁),MySQL 需掃描前 100000 行再丟棄,極慢。
解決方案:
WHERE id > last_seen_id ORDER BY id LIMIT 20(需 ID 遞增且不需跳頁)SELECT * FROM t INNER JOIN (SELECT id FROM t ORDER BY id LIMIT 100000, 20) tmp USING (id)COUNT(*) 慢 → 緩存總數(Redis)、近似值(EXPLAIN rows)、異步更新計數表分頁查詢的 ORDER BY 字段必須有索引,避免 filesort。
分頁三要素:PageSize、CurrentPage、TotalCount。核心 SQL:LIMIT (page-1)*size, size。工程化要點:
Paginator 類,分離業務邏輯與顯示掌握這套體系,從幾十條數據到億級表,都能構建流暢、高效的分頁體驗。