バインドとは、プリペアドステートメントに値を流し込む仕組み
プリペアドステートメントは、いわば「穴あきの SQL」です。そして バインド とは、その穴を 1 つずつ安全に埋めていく作業のこと。文字列を手で連結するのではなく、ドライバの API 経由で値を渡すのがポイントです。
書き方はいつも同じパターンです。プレースホルダ付きの SQL を用意して、値はそれとは別に渡す。これだけです。
CLIではバインドの動作そのものをデモするのは難しいです(シェルにはアプリ側のコードがついていないため)が、上のSQLはまさにアプリケーションが送信する形そのものです。? がプレースホルダで、Pythonの sqlite3、Nodeの better-sqlite3、Rustの rusqlite といった各ドライバが、別途 bind 呼び出しを通して値を埋め込んでくれます。
イメージとしては、SQLがレシピ、バインドする値が材料です。両者が直接混ざり合うことはありません。
位置指定のプレースホルダ:?
一番シンプルなプレースホルダが ? です。書いた順に、バインドした値が前から順番にあてはめられていきます。
INSERT INTO users (name, email) VALUES (?, ?);
Pythonならこう書きます:
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("ローザ", "rosa@example.com"),
)
1つ目の ? には "ローザ"、2つ目には "rosa@example.com" が入ります。値の数が合わない場合は、ステートメントが実行される前にドライバ側でエラーが投げられます。
?1、?2、?3 のように番号を明示的に振ることもできます。同じ値を複数箇所で使い回したいときに便利です。
SELECT ?1 AS greeting, ?1 AS still_the_same;
?1 を使うと、最初にバインドした値をそのまま再利用できます。番号を付けないと、同じ値を 2 回バインドする羽目になります。
名前付きプレースホルダ :name
ステートメントに穴が 3 つも 4 つも増えてくると、位置指定でのバインドはどれがどれだか分からなくなってきます。そこで活躍するのが名前付きパラメータです。
INSERT INTO users (name, email)
VALUES (:name, :email);
Pythonでの実装例:
cursor.execute(
"INSERT INTO users (name, email) VALUES (:name, :email)",
{"name": "Boris", "email": "boris@example.com"},
)
辞書のキーの順番は気にしなくて大丈夫で、名前さえ合っていれば動きます。SQLite では @name や $name も別のプレフィックスとして使えますが、挙動はどれも同じ。実際には :name が圧倒的によく使われています。
名前付きパラメータの真価が見えてくるのは、5 カラムを更新する UPDATE を書いたときや、WHERE と RETURNING で同じ値を使い回すクエリを書いたときです。
NULL をバインドする
NULL を入れたいときは、使っている言語の null 値をそのままバインディング API に渡すのが正解です。あとはドライバが変換してくれます。
INSERT INTO users (name, email) VALUES (?, ?);
-- バインド: ("Cyrus", None) Python の場合
-- バインド: ["Cyrus", null] Node の場合
SELECT id, name, email FROM users;
None、null、nil ── 言語によって呼び方は違いますが、ドライバはこれをきちんと SQL の NULL に変換してくれます。文字列の "NULL" をバインドしないように注意してください。これは「NULL」という4文字のテキストとして保存されてしまいます。同じく、SQL 文の中に NULL という単語を直接埋め込むのもNGです。それではバインドする意味がなくなってしまいます。
数値、BLOB、日付なども同じルールです。ネイティブな値をそのまま渡して、バインドはドライバに任せましょう。
値を変えながら同じステートメントを使い回す
バインドパラメータは、プリペアドステートメント(prepared statement)と組み合わせるのが定番です。一度 prepare しておけば、あとはバインドと実行を何度でも繰り返せます。パーサーが解析するのは最初の1回だけで、コンパイル済みの実行プランを毎回のバインド値に対して使い回せるわけです。
INSERT INTO users (name, email) VALUES (?, ?);
-- ("Ada", "ada@example.com") をバインド -> 実行
-- ("Boris", "boris@example.com") をバインド -> 実行
-- ("Cyrus", NULL) をバインド -> 実行
SELECT id, name, email FROM users ORDER BY id;
ほとんどのドライバでは、これを executemany(Python)や .run() のループ(Node)でラップして実行します。いずれにしても、節約できるのはパース処理のコストです。1 ステートメントあたりは小さいですが、数千行を INSERT するような場面では確実に効いてきます。
1 つのステートメント内でスタイルを混ぜない
SQLite は仕様上、位置パラメータと名前付きパラメータを同じステートメントで併用できます。が、やめておきましょう。
-- 合法だが落とし穴:
INSERT INTO users (name, email) VALUES (?, :email);
読み手が2つのバインディング方式を同時に頭で追わなければならず、しかも多くのドライバは混在記法をきれいにサポートしていません。1つのステートメントにつきスタイルは1つに統一しましょう。値が1〜2個なら ?、それ以外は :name がおすすめです。
よくある落とし穴:バインドは文字列フォーマットではない
バインドパラメータの本質は、値をSQLパーサに通さないという点にあります。次のPythonの2行を見比べてみてください。
# 間違い — 文字列フォーマット:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# 正しい — パラメータバインディング:
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
1行目は文字列連結で SQL を組み立てています。もし name が "'; DROP TABLE users; --" だったら、データベースは何の疑いもなくその注入された文を解析・実行してしまいます。一方、2行目では SQL と値を別ルートで渡しています。値はあくまで文字列としてバインドされるだけで、中にどんな文字が含まれていようと SQL として解釈されることはありません。どの入門記事でも「必ずバインドしろ」と書かれているのはこのためです。好みの問題ではなく、パーサーから見た構造そのものが変わってくるんです。
SQL インジェクション対策については、次のページでさらに踏み込んで見ていきます。
注意点:識別子はバインドできない
プレースホルダで渡せるのは 値 だけです。文字列、数値、BLOB、NULL ですね。テーブル名やカラム名、SQL のキーワードはバインドできません。
-- これは思い通りには動作しません:
SELECT * FROM ? WHERE id = ?;
-- 最初の ? はテーブル名ではなく、文字列リテラルとしてバインドされます。
本当にテーブル名やカラム名を動的に切り替えたい場合(アプリケーションコードではまれですが)は、許可リストで検証したうえで自分でSQLに連結してください。ユーザー入力をそのまま埋め込むのは絶対にNGです。それ以外はすべてバインドで処理しましょう。
実例で動かしてみる
ここまでの内容をまとめて、users テーブルへの書き込みも読み込みもすべてバインドパラメータ経由で行う小さなサンプルを見ていきます。
実際のアプリケーションでは、INSERT も SELECT もすべてプレースホルダを使って書くのが普通です。CLI にはバインド元になるアプリがないので、リテラル値がバインド結果の代わりに置かれているだけ、と考えてください。
次のステップ: SQL インジェクション対策
バインドパラメータは、あくまで「仕組み」の話です。なぜ これが SQL インジェクション対策になるのか、そしてバインドだけでは防ぎきれない数少ないケースとは何か——次のページでは、そのあたりを掘り下げていきます。
よくある質問
SQLite のバインドパラメータとは何ですか?
プリペアドステートメントに対して、SQL 文とは別に値を渡す仕組みのことです。SQL 側には ? や :name のようなプレースホルダだけ書いておき、実際の値はドライバの bind API 経由で渡します。バインドされた値はあくまで「データ」として扱われ、SQL としてパースされることはありません。
? と :name はどう使い分ければいいですか?
? と :name はどう使い分ければいいですか?? は位置指定のプレースホルダで、書いた順にバインドします。一方 :name(や @name、$name)は名前付きで、位置ではなく名前で値を紐付けます。値が 3 つ以上ある場合は、可読性も入れ替え耐性も高い名前付きパラメータの方がおすすめです。
SQLite で NULL をバインドするにはどうすればいいですか?
使っている言語の null / None / nil をそのまま bind API に渡せば OK です。ドライバが自動で SQL の NULL に変換してくれます。'NULL' という文字列を渡したり、SQL 文に NULL を直接埋め込んだりするのは NG。バインドの目的はあくまで「値を SQL パーサに触らせない」ことです。
1 つの SQL 文で位置指定と名前付きを混在させてもいいですか?
SQLite の仕様としては許されていますが、おすすめしません。? と :name が混ざった文は読みにくく、バインドミスの温床になります。1 つのステートメント内ではどちらかに統一するのが鉄則で、値が 3 つ以上なら名前付きで揃えると安全です。