アプリケーションから使う
データベース接続の仕組み、プレースホルダによる SQL インジェクション対策、ORM が発行する SQL と N+1 問題の正体に迫ります。
- アプリと DB の間にあるものは何か
- ユーザーの入力を SQL に混ぜると何が起きるか
- DB のログに出ないのに遅い、はなぜ起きるか
接続の正体: サーバー上のスレッドと通信
第 0 章の SHOW PROCESSLIST で確認したように、結果の 1 行がデータベースへの「接続(コネクション)」1 本に対応し、サーバー側では接続ごとに 1 本のスレッドが割り当てられます。CONNECTION_ID() はその識別番号です。 クエリを実行するたびに接続番号が変わることがあるのは、この実行環境が複数の接続をプールして使い回しているためです。これをコネクションプールと呼びます。 新規接続の確立には TCP ハンドシェイクや認証などで数ミリ秒〜数十ミリ秒のオーバーヘッドがかかるため、実務の Web アプリケーションではリクエストごとに毎回接続をつなぎ直すのではなく、事前に確立した接続をプールから一時的に借り受けて再利用します。
// コネクションプールのイメージ(擬似コード)
pool = createPool(接続設定, 最大 10 本)
rows = pool.query("SELECT * FROM customers WHERE id = ?", [42])「最大 10 本」は、そのアプリケーションの 1 プロセスが同時に保持できる接続数の上限です。 もし API サーバーが 20 台並列稼働していれば、データベースには最大で 200 本の同時接続が押し寄せることになります。 データベース側にも同時に受け入れ可能な接続上限数(max_connections、デフォルトは 151 など)が設定されており、これを超えると新規接続が拒否されてシステム障害となります。この同時接続の限界については第二部 10 章で詳しく検証します。
SQL インジェクション: ユーザー入力がクエリ構造を書き換える脆弱性
たとえば、顧客名での検索機能を「入力文字列をそのまま連結して SQL を組み立てる」形で実装してしまったケースを考えてみましょう。
// 危険な実装例: 文字列連結 sql = "SELECT * FROM customers WHERE name = '" + input + "'"
悪意あるユーザーが入力欄に ' OR '1'='1 と入力した場合、どのような SQL が構築されてしまうでしょうか。実行して何行返ってくるか確かめてみてください。
結果は全 10 名の顧客情報がすべて返ってきます。 ユーザーが入力したシングルクォート ' によって本来の文字列リテラルが途中で閉じられ、後ろに続いた OR '1'='1' が SQL の条件式の一部として解釈されてしまったためです。'1'='1' は常に真となるため、WHERE 条件が形骸化して全行が出力されます。 もしこれがログイン認証処理であれば、パスワードを知らなくても他人のアカウントでログインできてしまいます。これがSQL インジェクションの原理です。
さらに悪質な例として、入力値に '; DELETE FROM orders; -- などを注入された場合、複数文の実行を許可する設定になっている環境では、第 5 章で学んだ「全件 DELETE」が外部から勝手に実行されてしまいます。
// 安全な実装例: プレースホルダ(バインド機構)
pool.query("SELECT * FROM customers WHERE name = ?", [input])SQL 文の中で可変部分を ? などのプレースホルダにして値を分離して渡す手法(プリペアドステートメント / バインド機構)を採用すると、入力値は純粋な「データ値」としてのみ扱われ、SQL の構文構造そのものが書き換えられる危険性を完全に排除できます。 現代の ORM を使っていれば通常は自動的にプレースホルダが使用されますが、生 SQL を直接記述するメソッド(raw や query)で文字列結合を行うと、同様の脆弱性が生じてしまいます。
ORM が背後で発行している SQL を監視する
現代の多くの Web アプリケーションでは、ORM(Object-Relational Mapping: Prisma, TypeORM, ActiveRecord, Hibernate など)を利用して開発が進められます。 しかし、ORM は「開発者が SQL を手書きする手間を省いてくれる」ツールであって、データベースに対して「SQL を発行しなくて済む」わけではありません。 裏側でどのような SQL が何本発行されているのかを把握していなければ、パフォーマンスのトラブルシューティングは不可能です。
// ORM のコード例(擬似コード) orders = Order.where(status: "paid").includes(:customer).order(ordered_at: :desc).limit(10)
この 1 行のメソッドチェーンの裏側で、データベースにはおおよそ以下のような SQL が発行されています。
どの ORM にも「発行された生 SQL をコンソールに出力する」ロギング設定が必ず備わっています。 開発環境では常にこの SQL ログを有効化しておき、「1 回の API リクエストや画面アクセスで、実際に何本の SQL が発行されているか」を日常的に観察する癖をつけることが極めて重要です。
N+1 問題: スロークエリログに出ないサイレントな性能低下
チケットの課題「顧客一覧画面に、各顧客の注文件数を表示する」を素朴なループ処理で実装したとします。
// N+1 が発生するコード例
customers = query("SELECT * FROM customers") // 1 回(顧客 N 人を取得)
for c in customers:
c.orderCount = query("SELECT COUNT(*) FROM orders WHERE customer_id = ?", [c.id]) // N 回ループ内のクエリ 1 回あたりの所要時間はわずか 0.1 ミリ秒程度で、一瞬で完了します。 そのため、データベース側のスロークエリログには何ひとつ警告が出力されません。 しかし、顧客が 10 人いれば「1 + 10 = 11 回」、顧客が 1 万人に増えれば「1 + 10,000 = 10,001 回」もの通信往復(ネットワークラウンドトリップ)がアプリと DB 間で発生します。 1 回の往復レイテンシがわずか 1ms であっても、1 万回繰り返せばそれだけで 10 秒以上の待機時間が発生してしまいます。 問題なのは個々のクエリの重さではなく「クエリの発行回数」であり、これを N+1 問題 と呼びます。
1 回ずつ通信を往復させる場合と、1 回のクエリに集約した場合の通信フローを図で比較してみましょう。
現代の ORM には関連データを一括取得する仕組み(Prisma の include、Rails の includes、GraphQL の DataLoader など)が用意されており、これらを使うと自動的に WHERE customer_id IN (…) 形式の 1 本のクエリにまとめられます。 N+1 問題に気づくための最も確実な方法は、アプリケーションの SQL ログを監視し、「同じ構造の SELECT クエリがズラズラと大量に連続出力されていないか」をチェックすることです。
第一部のまとめと、第二部(性能編)への架け橋
第一部を通じて、SQL の基本文法、テーブル結合、データ更新、トランザクションによる整合性の保護、インデックスの概念、そしてアプリケーション連携の基礎を習得しました。 数十行〜数百行の小規模データでは、クエリの書き方の良し悪しが実行速度として顕在化することはありませんでした。 しかし第二部では、同一のショップが急成長し、注文明細データが 3,200 万行 に達した本番規模の巨大データベースを相手にしていきます。
- 第 2 章の WHERE / ORDER BY 列 → 第二部 1・4・6 章で、インデックスの成否を分ける設計対象になります。
- 第 3 章の COUNT(*) → 第二部 0 章で、全件集計に数秒を要する現実を目の当たりにします。
- 第 4 章の JOIN → 第二部 7 章で、読み出し順序の誤りによって 60 秒以上固まる障害を体験します。
- 第 7 章のトランザクション → 第二部 9 章で、複数トランザクションの更新衝突による「ロック待ち・デッドロック」に直面します。
- 本章の N+1 と接続数 → 第二部 10 章で、同時アクセス増加によるデータベースの飽和限界を実測します。
アプリケーションとデータベースの間には接続プールが存在し、入力値はプレースホルダで安全に渡し、ORM の発行クエリ数を常に監視して N+1 を防ぎましょう。