1. 長大トランザクション

トランザクションが長くなるほど、ロックの保持時間が延びて他のクエリをブロックする。 特に トランザクション内に外部 API 呼び出しやファイル I/O を含める のは最悪のパターンで、通信遅延がそのままロック時間になる。

❌ NG
Python + SQL — トランザクション内の外部処理
with db.begin():
    db.execute("UPDATE accounts SET balance = balance - 1000 WHERE id = 1")
    # ← ここで外部APIを呼ぶ(数百ms〜数秒かかる)
    result = payment_api.charge(amount=1000)
    db.execute("UPDATE accounts SET balance = balance + 1000 WHERE id = 2")
# APIが遅い間ずっとロックが保持される
✅ OK
Python + SQL — DB処理だけをトランザクションに閉じ込める
# 1. 外部処理を先に行う
result = payment_api.charge(amount=1000)

# 2. DBへの書き込みだけをトランザクションで囲む(短時間で完了)
if result.success:
    with db.begin():
        db.execute("UPDATE accounts SET balance = balance - 1000 WHERE id = 1")
        db.execute("UPDATE accounts SET balance = balance + 1000 WHERE id = 2")

2. ロック順序の不統一(デッドロック)

複数のテーブルや行をロックする順序がトランザクション間でバラバラだと、互いに相手のロック解放を待つ「デッドロック」が発生する。 デッドロックは DB がロールバックで自動解決するが、アプリケーションにエラーが返り、リトライ処理が必要になる。

❌ NG
SQL — ロック順序がバラバラ
-- トランザクション A: accounts → orders の順でロック
BEGIN;
  SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
  SELECT * FROM orders   WHERE id = 101 FOR UPDATE;
COMMIT;

-- トランザクション B: orders → accounts の逆順でロック
BEGIN;
  SELECT * FROM orders   WHERE id = 101 FOR UPDATE;
  SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- A がロック中で待機 → デッドロック
COMMIT;
✅ OK
SQL — ロック順序を統一する
-- A・B ともに同じ順序(テーブル名アルファベット順など)でロックする
-- 両方: accounts → orders の順
BEGIN;
  SELECT * FROM accounts WHERE id = 1   FOR UPDATE;
  SELECT * FROM orders   WHERE id = 101 FOR UPDATE;
COMMIT;

💡 ロック順序のルール例

チームで「テーブル名アルファベット順に取得する」「主キーの昇順でロックする」などのルールを決めてドキュメント化する。ORM を使う場合もソート順を意識する。

3. SELECT FOR UPDATE の過剰使用

SELECT FOR UPDATE は行ロックを取得する。読み取り結果をそのまま更新に使う場合は必要だが、更新しないのに FOR UPDATE を使うと不必要な行ロックで並列処理が大幅に低下する。

操作の意図適切な選択
値を確認して直後に UPDATE するSELECT ... FOR UPDATE
参照のみ(更新しない)通常の SELECT
他者の更新を読み飛ばしたくない(スナップショット読み取り)分離レベルの調整(SERIALIZABLE など)

4. GROUP BY と HAVING の誤用

HAVING は集計後のフィルタリングに使う句だ。集計前に絞り込める条件まで HAVING に書くと、全行を集計してから捨てることになり非効率になる。

❌ NG
SQL — 集計前に絞れる条件を HAVING に書く
-- user_id > 1000 は集計「前」に絞れるのに HAVING で書いている
-- 全 user_id を集計してから捨てる
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
HAVING user_id > 1000 AND COUNT(*) >= 3;
✅ OK
SQL — WHERE で先に絞ってから集計する
-- user_id > 1000 は WHERE に移す(集計対象を最初から絞る)
-- COUNT(*) >= 3 は集計後の条件なので HAVING に残す
SELECT user_id, COUNT(*)
FROM orders
WHERE user_id > 1000
GROUP BY user_id
HAVING COUNT(*) >= 3;

5. COUNT(*) と COUNT(col) の違い

使い分けを間違えると、結果が変わりバグになる。

記述意味NULL の扱い
COUNT(*)全行数(NULL を含む)カウントに含める
COUNT(col)col が NULL でない行数カウントから除外
COUNT(DISTINCT col)col の一意な値の数除外したうえで重複も除く
SQL — COUNT の違いを示す例
-- orders テーブル: 10行あるが coupon_code が NULL の行が3行
SELECT
  COUNT(*)              AS total_orders,      -- 10
  COUNT(coupon_code)    AS orders_with_coupon, -- 7
  COUNT(DISTINCT coupon_code) AS unique_coupons -- クーポンの種類数
FROM orders;

6. IN リストへの大量値の詰め込み

IN (v1, v2, ..., v5000) のように大量のリテラルを渡すと、プランナーが最適な実行計画を選べなくなる場合がある。また SQL 文字列の長さ上限(max_allowed_packet など)に引っかかることもある。

✅ OK
SQL — 一時テーブルまたはサブクエリに置き換える
-- 一時テーブルに入れて JOIN する(プランナーが最適化しやすい)
CREATE TEMPORARY TABLE target_ids (id INT PRIMARY KEY);
INSERT INTO target_ids VALUES (1),(2),...;  -- バルクインサート

SELECT p.* FROM products p
JOIN target_ids t ON t.id = p.id;

-- またはアプリケーション側でチャンク分割する (Python 例)
chunk_size = 500
for i in range(0, len(ids), chunk_size):
    chunk = ids[i:i+chunk_size]
    results += db.execute("SELECT * FROM products WHERE id IN (%s)"
                          % ",".join("?" * len(chunk)), chunk).fetchall()

7. 実行計画を確認しない

パフォーマンス問題の多くは「実行計画を読まずに書いたクエリ」から始まる。EXPLAIN ANALYZE は新しいクエリを書くたびに確認する習慣をつける。

SQL — EXPLAIN ANALYZE の使い方
-- PostgreSQL
EXPLAIN ANALYZE
SELECT * FROM orders WHERE created_at > '2024-01-01';

-- 見るべきキーワード
-- Seq Scan        → フルスキャン(インデックスが使われていない)
-- Index Scan      → インデックスが使われている ✅
-- Index Only Scan → カバリングインデックス(最速)✅
-- Sort (filesort) → ORDER BY がインデックスを使えていない
-- Hash Join / Nested Loop → 結合の種類

-- MySQL
EXPLAIN FORMAT=JSON
SELECT * FROM orders WHERE created_at > '2024-01-01';

SQL アンチパターン 総まとめ

カテゴリ主なアンチパターン
インデックス無効化関数適用・型変換・前方ワイルドカード・NOT IN・OR
不要な読み込みSELECT * ・大 OFFSET・DISTINCT 乱用
結合・サブクエリN+1・相関サブクエリ・デカルト積
ロック競合長大トランザクション・デッドロック・過剰 FOR UPDATE
集計・ソートHAVING 誤用・COUNT(col) と COUNT(*) の混同
その他大 IN リスト・EXPLAIN 未確認

次のページでは…

PART 04 からはコーディングアンチパターン編へ。ゴッドクラス・散弾銃手術・マジックナンバー・深い継承など、設計と命名に関するアンチパターンを解説する。