thinkphp 5.1 十万级数据分页变慢的根本原因是 mysql 的 limit offset, size 导致全表扫描前 offset 行;解决方案包括关闭 totalcount、使用 simple 分页、添加复合索引 (user_id, create_time)、确保字段 not null,并改用基于 id 的游标分页替代 offset 分页。

ThinkPHP 5.1 应用在数据量超过十万级后,用户翻到第200页、第500页时页面加载明显变慢,甚至超时,根本原因是 MySQL 执行 LIMIT 9990, 20 类查询时被迫扫描并丢弃前9990行——这不是 PHP 层能绕开的数据库底层瓶颈。
关闭 totalCount 统计跳过 COUNT(*) 全表扫描
默认 paginate() 会执行两条 SQL:一条 SELECT COUNT(*) 算总数,一条 SELECT LIMIT 取数据。COUNT(*) 在无索引或大表上可能耗时数百毫秒甚至秒级。
第一步:在控制器中调用 paginate 时,将第二个参数设为 false:
$list = Db::name('order')->where('user_id', $uid)->paginate(20, false);
第二步:视图中渲染分页 HTML 时,改用 simple 模式避免依赖总页数:
{$list->simple()->render()}
第三步:若需保留搜索参数(如 ?keyword=abc&page=3),必须显式追加:
{$list->simple()->appends(request()->param())->render()}
这一步不补 appends,翻页后所有 GET 参数都会丢失,搜索结果无法持续。
用联合索引让 WHERE + ORDER 快速定位起始行
即使关掉 COUNT,如果 LIMIT 起始位置无法通过索引直接跳转,MySQL 仍要从头扫描。关键不是“有没有索引”,而是“索引是否覆盖 WHERE 条件 + ORDER BY 字段”。
方法一:检查当前查询是否走索引
执行 EXPLAIN SELECT * FROM order WHERE user_id = 123 ORDER BY create_time DESC LIMIT 20;,确认 【type 不是 ALL,key 显示实际索引名】。
方法二:建复合索引,顺序必须严格匹配查询逻辑
WHERE 先过滤 user_id,再按 create_time 倒序排列,索引字段顺序就得是 (user_id, create_time):
ALTER TABLE `order` ADD INDEX idx_uid_ctime (user_id, create_time DESC);
注意:MySQL 8.0+ 支持降序索引,5.7 及以下版本会忽略 DESC,但依然能用该索引加速 ORDER BY。
方法三:强制 NOT NULL 并验证字段可索引性
如果 create_time 允许 NULL,MySQL 可能放弃使用该索引。执行 ALTER TABLE `order` MODIFY COLUMN create_time DATETIME NOT NULL; 再重建索引。
替换 offset 分页为游标分页(Cursor-based)
当用户滑动到第 1000 页以上,offset 分页已不可逆地成为性能毒药。游标分页不依赖页码,只依赖上一页最后一条记录的锚点值,彻底避开 OFFSET 扫描。
方法1:基于自增主键 id 的区间查询(最稳定)
首次请求:取最新20条 → SELECT * FROM order WHERE user_id = 123 ORDER BY id DESC LIMIT 20;
下一页请求:带上上一页最小 id(假设为 8842)→ SELECT * FROM order WHERE user_id = 123 AND id
方法2:基于时间戳 + 主键防重复(适用于 create_time 可能重复场景)
首次请求:取最新20条 → SELECT id, user_id, create_time, title FROM order WHERE user_id = 123 ORDER BY create_time DESC, id DESC LIMIT 20;
下一页请求:带上上一页最后一条的 (create_time, id) 作为边界 → SELECT ... WHERE user_id = 123 AND (create_time, id)
这一步必须用 【ORDER BY create_time DESC, id DESC】双字段排序】,否则时间相同的数据会乱序,导致漏查或重复。
方法3:在 ThinkPHP 5.1 中封装游标逻辑(不依赖 paginate)
控制器中手动构造查询:
$last_id = input('last_id/d', 0);
$map = ['user_id' => $uid];
if ($last_id > 0) { $map['id'] = ['
$list = Db::name('order')->where($map)->order('id DESC')->limit(20)->select();
前端分页链接传参只需 ?last_id=8842,无需 page 参数,URL 更干净,也杜绝了恶意传入极大 page 值触发深 offset 查询。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











