ThinkPHP Pgsql 驱动聚合查询引用陷阱
在 ThinkPHP 项目里用 PostgreSQL 做统计查询时踩到一个驱动层缺陷:带聚合函数或别名的查询,生成的 SQL 引用不正确。这篇文章完整记录复现过程、根因和规避方案。
问题现象
一个看起来很常规的计数查询:
1 | $count = Db::name('user') |
预期生成:
1 | SELECT COUNT(*) AS tp_count FROM "tb_user" WHERE "status" = 1 LIMIT 1 |
实际生成的 SQL 里,聚合表达式被当成了普通字段名处理,tp_count 别名没有被正确剥离,PostgreSQL 直接报语法错误。同类问题在 field('COUNT(id) as total')、group() 之后的聚合列上都会出现。
根因定位
问题出在 Pgsql 驱动的 parseKey 方法。ThinkPHP 的查询构造器会把每个字段表达式过一遍 parseKey,由各驱动决定如何加引号。Pg 驱动的实现大致是:
1 | protected function parseKey($key, $options = []) |
1 |
|
用 fetchSql() 打印而不是真执行,能直接看到被破坏的 SQL。
规避方案
在驱动修复之前,业务代码绕开这三类写法:
1. 聚合统计改用 query() 原生查询
1 | $total = (int) Db::query( |
直写 SQL 最稳,参数化绑定防注入,统计类查询本来也不复杂。
2. 需要构造器链式调用时,用 fieldRaw() 传表达式
1 | $list = Db::name('order') |
fieldRaw / havingRaw / whereRaw / orderRaw 这一族方法原样透传,不过 parseKey,是构造器场景下的安全通道。
3. count() 的替代:先查 ID 再 PHP 侧统计
1 | $ids = Db::name('user')->where('status', 1)->column('id'); |
数据量大时不如方案 1,但小表场景完全够用,且零 SQL 风险。
建议的修复方向
根治要在驱动层:parseKey 增加表达式识别——输入中包含 (、)、+、-、*、|| 等字符,或匹配 AS xxx 结尾时,视为表达式不加引号(必要时只给其中的裸标识符加引号)。向上游提 PR 时注意兼容 JSON -> 操作符和数组下标的既有逻辑。
经验总结
| 场景 | 安全写法 | 风险写法 |
|---|---|---|
| 单表计数 | Db::query() 参数化 | ->count() |
| 分组聚合 | fieldRaw / havingRaw | field('SUM(x) as y') |
| 排序表达式 | orderRaw | order('SUM(x)') |
| 普通等值查询 | where('field', $v) | ——(无风险) |
ORM 的便利性永远伴随着”它帮你做了什么你不知道”的风险。涉及聚合、表达式、别名的查询,养成用 fetchSql() 看一眼生成 SQL 的习惯,能避开这一类坑的绝大多数。