PostgreSQLの「S_1」が既に存在するエラーをPgBouncerで修正する

intermediate🐘 PostgreSQL2026-04-23| PostgreSQL 12以降、PgBouncer 1.12以降、Node.js(Prisma/TypeORM)、Go(pgx)、またはPython(SQLAlchemy)

Error Message

ERROR: prepared statement "S_1" already exists
#postgresql#prepared-statement#pgbouncer#sql-error#devops

問題の概要PgBouncerを追加してスケーリングの問題を修正し、最初の5分間は完璧に動作していました。しかし突然、ログに以下のエラーが表示され始めます:

ERROR: prepared statement "S_1" already exists

これは通常、コネクションプーラーをtransactionモードに切り替えたときに発生します。ローカル開発環境ではほとんど再現しないため、「自分の環境では動く」という典型的なバグです。このエラーが表示されるのは、特定の並行処理の閾値—多くの場合、毎秒10〜20リクエスト程度—に達し、複数のクライアントが同じバックエンドのデータベース接続を共有し始めたときだけです。

原因根本的な原因は、アプリケーションのORMとコネクションプーラーの間で、データベースセッションの「所有権」に関する根本的な認識の不一致があることです。

競合の仕組みプリペアドステートメントは、特定の接続のメモリに保存されるサーバーサイドのオブジェクトです。ORMがクエリを送信する際、将来の実行時間を節約するためにクエリを「プリペア」することがよくあります。PostgreSQLはこのプランにS_1のような名前を割り当てます。

transactionモードでは、PgBouncerは忙しいウェイターのように動作します。1コース(トランザクション)の間だけテーブル(接続)を提供し、すぐに同じテーブルを別のお客様に渡します。しかし、ウェイターはテーブルを片付けません。 クラッシュが発生する仕組みは以下の通りです:

  • アプリインスタンスAが接続を取得し、ステートメントS_1を作成して処理を終え、接続を解放します。- PgBouncerはその接続をプールに戻しますが、S_1はその接続のメモリに残ったままです。- アプリインスタンスBが同じ接続を取得し、自身のS_1ステートメントを定義しようとします。- PostgreSQLはS_1という名前がすでに使用中であることを検知し、エラーをスローしてクエリが失敗します。## デバッグの手順コードを変更する前に、プーラーの設定を確認しましょう。設定ファイルを確認するか、サーバーで以下のコマンドを実行します:
grep "pool_mode" /etc/pgbouncer/pgbouncer.ini

pool_mode = transactionと表示された場合、衝突の原因が見つかりました。このモードでは50〜100個の実際のデータベース接続スロットで数千の接続を処理できますが、セッションが1つのクライアントにのみ属するという前提が崩れてしまいます。

解決策### 1. Prismaの「pgbouncer」フラグを使用するPrismaユーザーにとっては最も簡単な方法です。機能を無効化する必要はなく、PrismaにPgBouncerと共存していることを伝えるだけです。.envファイルの接続文字列に?pgbouncer=trueを追加します:

DATABASE_URL="postgresql://user:pass@db-host:6432/mydb?pgbouncer=true"

このフラグにより、Prismaは匿名のプリペアドステートメントを使用するようになります。S_1のような名前がないため、衝突が発生しません。

2. ORMでプリペアドステートメントを無効化するほとんどのアプリケーションでは、プリペアドステートメントによるパフォーマンス向上は3〜5%未満であることが多く、不安定さを招くリスクに見合わないことがほとんどです。ORMレベルで無効化することが最も堅牢な修正方法です。

  • **TypeORM:**データソースオプションでprepareThreshold: 0を設定します。- **Sequelize:**設定でminifyAliases: trueを設定します。- Go (pgx):接続設定でPreferSimpleProtocol: trueを設定します。以下はTypeORMの標準的な修正例です:
{ 
  type: "postgres",
  host: "localhost",
  port: 6432,
  extra: {
    max_prepared_statements: 0,
    prepareThreshold: 0
  }
}

3. PgBouncerの新機能を使用する(v1.22.0以降)インフラをアップグレードできる場合、PgBouncer 1.22.0でこの問題がネイティブに解決されました。プリペアドステートメントをトラッキングできるようになっています。pgbouncer.iniに以下を追加します:

max_prepared_statements = 100

これにより、PgBouncerが透過的にクリーンアップを処理できるようになります。Ubuntu 22.04などの古いLTSを使用している場合、バージョンは1.16である可能性が高いため、ORM側の修正を使用する必要があります。

4. 最終手段:sessionモードに切り替えるアプリケーションがセッションレベルの状態に大きく依存しており、コードを変更できない場合は、sessionモードに切り替えます:

[pgbouncer]
pool_mode = session

**注意:**これにより「多重化」のメリットがなくなります。500個のアプリインスタンスがあり、データベース接続が100個しかない場合、400個のインスタンスは即座に接続に失敗します。

動作確認修正を適用したら、うまくいくことを祈るだけで終わりにしてはいけません。簡単な負荷テスト中にログを監視しましょう。wrkなどのツールを使用して、データベースへの負荷が最も高いエンドポイントにアクセスします:

wrk -t12 -c40 -d30s http://localhost:3000/api/users

その間、Postgresのログを監視します:

tail -f /var/log/postgresql/postgresql-main.log | grep "already exists"

テストが完了してもログに何も表示されなければ、プーリングの問題は解決されています。

まとめ- **接続をステートレスとして扱う:**transactionモードでは、次のクエリが同じ物理接続で実行されると仮定してはいけません。- **ORMの設定を確認する:**本番環境に移行する前に、使用しているライブラリに「PgBouncer」または「Simple Protocol」モードがないか必ず確認しましょう。- **常に最新版を使用する:**PgBouncerチームはこれらの問題を積極的に解消しています。ミドルウェアを最新の状態に保つことで、デバッグに費やす時間を大幅に削減できます。

Related Error Notes