ThinkPHP 6 的 union 不支持直接分页,需手动实现:先统计各子查询总数并累加得 $total,再按页码计算 $offset,最后对每个子查询分别加 limit 和 offset 后 union;若需分页渲染,须手动构造 Paginator 实例。

ThinkPHP 6 的 union 不支持直接分页
直接对 union 查询对象调用 paginate() 会报错或返回空,因为 ThinkPHP 的分页底层依赖 count() 和 limit/offset,而原生 UNION 结果集无法被框架自动推导总条数和分段偏移。这不是 bug,是设计限制——union 是多查询合并,不是单表或单视图。
手动实现 union 分页的三步关键操作
核心思路:先查总数,再按页码算 offset + limit,最后用 union 拼接带分页的子查询。注意必须给每个 SELECT 子句加上相同字段名和顺序,否则 UNION 会失败。
- 第一步:用
count(1)分别统计各子查询条数,加总得到$total - 第二步:根据当前页码
$page和每页条数$listRows计算$offset = ($page - 1) * $listRows - 第三步:对每个子查询分别加
limit $listRows offset $offset(⚠️不是在 union 后加 limit),再用union()连接
示例(两个模型联合):
$sub1 = Db::table('article')->where('status', 1)->field('id,title,created_at as time')->limit($listRows)->offset($offset);
$sub2 = Db::table('news')->where('status', 1)->field('id,title,updated_at as time')->limit($listRows)->offset($offset);
$result = $sub1->union($sub2)->select();注意:limit 和 offset 必须写在每个子查询上,且不能省略 offset,否则第 2 页开始数据重复。
立即学习“PHP免费学习笔记(深入)”;
用 paginate() 包装 union 结果需自行构造 Paginator 实例
如果仍想用框架分页渲染(比如 render() 输出页码 HTML),就不能依赖 Db::union()->paginate(),而要手动构造 think\Paginator\driver\Bootstrap 或对应驱动的实例:
- 传入
$result(上面查出的数组)作为数据 - 传入
$total(前面算出的总数) - 传入
$listRows、$page、$query(URL 参数)等必要参数
示例片段:
$paginator = new Bootstrap($result, $total, $listRows, $page, [
'query' => request()->param(),
]);这样得到的 $paginator 就能正常使用 {$pages->render()} 模板语法了。
性能与兼容性注意事项
多个子查询都带 limit+offset 并 union,在数据量大时可能比单表慢,尤其当某些子查询实际无数据却仍执行全表扫描。建议:
- 每个子查询务必有有效索引(如
WHERE字段 +ORDER BY字段组合索引) - 避免在
union中使用ORDER BY(MySQL 不允许子查询里直接写,除非套一层(SELECT ... ORDER BY) AS t) - ThinkPHP 5.1 和 6.x 的
union()方法签名略有差异:union($query, $all = false),TP6 默认不加ALL,去重开销略高;若业务允许重复,显式传true
最易忽略的一点:所有子查询的字段数量、类型、别名必须严格一致,否则 MySQL 报错 ERROR 1222 (21000): The used SELECT statements have a different number of columns,调试时别只盯 PHP 层,要打印出最终 SQL 去数据库里试跑。



















