静的SQL(Static SQL) vs 動的SQL(Dynamic SQL)
1. 概要
A. 定義
静的SQL(Static SQL) はプログラム作成(コンパイル)時点でSQL文の構造が確定し、あらかじめパース・最適化される方式であり、動的SQL(Dynamic SQL) は実行(ランタイム)時点でプログラムが文字列でSQLを組み立て、その都度実行する方式である。
静的SQLは伝統的に埋め込みSQL(Embedded SQL)環境で、ソースコード内にSQLを直接書いておきプリコンパイラがこれをあらかじめ解析・バインドする形態で実装された。一方、動的SQLはアプリケーションが実行中に条件に応じてSQL文字列を構成しデータベースへ渡す方式で、今日の検索画面・レポート・管理ツールのように照会条件が流動的な機能の大半がここに該当する。
二つの方式を分ける実質的な判断基準は「実行するSQLの構造を開発時点で分かるか」である。照会するテーブル・カラム・条件があらかじめ定まっていれば静的に確定できるが、ユーザーの選択や実行時点の状況に応じて文の構造自体が変わらねばならないなら動的に行かざるを得ない。この基準を明確に認識することが、以後の性能・セキュリティ設計の出発点となる。
B. 区分する理由と背景
二つを分ける根本的な軸は「SQLがいつ確定するか」 であり、この時点の差が性能・柔軟性・セキュリティという三つの相反する結果を生む。SQLがあらかじめ確定すればDBが実行計画(Execution Plan)を一度作って再利用でき速いが、照会条件が状況に応じて変わる画面には対応しにくい。逆にランタイムで文を作れば、どんな条件の組み合わせにも対応できるが、毎回パースせねばならず遅く、外部入力がSQL文に混じり込んでSQL Injectionのリスクが生じる。
この区分が実務で重要な理由は、一つのシステム内でも機能の性格に応じて二つの方式を意図的に分けて使わねばならないからである。夜間バッチや取引処理のように同じ文を毎秒数千回反復実行する所では実行計画の再利用から来る性能利得が決定的であり、ユーザーが望む条件だけ選んで検索する画面では柔軟性が優先される。したがってどちらを使うかは正誤の問題ではなく、性能・柔軟性・セキュリティというトレードオフを状況に合わせて秤にかける設計判断である。この判断を誤ると、反復クエリが不必要に遅くなったり、逆に柔軟であるべき検索が硬直したり、セキュリティ脆弱性が開く結果につながる。
歴史的に見れば静的SQL(埋め込みSQL)はCOBOL・Cベースの大型基幹系システムで性能と予測可能性のために広く使われ、Webアプリケーションが普及しユーザー相互作用が多様になるにつれ動的SQLの比重が大きくなった。近年は後述する永続化フレームワークが「動的に組み立てるが値はバインドする」折衷案を標準化しつつ、二つの方式の境界は開発者が毎回意識的に選択する問題というより、フレームワークの規約を正しく守る問題へ移ってきた。
2. 処理時点と内部動作の構造
以下の一つ目の概念図は二つの方式がSQLを処理する全体の流れを対比して示し、二つ目の詳細図はDBオプティマイザがSQLを受け実行するまでの内部段階と、実行計画キャッシュが介入する地点を示す。
flowchart LR
subgraph Static["静的SQL"]
S1["コンパイル時にSQL確定"] --> S2["事前パース・最適化"] --> S3["実行計画を保存"] --> S4["実行"]
end
subgraph Dynamic["動的SQL"]
D1["ランタイムでSQL生成"] --> D2["毎実行時にパース・最適化"] --> D3["実行"]
end
flowchart TB
Q["SQL受信"] --> C{"実行計画キャッシュにあるか?"}
C -->|"Yes(ソフトパース)"| P["計画を再利用"]
C -->|"No(ハードパース)"| SN["構文/意味解析"]
SN --> OPT["オプティマイザ:最適計画探索"]
OPT --> CACHE["計画キャッシュに保存"]
CACHE --> P
P --> EXE["実行および結果返却"]
静的SQLはコンパイル段階で文が固定されるためパース・最適化・実行計画策定をきっかり一度行い、その計画を保存しておいて以後の実行で再利用する。この事前処理は、プリコンパイラがソースコード内のSQLをあらかじめ解析し、必要な権限・オブジェクトの存在有無を確認し、アクセス計画をデータベースにバインドしておく過程からなる。その結果、実行時点では既に準備された計画をそのまま使うためオーバーヘッドが最小化される。
一方、動的SQLは文が実行直前になって確定するため、原則として毎実行ごとにパース・最適化の過程を再び経る。 アプリケーションが条件に応じて文字列を組み立てて渡すと、DBはその文が初めて見るものか確認し、初めてなら全体の解析・最適化を行う。この「1回の事前処理」と「毎回の再処理」の差がすなわち二つの方式の性能格差の根源であり、後で見るバインド変数はまさにこの再処理をソフトパースへ下げて格差を埋める仕掛けである。
二つ目の詳細図が示すハードパース(Hard Parsing) とソフトパース(Soft Parsing) の区分が性能差をより精密に説明する。DBはSQLを受けるとまず同一の文の実行計画がキャッシュ(例:OracleのShared Pool、SQL ServerのPlan Cache)にあるか確認する。あれば計画探索を飛ばす安価なソフトパースで終わるが、なければ構文解析からオプティマイザの最適計画探索まで行う高価なハードパースが起きる。静的SQLとバインド変数を使った動的SQLは文のテキストが同一に保たれソフトパースで処理される一方、値を文字列で直接繋げる動的SQLは実行ごとに文が変わり毎回ハードパースを誘発する。
キャッシュで文を識別する基準が文テキストの完全一致である点が核心である。WHERE id = 100 と WHERE id = 101 は人の目には同じクエリだがDBにとっては互いに異なる文なのでそれぞれハードパースされる。一方 WHERE id = ? でバインドすれば値が何であっても文テキストが一つに保たれ、最初の1回のみハードパースし以後はソフトパースで計画を再利用する。これがバインド変数が性能とセキュリティを同時に解決する原理の根幹である。
A. ハードパースが高価な理由
ハードパースは単純な構文検査ではなく、インデックス使用の可否・結合順序・結合方式など数多くの候補実行計画をコストベースで比較し最適を選ぶCPU集約的な作業である。オプティマイザは統計情報(テーブル行数、カラム分布、インデックス選択度など)をもとに各候補計画の予想コストを計算するが、この探索自体が相当な演算を要する。大量同時接続環境でハードパースが急増するとCPUと共有メモリ(ライブラリキャッシュ)の競合が激しくなり、個々のクエリは軽くてもシステム全体のスループットが急落する現象が実際の運用障害としてしばしば現れる。静的SQLやバインド変数はこのコストを最初の1回に限定し、こうしたボトルネックを根本的に回避する。
このため性能診断時には「ライブラリキャッシュミス率」や「ハードパース比率」が核心指標として使われる。ハードパース比率が異常に高ければ、値を文字列で繋げて文が毎回変わるSQLが存在するという強力なシグナルであり、これをバインド変数へ転換するだけでCPU負荷が劇的に下がる事例が多い。
一部のDBMSは文字列リテラル動的SQLが乱発される状況を緩和するため、リテラルを自動でバインド変数のように置換し計画を共有させる機能(例:OracleのCURSOR_SHARING)を提供する。しかしこれは根本的な処方ではなく応急措置に近く、先に述べたデータ分布の問題や副作用を誘発し得るため、そもそもアプリケーションコードでバインド変数を使うのが定石であることを忘れてはならない。
3. 比較
| 区分 | 静的SQL | 動的SQL |
|---|---|---|
| SQL確定時点 | コンパイル(作成)時 | 実行(ランタイム)時 |
| パース/最適化 | 1回事前実行 | 毎実行時に実行(ハードパース誘発) |
| 性能 | 速い(実行計画再利用) | 相対的に遅い |
| 柔軟性 | 低い(固定構造) | 高い(条件に応じて変更) |
| セキュリティ | 安全(構造固定・バインディング) | SQL Injectionリスク |
| エラー発見時点 | コンパイル時(早期発見) | ランタイム(実行後発見) |
| 代表的活用 | 定型反復クエリ(バッチ・取引) | 可変検索・管理ツール |
上の表で特に注目すべきは、性能・柔軟性・セキュリティが互いに噛み合って動くという点である。静的SQLは性能とセキュリティで勝るが柔軟性に欠け、文字列連結の動的SQLは柔軟性だけを得て性能・セキュリティをともに失う。この表を貫く結論は「文字列連結の動的SQLは最悪の選択」であり、柔軟性が必要ならば必ずバインド変数を結合して性能・セキュリティの損失を埋めねばならないということである。
性能差が生じる理由をもう少し掘り下げると、先に説明したハードパースのコストが核心である。静的SQLはこのコストを最初の1回だけ払うが、毎回異なる文字列を作る動的SQLは計画を再利用できず反復的にハードパースを誘発する。例えば毎秒5,000件が実行される取引クエリを文字列連結の動的SQLで組めば毎秒5,000回のハードパースが発生するが、バインド変数を使えば事実上1回パース後に再利用され、CPU使用量が数十倍の差になり得る。これは単なる理論ではなく、実際の運用で文字列連結SQLをバインディングへ転換した後にDB CPUが半分以下に下がる事例がしばしば報告される実測効果である。
柔軟性は正反対である。例えば検索画面でユーザーが名前・期間・地域のうち入力した条件だけをWHERE句に付けねばならないなら、条件が三つだけでも組み合わせは八通り(2³)に達し、条件数が増えるほど幾何級数的に増加するため静的SQLですべてあらかじめ書くのは難しい。この時は入力された条件だけ選んでWHERE句を動的に組み立てる動的SQLが自然である。もう一つよく見落とされる差がエラー発見時点である。静的SQLはコンパイル時にテーブル・カラム名の誤りをあらかじめ捉え安定的な一方、動的SQLは文がランタイムになって完成するため誤字や構造の誤りが実行時点になって現れ、テストの負担が大きい。
A. 具体事例:多重条件検索画面
電子商取引の注文照会画面を考えてみよう。ユーザーは注文番号・顧客名・注文日付範囲・状態(決済完了/配送中/取消)・金額範囲など複数のフィルタのうち望むものだけを選択する。静的SQLでこの要求を満たすには選択可能な条件の組み合わせごとに別途のクエリを準備せねばならないが、フィルタが五つなら理論上数十通りの組み合わせが出て保守が事実上不可能である。動的SQLでは「値が入力されたフィルタに対してのみ AND 句を追加」する方式で一つのロジックがすべての組み合わせを処理する。このように条件が可変的で選択的な画面が動的SQLの典型的な適用先である。ただしこの時も各フィルタ値は必ずバインドせねばならず、条件の有無だけ動的に判断し値自体はパラメータで渡すのが原則である。
B. 具体事例:大量反復取引(静的SQLの強み)
逆に銀行の口座振替トランザクションは常に「出金口座残高確認 → 差引 → 入金口座増額 → 履歴記録」という固定された文の集合を毎秒数千〜数万件反復する。文の構造が完全に固定されているため静的SQL(またはバインド変数の動的SQL)で実行計画を再利用すればハードパースのコストが除かれスループットが極大化される。ここに文字列連結の動的SQLを使うのは性能・セキュリティ両面で明白な誤りであり、実際こうしたコア取引ロジックは例外なくバインドされた定型クエリで作成する。
4. SQL Injection対応(動的SQL)
動的SQLの最大のリスクはSQL Injectionである。例えばログインクエリを "SELECT * FROM users WHERE id='" + 入力 + "'" のように文字列連結で作ると、攻撃者が入力欄に ' OR '1'='1 を入れてWHERE条件を常に真にし認証を回避できる。さらに '; DROP TABLE users; -- のような入力でデータ破壊や情報漏洩まで試み得るほか、UNIONを用いて他テーブルのデータを併せて照会したり(Union-based)、真/偽の応答差を観察してデータを1ビットずつ抽出する(Blind SQL Injection)精巧な攻撃へも発展する。根本原因はユーザー入力(データ)がSQL文法(コード)の一部として解釈される点にあるため、防御の核心は入力をコードではなく純粋なデータとして扱わせることである。これはOWASP Top 10で長らく最上位圏を占めてきた代表的なWeb脆弱性であり、実際の大型データ漏洩事故の相当数がこの経路に由来した。静的SQLが相対的に安全な理由もここにあり、文の構造がコンパイル時に固定され実行中に入力が構造を変えられないためである。結局Injectionは「ランタイムで文字列で文を作る」という動的SQLの特性から派生したリスクであり、その特性をバインディングで統制することが防御の要である。
防御技法は複数の層位に存在するが、効果と根本性で差が大きい。以下の表は代表的な対応策を整理したもので、このうちバインド変数が最も根本的で残りはこれを補完する性格である点に留意せねばならない。特に「危険文字を取り除く」方式は回避技法が絶えず登場するため単独防御では不十分である。
| 対応 | 内容 | 原理 |
|---|---|---|
| バインド変数 | Prepared Statement・パラメータバインディング | 文の構造を先に固定し値だけ後で注入 → 入力がコードとして解釈不可 |
| 入力検証 | ホワイトリスト・型・長さ検証 | 許可された形式のみ通過 |
| 最小権限 | DBアカウント権限の最小化 | 侵害時に被害範囲を縮小 |
| エラー処理 | 詳細エラーメッセージ露出の遮断 | DB構造情報の漏洩防止 |
| ストアドプロシージャ | パラメータ化されたプロシージャの使用 | 構造と入力を分離 |
A. バインド変数(最も根本的な防御)
最も確実な防御はバインド変数(Prepared Statement) である。WHERE id = ? のようにプレースホルダで文の構造を先にコンパイルした後、値だけパラメータで渡せば、入力に何が入ろうと文法ではなくデータとしてのみ処理されInjectionが源から遮断される。これは文の「骨格(コード)」と「値(データ)」を物理的に分離する方式なので、入力検証のように危険文字を取り除く事後的な防御より根本的で漏れのリスクがない。
技術的にバインド変数は、DBに「この文を準備せよ(prepare)」とまず要求し構文解析と計画策定を終えた後、実行時点で値だけを別チャネルで伝える2段階プロトコルで動作する。値がSQLテキストを経ずパラメータで直接伝わるため、その中にどんな特殊文字やSQLキーワードがあっても文法ではなくリテラルデータとしてのみ処理される。これが文字列エスケープ(危険文字置換)より根本的な理由で、エスケープは漏らしたり回避され得るがバインディングは構造的にコード/データの境界を強制するためである。
さらにバインド変数は値だけ異なり文の構造は同一なのでDBが実行計画をキャッシュして再利用でき、動的SQLの性能の弱点まで相当部分緩和する一石二鳥の効果がある。すなわちセキュリティと性能という二つの目標を一つの技法で同時に達成する点で、動的SQLにおいてバインド変数は選択ではなく必須の原則である。
B. 多層防御(Defense in Depth)
バインド変数だけで大半のInjectionを防げるが、テーブル・カラム名のように値ではない識別子を動的に変えねばならない場合(バインディング不可)には、必ずホワイトリスト検証で許可された名前だけを通過させねばならない。例えばソート基準カラムをユーザーに選ばせるなら、入力値をSQLにそのまま入れず {"date":"order_date","amount":"total_amount"} のような許可マップからコードが決めた安全な値のみ使わねばならない。ユーザー入力を識別子に直接使う瞬間、バインディングで防げない脆弱性が開く。
ここにDBアカウントの権限を必要な最小範囲に制限し(例:照会画面アカウントにはSELECTのみ付与)、詳細エラーメッセージの露出を遮断して攻撃者にテーブル・カラム構造情報を与えないなど、複数の防御線を重ねて置く多層防御(Defense in Depth) が実務の標準である。ある一つの防御が破られても他の層が被害を防いだり減らすよう設計するもので、バインド変数(1次)・入力検証(2次)・最小権限(3次)・エラー隠蔽(4次)がともに作動する時に初めて堅固な防御が完成する。さらにWAF(Webファイアウォール)で既知の攻撃パターンを事前遮断し、定期的なセキュリティ点検・模擬ハッキングで残余脆弱性を検証する運用次元の防御まで含めるのが望ましい。
C. ストアドプロシージャと最小権限の結合
ストアドプロシージャ(Stored Procedure)をパラメータ化して使うのも有効な防御である。アプリケーションがSQL本文の代わりにプロシージャ呼び出しとパラメータだけを渡せば、SQLロジックがDB内にカプセル化され入力が文の構造を変える余地が減る。ただしプロシージャ内部で再び文字列を連結し動的実行(EXECUTE IMMEDIATE など)を行えば同一のリスクが再発するため、プロシージャ内でもバインディング規約を守らねばならない。ストアドプロシージャは最小権限の原則ともよく馴染み、アプリケーションアカウントにはテーブル直接アクセス権限の代わりにプロシージャ実行権限のみ付与する設計が可能である。
5. 深化:フレームワークでの処理と実務適用
今日、大半のアプリケーションはSQLを直接文字列で扱うよりMyBatis・JPA(Hibernate) のような永続化フレームワークを経る。これらのフレームワークは内部的にバインド変数を使うため、正しく使えば動的クエリの柔軟性と安全性をともに確保できる。例えばMyBatisは <if>・<where> のような動的タグで条件を組み立てつつ値は #{} プレースホルダ(バインディング)で渡し安全に処理する。すなわち先に見た多重条件検索画面を、入力された条件だけ <if> で包んで柔軟に組み立てつつ値はすべてバインドされInjectionに安全な形態で実装できる。
この方式が静的/動的の長所を結合する点が重要である。文の骨格は実行時点で条件に応じて変わるが(動的SQLの柔軟性)、値は常にバインドされ文テキストの変動が最小化されるため実行計画の再利用可能性が高まり(静的SQLに近い性能)セキュリティも確保される。そのため現代的観点では「静的か動的か」の二分法より、「動的に組み立てるが値は必ずバインドする」という折衷が事実上の標準解法として定着した。
一つ留意すべき点は、条件の有無に応じて文の骨格が複数の形態に分かれると、その分だけ互いに異なる実行計画がキャッシュされるということである。フィルタの組み合わせが非常に多様だと計画キャッシュが膨張したり特定の組み合わせが稀にしか実行されず再利用効果が減り得るため、よく使われる組み合わせ中心にインデックスを設計し計画キャッシュの使用状況を監視する細心さが大規模システムでは求められる。
ただしフレームワークが万能ではない。MyBatisで ${}(文字列置換)を使うと値がそのままSQLに挿入されInjectionに露出するため値バインディングには必ず #{} を使わねばならず、${} はソートカラム名のようにバインディングが不可能な識別子に限りホワイトリスト検証とともに制限的にのみ使わねばならない。JPAでもJPQL・Criteria APIはバインディングを保証するが、ネイティブクエリに文字列を連結すれば同一のリスクが生じる。すなわちフレームワークを使うという事実自体が安全を保証するのではなく、その中で値バインディング規約を守るかが安全を左右する。
またフレームワーク使用時に性能面で留意すべき点がある。JPAの遅延ロード・N+1問題のように、利便性の裏に隠れた非効率が大量データ処理でボトルネックになり得るため、反復・大量処理の経路では生成される実際のSQLと実行計画を確認する習慣が必要である。フレームワークが作るSQLも結局DBでは静的/動的、バインディング/非バインディングの同一の規則に従うため、本主題で扱った原理はフレームワーク環境でもそのまま適用される。
実務性能の観点で一つ注意すべき点は、バインド変数が常に最善ではないというバインド変数の副作用(Bind Peeking) である。データ分布が甚だしく偏ったカラム(例:状態カラムの99%が「正常」で1%だけ「エラー」)にバインド変数を使うと、オプティマイザは最初に渡された値を覗き見て(peek)それに合う計画を作りキャッシュする。以後、分布が全く異なる値が入っても同じ計画を再利用するので、例えば「エラー」用に作られたインデックススキャン計画が大量の「正常」照会にそのまま使われかえって遅くなる逆効果が生じ得る。
こうした例外的状況では値をリテラルで露出して値ごとに計画を別々に作らせたり、オプティマイザの適応型カーソル共有(Adaptive Cursor Sharing)のような機能を活用するなど別途のチューニングが必要である。これは「バインド変数を基本とするが、データ特性に応じて例外を認識せよ」という深化した実務指針につながる。性能・セキュリティ・柔軟性の均衡点はシステムのデータ分布とアクセスパターンに応じて変わるため、一律の規則ではなく測定に基づく判断が求められる。
6. 考慮事項および示唆点
- 動的SQLは必ずバインド変数とともに:文字列連結方式を避けパラメータバインディングを強制すればInjection防御と実行計画キャッシュ再利用を同時に達成する。コードレビュー・静的解析ツールで文字列連結SQLを自動検出する体系を備えるのが望ましい。
- 混合設計(ハイブリッド):性能が重要な定型反復クエリ(取引・バッチ)は静的に、条件が可変な検索・レポートは動的に分けて適用するのが現実的である。機能の性格を基準に設計段階で方式を明確に区分せねばならない。特にコア取引ロジックに文字列連結の動的SQLが混じり込まないようアーキテクチャ原則として釘を刺すのがよい。
- フレームワーク規約の遵守:ORM/永続化フレームワークは正しく使えば柔軟性と安全性をともに与えるが、
${}・ネイティブクエリ文字列連結のような回避路で脆弱性が生じるため、チーム次元のコーディング規約とレビューが必要である。 - 識別子の動的化はホワイトリストで:値ではないテーブル・カラム名を動的に変えねばならない時はバインディングが不可能なので必ず許可リスト検証で処理し、ユーザー入力を識別子に直接反映しない。
- データ分布まで考慮したチューニング:バインド変数を基本原則としつつ、分布が極端に偏ったカラムではBind Peekingの副作用を認識し実行計画を点検するなど、データ特性ベースの性能チューニングを並行する。
- 観測・診断体系の構築:ハードパース比率・ライブラリキャッシュミス率のような指標を常時監視し、文字列連結SQLによる性能低下を早期に発見しバインディング転換で対応する運用プロセスを備える。性能の問題とセキュリティ脆弱性はしばしば同じ原因(文字列連結)に由来するため、一つを直すと両方が改善する場合が多い。
- 展望:ORM・クエリビルダ・型安全クエリツール(例:コンパイル時検証型クエリライブラリ)が発展し、開発者が文字列を直接扱わずとも安全で柔軟なクエリを書けるよう支援する方向へ生態系が移動している。技術士の観点では、こうしたツールを導入しつつその内部動作(バインディングの有無・計画再利用)を理解し規約を強制するガバナンスがともに行かねばならない。
参考資料
- OWASP, "SQL Injection Prevention Cheat Sheet": https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- OWASP Top 10 (Injection): https://owasp.org/www-project-top-ten/
- Oracle Database Concepts, "SQL Processing (Parsing)": https://docs.oracle.com/en/database/oracle/oracle-database/
一言まとめ: 静的SQLはコンパイル時に確定・事前最適化で速く安全、動的SQLはランタイム組み立てで柔軟だが毎回ハードパースして遅くInjectionに脆弱であり、バインド変数(Prepared Statement)を使えばInjectionを源から遮断し実行計画の再利用で性能まで補える。