thinkphp 6 的 wherein 分页必须先查 id 再分页,因 paginate() 自动追加的 count(*) 无法复用 where in 的复杂条件,导致总数与数据不一致;正确做法是先 column('id') 获取主键列表,再基于 id 链式调用 paginate()。

ThinkPHP 6 的 whereIn 分页必须先查 ID 再分页
直接对 whereIn 查询链式调用 paginate() 会导致分页错乱或 SQL 报错,尤其在关联查询、子查询或字段别名场景下。根本原因是 ThinkPHP 6 的 paginate() 默认会自动追加 COUNT(*) 统计,而带 WHERE IN 的复杂查询中,COUNT 子句可能无法复用主查询的 JOIN 或 WHERE 条件,最终统计数和实际数据不一致。
正确做法是:先用 column('id') 或 pluck('id') 提取符合条件的主键 ID 列表,再基于这些 ID 做二次分页查询。
-
whereIn条件应尽量精简,只保留必要字段(如状态、类型),避免在分页前就 JOIN 大表 - ID 列表不宜过大,超过 5000 条建议改用区间分页(
BETWEEN+id > ?)或缓存 ID 集合 - 若需返回总数,用
count()单独查一次,不要依赖paginate()自动统计
TP6 中 whereIn 分页的两步写法(推荐)
以「查出指定分类下已上架的商品并分页」为例,假设 $cateIds = [1, 5, 8]:
// 第一步:获取满足条件的 id 列表(注意去重、限制数量防爆内存)
$ids = ProductModel::where('status', 1)
->whereIn('category_id', $cateIds)
->column('id'); // 返回一维数组,如 [101, 102, 105, ...]
<p>// 第二步:用 id 列表分页(确保主键为 id,且已建索引)
$list = ProductModel::whereIn('id', $ids)
->order('sort', 'desc')
->paginate([
'list_rows' => 20,
'query' => request()->param() // 保持 URL 参数
]);</p>
这个写法绕过了 COUNT 和主查询结构不一致的问题,paginate() 内部执行的是简单 WHERE id IN (?),稳定可靠。
- 如果
$ids为空数组,whereIn('id', [])在 TP6+MySQL 下会查不到数据(符合预期),无需额外判空 - 不能用
select()后再手动切片,会丢失分页对象的render()、lastPage()等方法 - 如需关联数据(如查商品+分类名),在第二步的查询里用
with('category'),不要在第一步加
TP5.1 的 paginate() 对 whereIn 更宽容,但仍有陷阱
TP5.1 允许直接链式写 ->whereIn(...)->paginate(),底层会尝试重构 COUNT 查询。但遇到以下情况仍会翻车:
- 使用了
field()且字段含表达式(如'price * discount as final_price'),COUNT 语句无法生成 - 用了
union()或子查询作为主表 - MySQL 版本低于 5.7,不支持某些派生表语法
错误现象通常是:SQLSTATE[42000]: Syntax error or access violation 或分页显示 15 条却说共 100 页。此时必须退回到「先取 ID 再分页」的老办法,逻辑与 TP6 一致。
为什么不用 Db::raw 拼 IN 字符串?
有人试过这样写:where(Db::raw('id IN (' . implode(',', $ids) . ')')) —— 这非常危险:
- 没做整型过滤,
$ids若来自用户输入,直接导致 SQL 注入 - MySQL 默认
max_allowed_packet限制,ID 过多时触发Packets larger than max_allowed_packet - 无法利用预处理参数绑定,PDO 层面失去类型校验和缓存优势
ThinkPHP 的 whereIn() 内部会自动参数化、分段(默认每 1000 个值一组),安全且兼容。手拼字符串是倒退。
真正要注意的是 ID 列表长度和数据库配置匹配,而不是换写法。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











