ThinkPHP 6 分页+GROUP BY 报错是因 MySQL 严格模式拦截非法查询,需用 countQuery 手动指定分组后总组数,确保 SELECT 字段与 GROUP BY 兼容,避免 JOIN 导致重复计数。

ThinkPHP 6 分页 + GROUP BY 报错 SQLSTATE[HY000]: General error: 1055
直接在 paginate() 前加 group(),90% 情况会触发 MySQL 严格模式报错。这不是 ThinkPHP 的 bug,是 MySQL 5.7+ 默认开启的 sql_mode=ONLY_FULL_GROUP_BY 在拦截“非聚合字段出现在 SELECT 中但未出现在 GROUP BY”的非法查询。
常见错误写法:
$list = Db::name('order')
->field('user_id, count(*) as cnt')
->group('user_id')
->paginate(10); // 报错:user_id 不在 GROUP BY?但它明明在!
原因在于 ThinkPHP 分页会自动添加 COUNT(*) 查询做总条数统计,而这个 COUNT 查询不带 GROUP BY,导致 MySQL 拒绝执行整个语句。
- 必须显式绕过分页的自动 COUNT,改用
countQuery手动提供分组后的总组数 - 不能依赖
field()简化字段,需确保 SELECT 字段与 GROUP BY 完全兼容(或全为聚合) - MySQL 8.0+ 同样适用该规则,不是低版本兼容问题
手动构造分组分页:用 countQuery 替换默认 COUNT
核心是把“查多少组”和“查每组数据”拆开:前者用子查询算组数,后者正常分组查明细。ThinkPHP 6.1+ 支持 countQuery 参数接管总数逻辑。
立即学习“PHP免费学习笔记(深入)”;
正确示例(查每个用户的订单数及最新订单时间):
$countQuery = Db::name('order')
->field('user_id')
->group('user_id')
->buildSql();
<p>$list = Db::name('order')
->field('user_id, count(<em>) as order_count, max(create_time) as last_time')
->group('user_id')
->having('count(</em>) > 1') // 可选:组内过滤
->paginate([
'list_rows' => 15,
'countQuery' => $countQuery,
]);
-
buildSql()生成子查询 SQL(如(SELECT COUNT(*) FROM (SELECT user_id FROM `order` GROUP BY user_id) AS tp_count)),确保 COUNT 结果是组数而非原始记录数 -
having必须写在group之后,用于过滤分组结果;where是过滤分组前的行,二者不可互换 - 若模型中使用,
buildSql()需调用$this->db()->...而非静态Db::,避免连接上下文丢失
ThinkPHP 5.1 兼容写法:先查总数再查数据
TP5.1 不支持 countQuery,只能手动两步走。注意别漏掉 Page 类的引入和总条数赋值。
use think\Paginator;
<p>$groupCount = Db::name('order')
->field('user_id')
->group('user_id')
->count(); // 得到分组后总组数</p><p>$page = input('page', 1);
$limit = 10;
$offset = ($page - 1) * $limit;</p><p>$data = Db::name('order')
->field('user_id, count(*) as cnt, max(create_time) as latest')
->group('user_id')
->limit($offset, $limit)
->select();</p><p>// 手动构建分页对象
$paginater = Paginator::make($data, $groupCount, $limit, $page, [
'path' => request()->url(),
'query' => request()->param(),
]);
- 不要用
paginate()自动计算总数,否则又会触发 GROUP BY 冲突 -
max(create_time)这类聚合函数必须和GROUP BY字段对齐,否则 MySQL 仍可能报错(取决于 sql_mode) - 如果分组字段有索引(如
user_id),GROUP BY性能尚可;若用函数分组(如DATE(create_time)),务必在该表达式上建函数索引,否则分页会越来越慢
关联表分组分页:避免 JOIN 导致重复计数
查「用户 + 订单数 + 最新订单商品名」这类需求时,JOIN 会让分组 COUNT 失真——一个用户多订单多商品,COUNT(*) 会算成笛卡尔积数量。
安全做法是用子查询或 withCount 预加载(TP6.0+):
// 方案一:子查询(兼容 TP5/6)
$subSql = Db::name('order')
->field('user_id, count(*) as order_count, max(id) as last_order_id')
->group('user_id')
->buildSql();
<p>$list = Db::name('user')
->alias('u')
->field('u.*, o.order_count, o.last_order_id')
->join([$subSql => 'o'], 'u.id = o.user_id', 'LEFT')
->paginate(10);</p><p>// 方案二:TP6 withCount(更简洁,但只适合单 COUNT 场景)
$list = UserModel::withCount('orders')->paginate(10);
- JOIN 分组结果时,子查询必须包含全部 JOIN 条件字段(如
user_id),且别名要唯一 -
withCount底层也是子查询,但无法同时取max(create_time)这类额外聚合字段,需按需选择 - 千万避免在 JOIN 后直接
GROUP BY user_id并COUNT(*),这是最典型的重复计数陷阱
分组分页真正难的不是语法,而是想清楚“总数到底该算什么”——是原始记录数?分组数?还是满足某条件的分组数?一旦 COUNT 逻辑和业务语义对不上,后面所有分页都错得悄无声息。



















