核心需求
- 依時間倒序顯示留言
- 每頁固定筆數
- 自動計算總頁數
- 產生「首頁/上一頁/頁碼/下一頁/尾頁」導航
- 邊界處理:首頁無上一頁、尾頁無下一頁
資料庫準備
CREATE TABLE `note` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`content` TEXT NOT NULL,
`sendtime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`author` VARCHAR(50) DEFAULT NULL,
INDEX `idx_sendtime` (`sendtime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
分頁核心邏輯
<?php
// config.php
$host = 'localhost'; $user = 'root'; $pass = ''; $db = 'guestbook';
$perPage = 10; // 每頁筆數
// list.php
require 'config.php';
$conn = new mysqli($host, $user, $pass, $db);
$conn->set_charset('utf8mb4');
// 1. 取得並驗證頁碼
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
// 2. 查詢總筆數
$totalResult = $conn->query("SELECT COUNT(*) FROM `note`");
$totalRows = $totalResult->fetch_row()[0];
$totalResult->free();
// 3. 計算總頁數
$totalPages = (int)ceil($totalRows / $perPage);
if ($page > $totalPages) $page = max(1, $totalPages);
// 4. 計算 OFFSET
$offset = ($page - 1) * $perPage;
// 5. 分頁查詢(使用 LIMIT offset, count)
$stmt = $conn->prepare("SELECT id, content, author, sendtime FROM `note` ORDER BY sendtime DESC LIMIT ?, ?");
$stmt->bind_param("ii", $offset, $perPage);
$stmt->execute();
$result = $stmt->get_result();
// 6. 顯示留言
echo "<table width='100%'>";
while ($row = $result->fetch_assoc()) {
$time = date('Y-m-d H:i', strtotime($row['sendtime']));
$content = nl2br(htmlspecialchars($row['content']));
echo "<tr><td><strong>{$row['author']}</strong> $time<br>$content</td></tr>";
}
echo "</table>";
$result->free();
$stmt->close();
// 7. 產生分頁導航
echo "<div class='pagination' style='text-align:center; margin:20px 0;'>";
if ($page > 1) {
echo "<a href='?page=1'>首頁</a> ";
echo "<a href='?page=" . ($page - 1) . "'>上一頁</a> ";
}
// 顯示頁碼範圍(當前頁前後各 2 頁)
$start = max(1, $page - 2);
$end = min($totalPages, $page + 2);
for ($i = $start; $i <= $end; $i++) {
if ($i == $page) {
echo "<span class='current' style='font-weight:bold; color:red;'>[$i]</span> ";
} else {
echo "<a href='?page=$i'>$i</a> ";
}
}
if ($page < $totalPages) {
echo "<a href='?page=" . ($page + 1) . "'>下一頁</a> ";
echo "<a href='?page=$totalPages'>尾頁</a> ";
}
echo "<span class='info'>(第 $page / $totalPages 頁,共 $totalRows 筆)</span>";
echo "</div>";
$conn->close();
?>
關鍵變數對照表
| 變數 | 意義 | 計算方式 |
$perPage | 每頁筆數 | 固定常數或設定檔 |
$page | 當前頁碼 (1-based) | max(1, intval($_GET['page'])) |
$totalRows | 總資料筆數 | SELECT COUNT(*) |
$totalPages | 總頁數 | ceil($totalRows / $perPage) |
$offset | 跳過筆數 | ($page - 1) * $perPage |
LIMIT ?, ? | MySQL 分頁語法 | LIMIT offset, count |
原始碼邏輯與現代寫法對比
| 面向 | 原始碼 | 現代改良 |
| 資料庫介面 | mysql_* (已移除) | mysqli / PDO + Prepared Statement |
| 查詢方式 | 先查全部再 PHP 陣列切片 | SQL LIMIT 直接取所需頁面 |
| 總筆數查詢 | mysql_numrows($result) (錯誤用法) | 獨立 COUNT(*) 查詢 |
| 陣列索引 | $show[$i][content] (語法錯誤) | 直接迴圈輸出 fetch_assoc() |
| URL 參數 | 手動拼接 $qqname=$qqname&serial=$serial | http_build_query() 自動編碼 |
| 頁碼顯示 | 全部列出 | 當前頁附近範圍 (如 ±2) |
| XSS 防護 | 無 | htmlspecialchars() 輸出轉義 |
效能優化策略
1. 避免深分頁效能問題
大偏移量 LIMIT 100000, 10 會掃描前 10 萬行。解決方案:
- 游標分頁:記住上一頁最後一筆的 PK/時間,用
WHERE id > ? ORDER BY id LIMIT 10(需唯一排序鍵)。
- 延遲關聯:
SELECT * FROM note WHERE id IN (SELECT id FROM note ORDER BY sendtime DESC LIMIT 100000, 10) 先只查 ID 再回表。
- 限制最大頁碼:如僅允許前 100 頁,超過導向搜尋/篩選。
2. 總筆數快取
// Redis 快取 1 分鐘
$cacheKey = 'note:total_count';
$totalRows = $redis->get($cacheKey);
if ($totalRows === false) {
$totalRows = $conn->query("SELECT COUNT(*) FROM note")->fetch_row()[0];
$redis->setex($cacheKey, 60, $totalRows);
}
3. 覆蓋索引
-- 僅查詢需要的欄位,建立覆蓋索引避免回表
CREATE INDEX idx_note_paging ON note (sendtime DESC, id) INCLUDE (content, author); -- MySQL 8.0+
-- 或組合索引
CREATE INDEX idx_note_paging ON note (sendtime DESC, id, content, author);
現代框架整合範例
Laravel Eloquent
// Controller
$notes = Note::latest('sendtime')->paginate(10);
// Blade 模板
@foreach ($notes as $note)
<div>{{ $note->author }} - {{ $note->sendtime->format('Y-m-d H:i') }}<br>{{ $note->content }}</div>
@endforeach
{{ $notes->links() }} // 自動產生分頁導航
PDO 通用分頁類別
class Paginator {
public function __construct(
private PDO $pdo,
private string $table,
private int $perPage = 10,
private string $orderBy = 'id DESC',
private string $where = '',
private array $params = []
) {}
public function getPage(int $page = 1): array {
$page = max(1, $page);
$total = $this->pdo->query("SELECT COUNT(*) FROM {$this->table} {$this->where}", $this->params)->fetchColumn();
$totalPages = (int)ceil($total / $this->perPage);
$page = min($page, max(1, $totalPages));
$offset = ($page - 1) * $this->perPage;
$stmt = $this->pdo->prepare("SELECT * FROM {$this->table} {$this->where} ORDER BY {$this->orderBy} LIMIT ?, ?");
$stmt->execute(array_merge($this->params, [$offset, $this->perPage]));
$items = $stmt->fetchAll();
return [
'items' => $items,
'current_page' => $page,
'per_page' => $this->perPage,
'total' => $total,
'total_pages' => $totalPages,
'has_prev' => $page > 1,
'has_next' => $page < $totalPages,
];
}
}
// 使用
$pager = new Paginator($pdo, 'note', 10, 'sendtime DESC');
$data = $pager->getPage($_GET['page'] ?? 1);
常見錯誤與除錯
| 錯誤現象 | 可能原因 | 解決 |
| 第 1 頁正常,第 2 頁空白 | OFFSET 計算錯誤或 LIMIT 參數順序反 | 確認 LIMIT offset, count 順序 |
| 總頁數為 0 | COUNT(*) 查詢失敗或資料表為空 | 檢查 SQL 錯誤、資料是否存在 |
| 頁碼連結點擊無反應 | URL 參數名稱不一致(page vs p) | 統一參數名,使用 $_GET['page'] |
| 中文亂碼 | 連線 charset 未設定 | $conn->set_charset('utf8mb4') |
| 分頁導航顯示錯誤 | 邊界條件判斷錯誤(<= vs <) | 仔細檢查 $page > 1, $page < $totalPages |
- 上一篇文章: 留言板摺疊展開效果:純 CSS/JS 實作下拉式內容顯示
- 下一篇文章: PHP 與 MySQL 打造即時聊天室(二):線上用戶列表與狀態管理
Copyright ©
程式碼之家 All Rights Reserved