PostgreSQLでの『ERROR: prepared statement already exists』の原因と対処法

PostgreSQLでの『ERROR: prepared statement already exists』の原因と対処法

PostgreSQLを使った開発中に頻出する『ERROR: prepared statement already exists』。このエラーが発生する具体的な条件やその回避策、根本的な修正方法を体系的にまとめました。

エラーの概要

このエラーは、同じ名前のプリペアドステートメントを再び作成しようとした場合に発生する。

ERROR:  prepared statement "your_statement_name" already exists

ステートメント名がPostgreSQL内でまだ有効な状態で残っているにも関わらず、同じ名前で新たにPREPARE文や同等の処理を行うと、このエラーが返される。

発生条件

  • アプリケーション側でステートメント名を明示的に指定している場合
  • 接続プールが有効で、コネクションが再利用される環境
  • ステートメントの解放(DEALLOCATE)を忘れている
  • フレームワークやORMが自動的に同名のステートメントを生成している

エラーが発生するサンプル

PREPARE my_stmt AS SELECT * FROM users WHERE id = $1;
PREPARE my_stmt AS SELECT * FROM users WHERE name = $1;
-- ↑ ここで "my_stmt" はすでに存在しているため、エラーになる

対応策1: DEALLOCATEで明示的に開放する

使い終わったステートメントは明示的に解放する。

DEALLOCATE my_stmt;
PREPARE my_stmt AS SELECT * FROM users WHERE name = $1;

対応策2: IF NOT EXISTS構文は存在しない

PostgreSQLには、PREPARE IF NOT EXISTSのような構文は存在しない。そのため、存在チェックを別の手段で行う必要がある。

対応策3: try-catchで握り潰す(PL/pgSQLの場合)

PL/pgSQLなどでラップされている場合、例外を握り潰すことで回避できるケースもある。

DO $$
BEGIN
    BEGIN
        PREPARE my_stmt AS SELECT * FROM users WHERE id = $1;
    EXCEPTION
        WHEN duplicate_prepared_statement THEN
            -- すでに存在しているのでスキップ
    END;
END;
$$;

対応策4: DEALLOCATE ALLでまとめて削除

一括でステートメントを削除する方法。セッション終了時にクリーンアップしたい場合に使う。

DEALLOCATE ALL;

※ ただし、これによりすべてのプリペアドステートメントが削除されるため、副作用に注意。

対応策5: アプリケーション側でステートメント名を一意にする

ステートメント名をランダムまたはユニークな接頭辞付きで作成することで競合を避けられる。

-- Javaなどの例
String stmtName = "stmt_" + UUID.randomUUID().toString();

対応策6: ORマッパー・フレームワークの設定を確認する

PostgreSQLと接続するORM(例: psycopg2, JDBC, Sequelizeなど)が内部で自動的にプリペアドステートメントを管理していることがある。

設定例(psycopg2):

# デフォルトで prepared=True の場合
cursor.execute("...", params, prepared=False)

対応策7: ステートメントキャッシュの制限を設ける

ドライバやORMによっては、キャッシュされるステートメント数を制御できる。

例(JDBC):

jdbc:postgresql://localhost:5432/mydb?preparedStatementCacheQueries=0

対応策8: PostgreSQLのログで原因を調査する

PostgreSQLのlog_statementlog_min_duration_statementを有効にしておくことで、どのタイミングでステートメントが作成されているかを追跡可能。

# postgresql.conf
log_statement = 'all'
log_min_duration_statement = 0

根本的なアプローチ

  • アプリケーションが同一セッションで同名のプリペアドステートメントを複数回使わないような設計に変更する
  • 接続ごとに状態を初期化するようにする
  • DBコネクションプールの再設計を検討

まとめ

このエラーは単なる構文ミスではなく、状態を持ちすぎているセッション設計に原因があるケースが多い。セッションごとにプリペアドステートメントを管理する責任を明確にし、競合を避ける実装・設計を心がけたい。