🧩 概念與核心語法 #
當資料表有成千上萬筆記錄時,一次載入全部會拖慢速度、影響閱讀。分頁(Pagination)的做法是用 MySQL 的 LIMIT 子句,每次只取指定數量的記錄。
LIMIT 語法(兩種寫法,意思一樣) #
-- Syntax 1: offset comes first
SELECT * FROM table_name LIMIT offset, row_count;
-- Syntax 2: using the OFFSET keyword (more readable)
SELECT * FROM table_name LIMIT row_count OFFSET offset;| 參數 | 說明 |
|---|---|
row_count | 每頁顯示多少筆記錄 |
offset | 要跳過多少筆才開始取(由 0 開始,OFFSET 0 即第一筆) |
分頁核心數學(三個變數) #
$per_page = 10; // rows per page
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; // current page
$offset = ($page - 1) * $per_page; // rows to skip✅ 第 1 頁 → offset = 0;第 2 頁 → offset = 10;第 3 頁 → offset = 20
📊 完整範例(mysqli 程序式) #
以下是一個可直接執行的完整範例,用 mysqli 讀取 students 資料表並以分頁表格顯示:
<?php
// 1. Connect to the database
$conn = mysqli_connect('localhost', 'root', '', 'pagination');
if (!$conn) {
die('Connection failed: ' . mysqli_connect_error());
}
mysqli_set_charset($conn, 'utf8mb4'); // support Chinese, avoid mojibake
// 2. Pagination variables
$per_page = 5; // rows per page
$page = max(1, (int)($_GET['page'] ?? 1)); // current page (min 1)
$offset = ($page - 1) * $per_page; // rows to skip
// 3. Get total records, then compute total pages
$total_result = mysqli_query($conn, 'SELECT COUNT(*) FROM students');
$total_records = mysqli_fetch_row($total_result)[0];
$total_pages = (int) ceil($total_records / $per_page); // ceil rounds up
// 4. Fetch current-page records using LIMIT + OFFSET
$sql = "SELECT * FROM students ORDER BY id LIMIT $offset, $per_page";
$result = mysqli_query($conn, $sql);
// 5. Render the table
echo '<table border="1">';
echo '<tr><th>ID</th><th>Name</th><th>Age</th></tr>';
while ($row = mysqli_fetch_assoc($result)) {
echo '<tr>';
echo '<td>' . htmlspecialchars($row['id']) . '</td>';
echo '<td>' . htmlspecialchars($row['name']) . '</td>';
echo '<td>' . htmlspecialchars($row['age']) . '</td>';
echo '</tr>';
}
echo '</table>';
// 6. Pagination navigation links
for ($i = 1; $i <= $total_pages; $i++) {
if ($i == $page) {
echo "<strong>$i</strong> "; // bold the current page
} else {
echo "<a href='?page=$i'>$i</a> ";
}
}
mysqli_close($conn);
?>🔒 安全性(Prepared Statement) #
直接把 $offset、$per_page 拼入 SQL 有 SQL injection(SQL 注入)風險。用 prepared statement 綁定整數參數才是正確做法:
<?php
$conn = mysqli_connect('localhost', 'root', '', 'pagination');
$per_page = 5;
$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $per_page;
// use ? placeholders, then bind integer parameters
$stmt = mysqli_prepare($conn, 'SELECT id, name, email FROM users ORDER BY id LIMIT ? OFFSET ?');
mysqli_stmt_bind_param($stmt, 'ii', $per_page, $offset); // 'ii' = two integers
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
echo htmlspecialchars($row['name']) . '<br>';
}
mysqli_close($conn);
?>PDO 版本(更推薦) #
<?php
$pdo = new PDO('mysql:host=localhost;dbname=demo', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$per_page = 20;
$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $per_page;
// total record count
$total_records = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
$total_pages = (int) ceil($total_records / $per_page);
// fetch the current page
$stmt = $pdo->prepare('SELECT id, name, email FROM users ORDER BY id LIMIT :limit OFFSET :offset');
$stmt->bindValue(':limit', $per_page, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
echo htmlspecialchars($row['name']) . '<br>';
}
?>⚠️ 注意事項 #
| 注意事項 | 說明 |
|---|---|
必配 ORDER BY | 沒有 ORDER BY 時 MySQL 可任意排序,分頁結果會不穩定。一定要配 ORDER BY 唯一欄位(如 id) |
| 兩次查詢 | 分頁需要兩條 query:一條 COUNT(*) 取總筆數、一條 LIMIT/OFFSET 取目前頁資料 |
| offset 由 0 開始 | 常見 off-by-one 錯誤:第 1 頁要 OFFSET 0,所以公式是 ($page - 1) * $per_page |
| 深分頁慢 | LIMIT 20 OFFSET 100000 會令 MySQL 掃過前面 10 萬筆。大型資料建議用 keyset pagination(WHERE id > :lastId) |
| 中文亂碼 | 記得 mysqli_set_charset($conn, 'utf8mb4'),否則中文會變亂碼 |
| 輸出要 escape | 顯示前用 htmlspecialchars() 防止 XSS(跨站腳本攻擊) |
🛠️ 快速對照 #
| 項目 | mysqli(程序式) | PDO |
|---|---|---|
| 連線 | mysqli_connect() | new PDO('mysql:host=...;dbname=...') |
| 取結果 | mysqli_fetch_assoc() | fetchAll(PDO::FETCH_ASSOC) |
| 綁定參數 | mysqli_stmt_bind_param() | bindValue() |
| 支援多資料庫 | 只支援 MySQL | 多種資料庫 |
| 推薦程度 | 一般 | ✅ 更推薦(更安全、易維護) |
🔗 參考連結 #
| 來源 | 連結 |
|---|---|
| W3docs — MySQL LIMIT | w3docs.com |
| Tutorial Republic — LIMIT 教學 | tutorialrepublic.com |
| CodeShack — PHP Pagination | codeshack.io |
| Mike Lopez — 精簡分頁 | mikelopez.com |
| Campcodes — 自訂分頁 | campcodes.com |
