GROUP BY
これは、JDBC API の制限と、PostgreSQL が句に関してかなり厳密であることを組み合わせたように見えます。
重要な違いは、手動の PgAdmin テストでは 1つのパラメーターを使用し、クエリで 2 回使用することです。対照的に、Hibernate クエリは値を 2つの個別のパラメーターとして 2 回渡します。PostgreSQLは、実際には等しいことがわかっていても、それが常に等しいPREPARE
ことを証明できないため、 PostgreSQL はクエリの計画を拒否します。$1
$2
問題のデモ
デモのセットアップ:
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;
デモ 2、単一パラメーター、Pg はある場所では常に別の場所(y+$1)
と等しいことを証明できるため、正常に動作します。(y+$1)
PREPARE preptest1(integer) AS select x, (y+$1) from somedemo GROUP BY x, y+$1;
EXECUTE preptest1(1);
デモ 3、2 つのパラメーター。Pg がat time(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 ...
^
これは、JDBC ドライバーがサーバー側のパラメーターを置き換えるため、プロトコル レベル 2 を強制するときに機能します。
他の言語がこれをどのように処理するか
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 ではこれを修正できません。3 つの場所すべてで同じパラメータであることはわかってい:p1
ますが、JDBC の位置パラメータ インターフェイスの制限により、PostgreSQL にそのことを伝える方法がありません。すべてのパラメーターをクエリ テキストに置き換えることもできますが、ほとんどの場合、これは間違った方法です。これはかなり珍しいコーナー ケースです。
これに対する唯一の確実な修正は、 PgJDBC が、 または などの名前付きまたは序数パラメーターで JDBC を拡張すること?:p1
です?:1
。次に、Hibernate の PostgreSQL ダイアレクトを拡張して、それらをサポートすることができます。互換性の問題を回避するには、拡張パラメーター構文を有効にするために接続パラメーターを設定する必要があります。これはすべて非常に面倒に思えるので、JDBC 仕様が実際の名前付きパラメーターのサポートを追加するまで待つことをお勧めします (つまり、息を止めないでください。あなたの孫がそれが起こるのを見るために生きているかもしれません)。
回避策
サブクエリを使用して、生成された値で仮想テーブルを生成し、それを外側のクエリでグループ化するのが最善の方法だと思います。これを行うための SQL は次のようになります。
SELECT x, y_plus FROM (
SELECT x, (y+?) FROM somedemo
) temptable(x,y_plus)
GROUP BY x, y_plus;
この言い回しでは、パラメーターへの参照は 1 つだけ必要です。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 句でパラメーターを 1 回渡すネイティブ クエリを作成することで、これを回避できます。
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 から以前に設定されたサブクエリ テーブル アプローチを使用できない場合、ハッキーな 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);