プリペアドステートメント
要約 Apache Doris は、MySQL ワイヤープロトコルを介したサーバー側プリペアドステートメントをサポートします。JDBC URL に
useServerPrepStmts=trueを設定すると、FE はセッションごとにステートメントの解析とプランニングを一度だけ行い、そのプランをEXECUTEのたびに再利用します。高 QPS の一点検索では、ショートサーキット経路によってスループットが約 4 倍になります。バッチ INSERT や同じ形式のクエリを繰り返す処理でも、行ごとの解析とプランニングのコストを回避できます。

Apache Doris でプリペアドステートメントを使う理由
Apache Doris のプリペアドステートメントは、解析済みのプランをセッションごとにキャッシュして EXECUTE のたびに再利用します。これにより、高 QPS のワークロードで支配的な FE 側の解析、意味解析、最適化のコストを取り除きます。多くの分析エンジンでは、クエリごとに解析とプランニングのコストが発生します。日次レポートのジョブでは、このコストは目立ちません。しかし、サービスが同じクエリを毎秒数千回実行するようになると、無視できなくなります。ユーザー向けダッシュボードはクリックのたびに主キーで行を読み取ります。特徴量ストアは予測のたびにエンティティ ID からベクトルを読み取ります。大量データのローダーは同じ INSERT INTO t VALUES (?, ?, ?) を繰り返し実行します。
この三つのケースでは、SQL テキストは同じで、変わるのはパラメータだけです。呼び出しのたびにステートメントを再解析してプランを作り直すと、FE の CPU を無駄に消費します。QPS が数百程度になると、BE が実質的な処理を行う前に FE が飽和し、BE ノードを増やしても効果がありません。このような場合に PreparedStatement を使用します。
- 高 QPS では、ストレージエンジンではなく Frontend のプランナーがボトルネックになります。
- 同じクエリを繰り返すたびに、解析、意味解析、最適化、シリアライズのコストが発生します。
- 「一点検索を低コストにしたい」という目的のために別の KV ストアを導入するのは、大がかりな対策です。
Apache Doris のプリペアドステートメントとは
Apache Doris のプリペアドステートメントは、MySQL ワイヤープロトコルの COM_STMT_PREPARE / COM_STMT_EXECUTE の組(削除には COM_STMT_CLOSE を使用)をサーバー側で実装したものです。FE が処理し、セッションごとのプランキャッシュを使用します。クライアントが useServerPrepStmts=true を指定して接続すると、JDBC ドライバーは SQL テキストごとに COM_STMT_PREPARE を一度発行します。FE は SQL を解析し、プランを作成して、ステートメント ID をキーとするセッションごとのキャッシュに保存します。以降の EXECUTE では、バイナリ形式のパラメータ値だけを送信します。FE は値をキャッシュ済みのプランにバインドして実行します。
主な用語
useServerPrepStmts=true:MySQL Connector/J ドライバーをクライアント側の文字列補間から、バイナリプロトコルによるCOM_STMT_PREPARE/COM_STMT_EXECUTEのやり取りに切り替える JDBC URL のフラグです。cachePrepStmts=true:プリペアドステートメントのハンドルをクライアント側でキャッシュする、別の JDBC フラグです。これがないと、呼び出しのたびにCOM_STMT_PREPAREが再発行され、高速化の効果が失われます。- セッションごとのステートメントキャッシュ:FE 上のメモリ内マップ(
ConnectContext.preparedStatementContextMap)で、解析済みのPrepareCommand、StatementContext、および任意のShortCircuitQueryContextを保持します。上限はmax_prepared_stmt_count(デフォルトは 100,000)で決まります。 - プレースホルダー(
?):実行時に値がバインドされる位置パラメータです。一つのステートメントには最大 65,536 個指定できます。 ShortCircuitQueryContext:クエリがショートサーキットによる一点検索の対象となる場合に、プリペアドプランとともに保持される追加のキャッシュです。シリアライズ済みのディスクリプターテーブル、出力式、および BE が自身の事前構築済み構造を検索するために使用する UUID を保持します。
Apache Doris のプリペアドステートメントの仕組み
Apache Doris のプリペアドステートメントは、FE で SQL を準備し、条件を満たす場合にショートサーキットプランをキャッシュし、バイナリ形式のパラメータ値で実行し、必要に応じて監査ログを記録し、最後に閉じてキャッシュから削除する、という五段階で動作します。
- 準備する。 クライアントは
COM_STMT_PREPARE "SELECT ... WHERE k1 = ?"を送信します。FE は SQL を解析し、Nereids による意味解析を実行して、接続のマップにPreparedStatementContextを保存し、ステートメント ID とパラメータ数を返します。 - ショートサーキット経路をキャッシュする(条件を満たす場合)。 クエリの対象が一つの Unique Key テーブルで、キー全体に等価述語が指定されている場合、書き換えルール
LogicalResultSinkToShortCircuitPointQueryがフラグを切り替えます。FE はディスクリプターテーブルと出力式をシリアライズして、UUID をキーとするShortCircuitQueryContextに保存します。 - 実行する。 各
COM_STMT_EXECUTEには、ステートメント ID、NULL ビットマップ、型コード(初回)、パラメータ値のバイト列が含まれます。FE はこれらをLiteral値にデコードし、プレースホルダーに設定して、キャッシュ済みのプランを実行します。再解析、意味解析のやり直し、再最適化は行いません。 - 監査ログを記録する(任意)。
enable_prepared_stmt_audit_log=trueを設定すると、デバッグ用にfe.audit.logでパラメータ値を埋め込んだステートメントを再構成します。高 QPS のトラフィックではログが大量に出力されるため、デフォルトでは無効です。 - 閉じる。
COM_STMT_CLOSEはセッションキャッシュからエントリを削除します。接続が閉じられると、マップ全体が破棄されます。
クイックスタート
CREATE TABLE tbl_point_query (
k1 INT,
v1 VARCHAR(64)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH(k1) BUCKETS 1
PROPERTIES (
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"store_row_column" = "true"
);
String url = "jdbc:mysql://fe-host:9030/db"
+ "?useServerPrepStmts=true&cachePrepStmts=true"
+ "&prepStmtCacheSize=500&prepStmtCacheSqlLimit=1024";
try (Connection c = DriverManager.getConnection(url, "root", "");
PreparedStatement ps = c.prepareStatement(
"SELECT * FROM tbl_point_query WHERE k1 = ?")) {
ps.setInt(1, 42);
try (ResultSet rs = ps.executeQuery()) { /* ... */ }
}
期待される結果
fe.audit.log では、二回目以降の呼び出しに次のように表示されます。
Stmt=EXECUTE(-2147481418)
これは生の SQL の代わりに表示され、キャッシュ済みのプランが再利用されていることを示します。さらに EXPLAIN でスキャンノードに SHORT-CIRCUIT が表示されることを確認すると、両方の経路が有効であると判断できます。
Apache Doris のプリペアドステートメントを使う場面
Apache Doris のプリペアドステートメントは、Unique Key テーブルに対する高 QPS の一点検索、繰り返し実行するバッチ INSERT、および同じ形式のステートメントを毎秒数千回実行するクライアント側のワークロードに適しています。一度きりのアドホッククエリ、サーバー側の機能を適切にサポートしないドライバー、またはショートサーキット経路で非決定的関数を使うステートメントには適していません。
適したケース
- 解析コストが支配的となる、Unique Key テーブルに対する高 QPS の一点検索。
- Group Commit を使ったバッチ INSERT。ローダーが同じ
INSERT INTO t VALUES (?, ?, ?)を毎秒数千回実行する場合に適しています。 - 特徴量の検索、ステータスの確認、許可リストの照合、オンラインサービスでの読み取りなど、同じクライアントが一定以上の頻度で繰り返すクエリ。
- Apache Doris と並行して別の KV ストアを運用したくないワークロード。
適さないケース
- 一度きりのアドホッククエリ。
PREPAREのやり取りのコストが、節約できる解析コストを上回ります。 - サーバー側プリペアドステートメントを適切にサポートしないドライバー。たとえば、Rust の
sqlxドライバーでは、Apache Doris の実装に関する問題が報告されています。バイナリプロトコルに適切に対応する MySQL Connector を使用してください。 useServerPrepStmts=trueを設定し、cachePrepStmts=trueを指定していない接続。呼び出しのたびにCOM_STMT_PREPAREが再発行され、デフォルトのクライアント側モードより遅くなります。- 非決定的関数(
NOW()、RAND())を使うステートメント。BE はキャッシュ済みのショートサーキットコンテキストの再利用を拒否するため、節約できるのは解析コストだけです。 - ショートサーキットによる高速化を期待している範囲クエリや集約クエリ。PreparedStatement で解析コストは節約できますが、BE は通常の分散プランを実行します。このようなワークロードでは、代わりにデータプルーニングを活用してください。
性能と検証
Doris は、FE の CPU がボトルネックとなる一点検索のワークロードで、PreparedStatement によってスループットが 4 倍以上向上すると報告しています。これは高並行性の一点検索の機能紹介でショートサーキットの観点から示される 4 倍と同じ値であり、二つを掛け合わせられるわけではありません。高並行性に向けた構成全体(Unique Key + Merge-on-Write + 行ストア + 行キャッシュ + ショートサーキット + PreparedStatement)では、16 コア / 64GB のマシンで YCSB を実行し、ノードあたり 30,000+ QPSに達し、最適化前のベースラインと比べて平均レイテンシーが 96% 減少しました。PreparedStatement 単体の寄与をほかの構成要素から切り分けるのは困難です。FE が性能の上限を決めている場合、4 倍をプランニング上の最低目安として扱ってください。
次の二点を確認すると、この経路が有効かどうかを判断できます。
- 繰り返しの呼び出しについて、
fe.audit.logに生の SQL ではなくStmt=EXECUTE(stmtId)が表示される。 - 一点検索について、
EXPLAINのスキャンノードにSHORT-CIRCUITが表示される。
FE の CPU が引き続き性能の上限となる場合は、Observer を増やし、JDBC のロードバランシングを使用します:jdbc:mysql:loadbalance://h1,h2,h3/db?useServerPrepStmts=true&cachePrepStmts=true。
関連資料
- 高並行性の一点検索(詳細ガイド)
- 高並行性の一点検索の機能紹介
- 高スループットの INSERT のための Group Commit
- 一点検索以外のクエリのためのデータプルーニング
- Unique Key:
store_row_columnと組み合わせることで、KV 形式の一点検索に適したテーブルモデルです。