在Web应用的后台列表或前台信息流中,当数据量增长到几千甚至上百万条时,一次性从MySQL查出全部记录再渲染到页面既耗费内存又拖慢响应。通过PHP配合MySQL的LIMIT机制,可以把结果集按固定大小切分成多页,每次只取当前页所需数据。这种方式既减轻了数据库压力,也让用户浏览更顺畅。

一、分页的底层原理
MySQL本身不提供“页”的概念,所谓分页本质上是利用LIMIT关键字控制结果集的起始位置和返回行数。假设每页显示10条,那么第n页对应的偏移量就是(n-1)*10,SQL写作LIMIT 偏移量, 每页条数。PHP的职责是根据用户请求的页码算出这个偏移量,并先通过COUNT(*)拿到总记录数,从而推算出总页数。
很多初学者容易忽略总条数查询与数据查询必须基于同一筛选条件,否则会出现页码对不上或最后一页数据缺失。另外,LIMIT在大偏移量时性能会下降,例如LIMIT 100000, 10实际要扫描十万行再丢弃,后续可借助游标或延迟关联优化,但基础分页先保证逻辑正确。
二、数据库准备与示例表
我们创建一张简单的文章表来演示。表结构包含自增主键、标题与发布时间,数据量可以自行制造一些测试行。
CREATE TABLE `article` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入测试数据可以用存储过程或简单脚本循环写入,这里不展开。重点在于后续查询都基于这张表,确保字段名与代码中的SQL保持一致,避免因为拼写错误导致空结果。
三、PHP实现基础分页逻辑
下面是一段不依赖框架的原生PHP代码,展示如何接收页码、计算偏移、执行两条SQL并渲染简易分页条。注意对页码参数做了整数化和越界保护。
<?php
// 假设已建立 $pdo 连接
$pageSize = 10;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) {
$page = 1;
}
// 统计总数
$countSql = "SELECT COUNT(*) FROM article";
$total = $pdo->query($countSql)->fetchColumn();
$totalPages = ceil($total / $pageSize);
if ($page > $totalPages && $totalPages > 0) {
$page = $totalPages;
}
$offset = ($page - 1) * $pageSize;
$dataSql = "SELECT id, title, created_at FROM article ORDER BY id DESC LIMIT " . $offset . ", " . $pageSize;
$stmt = $pdo->query($dataSql);
$list = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
上述代码先把用户传来的page转成整数,防止SQL注入和非法值。然后通过COUNT拿到总条数,用ceil计算总页数。若请求页超出范围则纠正到最后一页,这是提升健壮性的关键一步。
数据查询部分直接拼接偏移量和每页大小,因为二者都是PHP整数运算结果,不存在外部字符串注入风险。实际项目中若使用框架的查询构造器,也应显式传参而非拼字符串,保持同样的安全意识。
四、前端展示与分页导航
取回列表后,需要把数据循环输出,并生成上一页、下一页及页码链接。以下代码紧接上面的PHP变量,用简易HTML呈现。
<ul>
<?php foreach ($list as $row): ?>
<li><?php echo htmlspecialchars($row['title']); ?> (<?php echo $row['created_at']; ?>)</li>
<?php endforeach; ?>
</ul>
<div>
<?php if ($page > 1): ?>
<a href="?page=<?php echo $page - 1; ?>">上一页</a>
<?php endif; ?>
<span>第 <?php echo $page; ?> / <?php echo $totalPages; ?> 页</span>
<?php if ($page < $totalPages): ?>
<a href="?page=<?php echo $page + 1; ?>">下一页</a>
<?php endif; ?>
</div>
这里使用htmlspecialchars对标题做转义,防止存储型XSS。分页链接直接改URL的page参数,浏览器刷新即触发新的查询,逻辑简单清晰,适合后台或轻量前台。
如果页码很多,可以只显示当前页前后几页加省略号,避免链接过长。但这属于交互优化,底层依然依赖同一个page参数与相同的SQL分页规则。
五、封装为可复用函数
为避免每个列表都重写一遍计数和偏移计算,可以提炼一个函数。它接收表名、页码、每页大小,返回数据与分页信息数组。
<?php
function paginate($pdo, $table, $page, $pageSize) {
$page = max(1, (int)$page);
$total = $pdo->query("SELECT COUNT(*) FROM " . $table)->fetchColumn();
$totalPages = ceil($total / $pageSize);
$page = min($page, $totalPages ?: 1);
$offset = ($page - 1) * $pageSize;
$sql = "SELECT * FROM " . $table . " LIMIT " . $offset . ", " . $pageSize;
$data = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
return [
'data' => $data,
'page' => $page,
'totalPages' => $totalPages,
'total' => $total
];
}
?>
这个函数把边界处理收拢在一处,调用方只需关心传入页码。注意示例中表名直接拼接,仅适合内部固定字符串,若表名来自用户输入必须白名单校验。
在复杂业务里,可能还要接收WHERE条件和排序参数,此时建议用预处理语句绑定值,而LIMIT部分因是数字仍可安全拼接。封装后控制器代码更干净,也方便统一加缓存。
六、常见误区与优化建议
一个典型误区是只用LIMIT 10却不管页码,导致点第二页还是第一页数据。必须带上偏移量。另一个误区是在循环里执行COUNT,严重浪费连接资源,应一次统计完。
当单表超百万且常翻到很后面时,可改用“上一页最大ID”方式:WHERE id < 上次最后ID ORDER BY id DESC LIMIT 10,避免大偏移扫描。对于模糊搜索分页,建议对搜索词建索引,否则COUNT也会变慢。合理组合这些手段,PHP与MySQL的分页就能稳定支撑大多数场景。
PHPMySQL pagination修改时间:2026-08-01 18:18:34