コネクションとセッション状態・プロトコルパイプライン
DB が遅い原因の多くはクエリ自体でなく往復回数です。ワイヤプロトコルのプリペア・バインド・実行、セッション状態、カーソル、パイプライン化でラウンドトリップを削る原理がつかめます。
- 1.DB のワイヤプロトコルはパース・バインド・実行・フェッチに分かれ、各段が原則1往復のネットワークコストを伴うため、回数を減らす設計が支配的に効く。
- 2.プリペアドステートメントは解析済みプランをサーバーのセッションに保持し、以後はバインド値だけ送って再実行する。プランやカーソルはセッション状態に属し、接続を共有・移し替えると壊れる。
- 3.パイプライン化は応答を待たずに複数要求を連続送信して往復を1回に畳む手法で、依存のない一括処理で遅延を桁で削減できる。
クエリより往復が支配する
同じ SQL でも、ネットワークを挟むと実行時間の正体は「クエリの計算量」より「クライアントとサーバーの往復回数(ラウンドトリップ)」になることが多々あります。1 往復の遅延が同一データセンター内で 0.5 ミリ秒、リージョン跨ぎなら数十ミリ秒に達するため、1 件処理するのに 4 往復かかる設計を 1 往復に畳めれば、クエリを最適化するより劇的に速くなります。本稿は DB の ワイヤプロトコル(クライアントとサーバーが交わすバイト列の取り決め)を内部から追い、プリペア・バインド・実行、セッション状態、カーソル、パイプライン化がどこで往復を生み、どう削るかを正確に整理します。
ワイヤプロトコルの段階:パース・バインド・実行
多くの DB のプロトコルは、1 つのクエリ実行を複数のメッセージに分解します。PostgreSQL の拡張クエリプロトコルを例にとると、論理的な段階は次のとおりです。
- Parse:SQL 文字列を受け取り、構文解析・検証してプリペアドステートメント(解析済みの骨格)を作る。プレースホルダ(
$1など)の型もここで確定する。 - Bind:プリペアドステートメントに実引数を束ね、ポータル(実行可能なインスタンス)を作る。最適化(プラン生成)はこの段で行われ得る。
- Execute:ポータルを実行し、結果行(行データ)をクライアントへ返す。取得行数の上限を指定でき、カーソルとして部分取得もできる。
- Sync:処理の区切りを示し、暗黙のトランザクションを閉じてサーバーが「次の入力を受付可能」(ReadyForQuery)を返す。エラー時はこの Sync まで以降の要求が読み飛ばされる。
Parse ("SELECT * FROM orders WHERE customer_id = $1") -- 骨格を1回作る
Bind ($1 = 42) -- 値を束ねてポータル化
Execute -- 実行して行を返す
Sync -- 区切り
素朴に書くと各段で往復が発生しますが、ドライバは複数メッセージを 1 つの TCP 送信にまとめ、最後にまとめて応答を読みます。重要なのは、段が分かれていること自体がパイプライン化の余地を作る点です。なお値を SQL 文字列へ文字列連結せずプレースホルダで分離するため、プリペアドステートメントは SQL インジェクション耐性の根拠にもなります。
プリペアドステートメントとプラン再利用
Parse の成果物であるプリペアドステートメントは、同じ骨格のクエリを値だけ変えて何度も実行するときに効きます。骨格を一度だけ解析・最適化し、以後はバインド値を送るだけで再実行できるため、パース・最適化の CPU と、プランキャッシュ自体への内部ロック競合を節約できます(→ プランキャッシュとパラメータスニッフィングの内部)。
プリペアには 2 系統あります。PREPARE 文で明示するサーバーサイド・プリペアと、ドライバが内部で名前付きステートメントを張る暗黙のプリペアです。後者は「初回は Parse+Bind+Execute、2 回目以降は Bind+Execute だけ」という最適化を透過的に行います。
サーバーサイドのプリペアドステートメントは、それを張ったセッションにのみ存在します。PgBouncer のトランザクション/ステートメントモードのように、リクエストごとに別の物理接続へ多重化する外部プーラーを挟むと、Parse したセッションと Execute するセッションが食い違い「prepared statement does not exist」が出ます。対策はプリペアを無効化(毎回テキストプロトコルで送る)するか、プーラー側のプロトコル対応・プランキャッシュ機能を使うことです。
セッション状態:接続は使い回せても状態は引き継げない
ワイヤプロトコルの設計を理解するうえで決定的なのが、セッション状態という概念です。1 本の接続には、その接続に紐づく可変状態がぶら下がります。代表例は次のとおりです。
| セッション状態の例 | 内容 | 接続を移すと |
|---|---|---|
| プリペアドステートメント | Parse 済みの骨格とプラン | 消える(再 Parse が必要) |
| オープン中のトランザクション | 未コミットの変更・取得済みロック | 宙づり・破綻する |
| カーソル / ポータル | 結果集合の現在位置 | 失われる |
| 一時テーブル | そのセッション限定の中間データ | 見えなくなる |
| セッション変数・設定 | search_path・タイムゾーン・分離レベル等 | 既定へ戻る |
| advisory ロック | 明示的に取った助言ロック | 解放される |
ここがコネクションプーリング(→ コネクションプーリング)の設計を縛ります。プールは接続を借りて返すことで確立コストを償却しますが、返す前にセッション状態をリセットしないと、次に借りた処理へ前の状態が漏れます。トランザクションを開けっぱなしで返せば次の利用者が巻き込まれ、一時テーブルやセッション変数が残れば不可解なバグになります。だからこそプーラーは返却時に未完トランザクションをロールバックし、設定を初期化します。「接続は共有資源だが、セッション状態は私物」という非対称性が要点です。トランザクションそのものの境界とコミットの意味は別稿に譲ります(→ トランザクション)。
カーソル:結果を小出しにして往復とメモリを抑える
巨大な結果集合を一度に返すと、サーバーは全行を作り切るまで応答を返せず、クライアントも全行をメモリに載せます。カーソルは結果集合に対する現在位置のついた窓で、Execute の取得行数上限や FETCH によって部分的に行を取り出せます。これにより最初の 1 行までの待ち時間(time-to-first-row)が縮み、メモリ使用も一定に保てます。
ただしカーソルもセッション状態です。多くの DB ではトランザクション内でのみ生存する(WITHOUT HOLD)ため、FETCH のたびに別接続へ振り分けるような構成では使えません。また 1 行ずつ FETCH すれば行数ぶんの往復が発生するので、実務ではフェッチサイズを数百〜数千に設定し、まとめて取得して往復を償却します。小さすぎれば往復過多、大きすぎればメモリ過多という、典型的なバッチサイズのトレードオフです。
-- フェッチサイズ=1000 のとき
FETCH 1000 -- 1往復で1000行
FETCH 1000 -- 次の1000行
...
パイプライン化:応答を待たずに送る
ワイヤプロトコルが段に分かれている恩恵が最大化されるのがパイプライン化です。通常は「要求を送る → 応答を待つ → 次を送る」と直列に進み、N 件の操作で N 往復します。パイプライン化では、前の応答を待たずに次の要求をネットワークへ流し込み、応答はあとでまとめて読みます。要求と応答の順序対応はプロトコルが保証するため、論理的な正しさは保たれます。
| 方式 | N件の往復回数 | 前提条件 |
|---|---|---|
| 逐次(要求ごとに待つ) | N 往復 | 各操作が前の結果に依存してよい |
| パイプライン | 実質1往復ぶんの遅延 | 操作間に結果依存がない |
| バッチ/多値INSERT | 1往復 | 同種操作を1文に畳める |
効くのは操作どうしに結果依存がない場合です。たとえば 1000 件の独立した INSERT は、1 件ずつ往復させると 1000 往復ですが、パイプラインで一気に流せば、ネットワーク遅延の支配は実質 1 往復ぶんに縮みます。「直前の取得結果を見て次の SQL を決める」ような依存がある処理は畳めません。
パイプライン中の 1 件が失敗したとき、どこまでが実行済みかはトランザクション境界に依存します。多くの実装は、エラー以降の要求を同一トランザクション内ではスキップし、次の Sync まで読み飛ばします。畳んだ複数操作を「全部成功か全部失敗か」にしたいなら、明示的に 1 つのトランザクションで囲むのが安全です。Sync をどこに置くかが、原子性とエラー時の挙動を決めます。
プロトコルの段:Parse(骨格作成)→ Bind(値束ね+プラン)→ Execute(実行)→ Syncを即答できること。プリペアドステートメント・カーソル・トランザクション・一時テーブル・セッション変数はすべてセッション状態で、接続を移すと壊れる――これが外部プーラーとプリペアの相性問題、返却時リセットの必要性の根拠です。パイプライン化は結果依存のない操作で往復を畳む手法で、依存がある処理には使えない点まで対比できれば十分です。
まとめ
DB の性能はクエリ計算量だけでなくワイヤプロトコルの往復回数に強く支配されます。プロトコルは Parse / Bind / Execute / Sync に分かれ、プリペアドステートメントは Parse の成果をセッションに保持して再実行コストを下げます。プリペア・カーソル・トランザクション・一時テーブル・設定はいずれもセッション状態であり、接続を共有・移し替えると失われるため、プーリングでは返却時リセットが要り、外部プーラーとプリペアは相性問題を起こします。カーソルはフェッチサイズで往復とメモリのバランスを取り、パイプライン化は結果依存のない操作の往復を実質1回に畳みます。回数を減らす設計こそが、ネットワーク越しの DB を速くする本質です。分離レベルなど状態の意味論は別稿(→ トランザクション分離レベル)で深掘りできます。
データベース Article
コネクションとセッション状態・プロトコルパイプラインを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ワイヤプロトコル
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
プリペアドステートメントは解析済みプランをサーバーのセッションに保持し、以後はバインド値だけ送って再実行する。プランやカーソルはセッション状態に属し、接続を共有・移し替えると壊れる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ワイヤプロトコル / プリペアドステートメント」に近いか確認する。
- 強みである「DB のワイヤプロトコルはパース・バインド・実行・フェッチに分かれ、各段が原則1往復のネットワークコストを伴うため、回数を減らす設計が支配的に効く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。