SQL 06 実務の注意(インデックス・トランザクション・SQLi・AI開発)

更新日:

← SQL_05_データ操作と設計(INSERT・UPDATE・DELETE・CREATE) / 入口 → SQL

1. インデックス = 検索を速くする索引

本の索引と同じ

分厚い技術書で「トランザクション」の載っているページを探すとき:

  • 索引なし → 1ページ目から全部めくる(= フルスキャン)
  • 索引あり → 巻末の「た行」を見て p.234 に直接飛ぶ
SELECT * FROM orders WHERE user_id = 1;
  • インデックスなし:100万行あれば100万行すべてを確認。行が増えるほど遅くなる
  • インデックスあり:user_id 順に並んだ索引をたどって該当行に直接たどり着く。100万行でも数回たどるだけ

中身のイメージ(B-tree)

多くのDBは B-tree という木構造で、値を常に並べ替えた状態で持っている。

              [50]
           /        \
      [20, 35]     [70, 90]
      /  |  \       /  |  \
   …1〜19 … 36〜49 …   …91〜
            ↓
       各値 → 実際の行の場所

辞書を真ん中から開いて「前か後ろか」で絞り込むのと同じやり方。

作り方

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);  -- 複合インデックス

速くなる場面

  • WHERE user_id = 1(絞り込み)
  • JOIN … ON o.user_id = u.id(テーブルの結合)
  • ORDER BY created_at(並べ替え。索引はすでに並んでいるので速い)
  • UNIQUE 制約の重複チェック

デメリット

デメリット理由
追加・更新・削除が遅くなるデータが変わるたびに索引も更新するため
容量が増える索引そのものもデータとして保存されるため

→ 全部の列に貼るのは逆効果。WHERE / JOIN ON / ORDER BY でよく使う列に絞って貼る。

覚えておくこと

  • 主キー(id)と UNIQUE を付けた列 には自動で作られる
  • 外部キー列(user_id など)は自動では作られないことが多い → 自分で作るのが定番
  • 数百〜数千行なら索引なしでも気にならないことが多い。目安として 数万〜数十万行 を超えたあたりで差がはっきり出る

「指定しておけば自動で使われる」は半分正解

  • 事前に「この列に索引を作る」と指定する(CREATE INDEX)
  • クエリ側は何も変えなくてよい。WHERE user_id = 1 と普通に書くだけ
  • 索引の中身もDBが自動で最新に保つ(追加・更新のたびに更新される)

ただし 必ず使われるわけではない。使うかどうかは DB の中の オプティマイザ(実行計画を立てる係) がクエリごとに判断する。索引は「使える道具を用意しておく」もので、使う・使わないは DB 任せ。

索引が使われないケース理由
テーブルが小さい全部読んだほうが早いと DB が判断する
条件に当てはまる行が多すぎる(例:status = 'active' が9割)索引をたどるより全部読むほうが速い
列に関数や計算をかけている(LOWER(email) = …)索引は加工前の値で並んでいるため使えない(関数インデックスが必要)
LIKE '%田中'(先頭が %)先頭が決まらないと並び順を利用できない('田中%' は使える)
複合索引の先頭の列を条件に使っていない下記

複合インデックスは「左から順に」使われる

CREATE INDEX idx ON orders(user_id, created_at);
クエリの条件索引が使えるか
WHERE user_id = 1○
WHERE user_id = 1 AND created_at > '2026-01-01'○(最も効く)
WHERE created_at > '2026-01-01' だけ✗(先頭の user_id がないため)

電話帳が「名字 → 名前」の順に並んでいるのと同じ。名字が分かれば探せるが、名前だけでは探せない。

まとめ:索引を用意しておけば、使えそうな場面で DB が自動で選んでくれる。ただし 索引が使われる書き方になっているかは、クエリを書く側が気にする必要がある。

遅い原因を調べる(実行計画)

EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 1;   -- SQLite / D1
EXPLAIN ANALYZE    SELECT * FROM orders WHERE user_id = 1;   -- PostgreSQL

SCAN(全件)ではなく SEARCH … USING INDEX と出ていれば索引が使われている。

2. トランザクション = 全部成功 or 全部なかったこと

送金のように「片方だけ成功すると困る」処理をまとめる。

BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;      -- 確定。途中で問題があれば ROLLBACK; で全部取り消し
ACID意味
Atomicity(原子性)全部 or なし
Consistency(一貫性)制約を破った状態で終わらない
Isolation(独立性)同時に動く他の処理と混ざらない
Durability(永続性)COMMIT したら電源が落ちても消えない
  • 手作業で本番データを触るときも BEGIN; → 実行 → SELECT で確認 → COMMIT; / ROLLBACK;
  • Cloudflare D1 は BEGIN を直接使えず、db.batch([...]) でまとめた文が1トランザクションになる
  • 同時更新の競合(在庫の取り合いなど)は UPDATE … SET stock = stock - 1 WHERE stock > 0 のように 1文で条件付き更新 すると安全

3. SQL インジェクション(SQLi)対策 ― 最重要

ユーザー入力を 文字列連結で SQL に埋め込む と、SQL そのものを書き換えられる。

// ✗ 絶対ダメ
db.query(`SELECT * FROM users WHERE email = '${email}'`);
// email に  ' OR '1'='1  と入力されると → 全ユーザーが返る
// '; DROP TABLE users; -- なら → テーブルが消える
 
// ○ プレースホルダ(パラメータ化クエリ / プリペアドステートメント)
db.query('SELECT * FROM users WHERE email = ?', [email]);           // MySQL / SQLite
db.query('SELECT * FROM users WHERE email = $1', [email]);          // PostgreSQL
env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(email);  // Cloudflare D1
  • 値は 必ず ? / $1 で渡す。自前のエスケープ処理に頼らない
  • ORM(Prisma・Drizzle)は基本安全だが、$queryRawUnsafe や sql.raw() など "raw" / "unsafe" と名のつく関数 に入力を入れると同じ穴があく
  • テーブル名・列名・ORDER BY の向きはプレースホルダにできない → 許可リスト から選ぶ
    const col = ['name', 'created_at'].includes(sort) ? sort : 'created_at';
  • 詳しくは Webセキュリティ対策

4. その他の実務ポイント

  • 最小権限:アプリ用の DB ユーザーには必要な権限だけ(DROP できないなど)
  • 接続情報は秘密情報:接続文字列・パスワードは .env / シークレットへ(APIキー・シークレット管理)
  • N+1 に注意:ループの中でクエリを投げない。JOIN か IN (…) でまとめる
  • 本番 DB を直接いじらない:まずローカル/ステージングで試す。マイグレーション経由で変更する
  • バックアップと復元手順を一度は試す:「取れているつもり」が一番危ない
  • 時刻は UTC で保存、表示するときに日本時間へ変換

5. 生成AI に SQL を書かせるときのコツ

やること理由
DB の種類を伝える(「D1 / SQLite で」「PostgreSQL 16 で」)日付関数・UPSERT・型が製品ごとに違う
テーブル定義(CREATE TABLE か ORM のスキーマ)を渡す列名を推測で書かれるのを防ぐ
「プレースホルダを使って」と明記文字列連結のコードを出されることがある
UPDATE / DELETE / DROP / マイグレーションは必ず自分で読むWHERE 漏れ・CASCADE で関連データ全消えなどを防ぐ
まず SELECT 版を書いてもらい件数を確認影響範囲を見てから実行できる
遅いときは EXPLAIN の結果ごと渡すインデックスの提案が的確になる
本番 DB への書き込み権限をエージェントに渡さない誤操作が取り返せない(Claude Code の権限設定で制限)

AI が書いた SQL のチェックリスト

  • UPDATE / DELETE に WHERE があり、条件が想定どおりか
  • ユーザー入力はすべてプレースホルダ経由か
  • JOIN の ON 条件が正しいか(行数が爆発・重複していないか)
  • LEFT JOIN なのに WHERE で右側を絞って INNER 化していないか
  • SELECT * で不要・秘密の列まで返していないか
  • NULL の扱い(= NULL、NOT IN、SUM が NULL)は大丈夫か
  • 検索条件の列にインデックスがあるか
  • ループの中でクエリを投げていないか(N+1)