PHP分页是后端用LIMIT和OFFSET精确控制数据库取数,并动态生成导航链接的完整逻辑;关键在正确计算offset=($current_page-1)*$per_page、校验页码范围、COUNT(*)与业务SQL条件严格一致、安全拼接SQL参数及保留URL其他查询参数。

PHP分页不是前端切数据,也不是靠 JavaScript 拼页码——它是后端用 LIMIT 和 OFFSET 精确控制从数据库取哪一段记录,并配合总条数、当前页码动态生成导航链接的一整套逻辑。核心就一句话:每次只查一页该显示的数据,不浪费带宽和内存。
怎么算 offset 和 limit 才不越界?
很多人写 $offset = $_GET['page'] * $per_page,结果第 1 页直接跳过前 10 条——因为页码是从 1 开始,而 offset 是从 0 开始。
-
$current_page必须强制转为整型并校验下限:$current_page = max(1, (int)($_GET['page'] ?? 1)) -
$offset正确公式是:($current_page - 1) * $per_page -
$total_pages = ceil($total_records / $per_page),但若$total_records === 0,ceil(0)是 0,会导致$current_page被设为 0,必须兜底:$current_page = min($current_page, $total_pages ?: 1) - MySQL 的
LIMIT $offset, $limit语法中,$offset超出总记录数时不会报错,但会返回空数组——这没问题;但前端如果没处理“无数据”状态,页面就变空白,容易误判为 bug
为什么 COUNT(*) 查询必须和业务 SQL 条件完全一致?
比如你查文章列表加了 WHERE status = 1,但 COUNT 却写 SELECT COUNT(*) FROM articles,那算出来的 $total_pages 就是错的——用户点到最后一页会发现数据突然少一半,或者“下一页”链接点进去是空页。
- 所有过滤条件(
WHERE、JOIN、HAVING)必须在 COUNT 查询里原样复现 - 别图省事用缓存总数,除非你能保证缓存和查询条件强一致;否则宁可多一次 COUNT,也别让用户翻页翻丢数据
-
大数据量表(千万级)慎用
COUNT(*),它会扫全表;可考虑估算(如EXPLAIN的rows)、或改用键集分页(WHERE id > ? ORDER BY id LIMIT 10)
预处理语句里 bind offset 和 limit 为什么总报错?
MySQL 不允许对 LIMIT 和 OFFSET 使用占位符绑定,PDO::PARAM_INT 在这儿无效——这是常见卡点,错误信息通常是:SQLSTATE[HY093]: Invalid parameter number 或直接执行失败。
立即学习“PHP免费学习笔记(深入)”;
- 正确做法:用
intval()或强制类型转换确保变量安全,再拼进 SQL 字符串,例如:"LIMIT " . (int)$offset . ", " . (int)$limit - 只要
$offset和$limit是你严格控制的整型(来自校验后的$_GET或配置),拼接是安全的;比强行用 bind 导致报错更可控 - 千万别用
mysqli_real_escape_string()处理数字——它对整型无效,且易引入类型混淆
页码链接生成时 URL 参数怎么保留?
用户本来在搜 ?q=php&sort=date,点第 2 页却变成 ?page=2,搜索词和排序丢了——这不是分页逻辑问题,是链接构造疏忽。
- 提取原始
$_GET,剔除page后重新拼 URL:http_build_query(array_diff_key($_GET, ['page' => ''])) - 避免手写
?" . http_build_query(...)后再加&page=,容易漏&或开头多& - 如果用了伪静态(如
/list/2),就得在路由层解析页码,而不是依赖$_GET,否则参数无法透传
实际项目里最常被忽略的,是把分页当成独立模块来写——它必须和你的查询条件、缓存策略、URL 设计绑死。少一个环节对齐,用户就会在某一页莫名其妙看不到数据。











