这看起来像是 JDBC API 限制加上 PostgreSQL 对GROUP BY
子句相当严格。
关键的区别在于您的手动 PgAdmin 测试使用单个参数并在查询中使用它两次。相比之下,Hibernate 查询将值作为两个单独的参数传递两次。有时PREPARE
PostgreSQL 不能证明$1
总是相等$2
的,即使在实践中你知道它们会,所以 PostgreSQL 拒绝计划查询。
问题演示
演示设置:
CREATE TABLE somedemo( x integer, y integer );
INSERT INTO somedemo(x,y) SELECT a,a from generate_series(1,15) a;
演示 1,文本替换,工作正常:
SELECT x, (y+1) FROM somedemo GROUP BY x, y+1;
Demo 2,单参数,工作正常,因为 Pg 可以证明(y+$1)
在一个地方总是等于(y+$1)
在另一个地方:
PREPARE preptest1(integer) AS select x, (y+$1) from somedemo GROUP BY x, y+$1;
EXECUTE preptest1(1);
Demo 3,两个参数。失败是因为 Pg 无法证明它(y+$1)
等于(y+$2)
时间PREPARE
:
regress=> PREPARE preptest2(integer,integer) AS SELECT x, (y+$1) FROM somedemo GROUP BY x, y+$2;
ERROR: column "somedemo.y" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: PREPARE preptest2(integer,integer) AS SELECT x, (y+$1) FROM ...
^
这在您强制协议级别 2 时有效,因为 JDBC 驱动程序替换了服务器端的参数。
其他语言如何处理这个
在 Python+psycopg2 或其他具有更复杂数据库驱动程序的语言中,我将使用命名或位置参数来处理这个问题:
$ python
Python 2.7.3 (default, Aug 9 2012, 17:23:57)
>>> import psycopg2
>>> conn = psycopg2.connect('')
>>> curs = conn.cursor()
>>> curs.execute("SELECT x, (y+%(parm)s) FROM somedemo GROUP BY x, y+%(parm)s", { 'parm': 1 })
>>> curs.fetchall()
[(15, 16), (3, 4), (12, 13), (14, 15), (10, 11), (11, 12), (8, 9), (5, 6), (13, 14), (1, 2), (2, 3), (4, 5), (7, 8), (9, 10), (6, 7)]
>>>
不幸的是,看起来 JDBC 只支持 ; 中的命名参数CallableStatement
。我们再次看到 Java 遗留问题的痛苦来咬我们。
为什么修复它并不简单
为了处理这个服务器端,PostgreSQL 将不得不延迟计划这些语句,直到它获得参数,然后将其作为常规的临时查询执行。尽管引入准备好的语句重新计划已经奠定了一些基础,但目前尚不支持这样做。
目前尚不清楚我们将如何在 JDBC 驱动程序端透明地处理它。即使我们延迟发送准备好的语句直到我们得到第一组参数,我们也不知道“$1”总是等于“$2”(并且可以组合),因为它们在第一次执行时是相等的。 ..
Hibernate 无法解决这个问题;它知道这:p1
在所有三个地方都是同一个参数,但它无法通过 JDBC 位置参数接口的限制告诉 PostgreSQL。它可以将所有参数替换到查询文本中,但这几乎总是错误的做法,这是一个相当不寻常的极端情况。
我看到的唯一可靠的解决方法是让 PgJDBC 使用命名或序数参数(如?:p1
or )扩展 JDBC ?:1
。然后可以扩展 Hibernate 的 PostgreSQL 方言以支持它们。为了避免兼容性问题,需要设置连接参数才能启用扩展参数语法。这一切看起来都很痛苦,所以我宁愿等到 JDBC 规范添加真正的命名参数支持(即:不要屏住呼吸,你的孙子可能会活着看到它发生)或者只是解决这个问题。
解决方法
我怀疑最好的选择是使用子查询来生成具有生成值的虚拟表,然后在外部查询中对其进行分组。执行此操作的 SQL 如下所示:
SELECT x, y_plus FROM (
SELECT x, (y+?) FROM somedemo
) temptable(x,y_plus)
GROUP BY x, y_plus;
此措辞只需要对参数的一次引用。将其翻译成 HQL 留给读者作为练习;-)。
PostgreSQL 的查询优化器通常会将其转换为与简单字符串替换形式一样高效的计划,如下所示:
regress=> PREPARE preptest5(integer) AS SELECT x, y_plus FROM (SELECT x, (y+$1) FROM somedemo) temptable(x,y_plus) GROUP BY x, y_plus;
regress=> explain EXECUTE preptest5(1);
QUERY PLAN
---------------------------------------------------------------
HashAggregate (cost=1.26..1.45 rows=15 width=8)
-> Seq Scan on somedemo (cost=0.00..1.19 rows=15 width=8)
(2 rows)
regress=> explain SELECT x, y+1 FROM somedemo GROUP BY x, y+1;
QUERY PLAN
---------------------------------------------------------------
HashAggregate (cost=1.26..1.45 rows=15 width=8)
-> Seq Scan on somedemo (cost=0.00..1.19 rows=15 width=8)
(2 rows)
对于非性能关键的临时或不经常使用的功能,您可以通过编写在 CTE VALUES 子句中传递参数一次的本机查询来解决此问题,例如:
PREPARE preptest3(integer) AS
WITH params(a) AS (VALUES($1))
SELECT x, (y+a) FROM somedemo CROSS JOIN params GROUP BY x, y+a;
EXECUTE preptest3(1);
不用说这很笨拙,可能表现得不是特别好,但它适用于您必须在许多不同上下文中引用参数的情况。
如果您不能使用前面从 HQL 中列出的子查询表方法,那么 hacky CTE 的更好替代方法是将查询包装在 SQL 函数中并从 JDBC 调用该函数,例如:
-- Define this in your database schema or run it on app startup:
CREATE OR REPLACE FUNCTION test4(integer) RETURNS TABLE (x integer, y integer) AS $$
SELECT x, (y+$1) FROM somedemo GROUP BY x, y+$1;
$$ LANGUAGE sql;
-- then in JDBC prepare a simple "SELECT * FROM test4(?)", resulting in:
PREPARE preptest4(integer) AS SELECT * FROM test4($1);
EXECUTE preptest4(1);