10月19日、Jordan Goodman氏が「SQL Anti-Patterns You Should Avoid」と題したブログ記事を公開して、海外で注目を集めている。この記事では、日々の開発でよく見かけるSQLのアンチパターン(やってはいけない書き方)と、それを避けるための考え方について詳しく紹介されている。以下に、その内容を紹介する。
概要
記事のテーマは、「SQLは最初は簡単そうに見えるが、チームやシステムが大きくなるほど、設計上の小さな妥協が後で大きなトラブルを生む」というものだ。
データ処理のパフォーマンスが下がったり、同じデータから異なる結果が出たりする背景には、こうした“アンチパターン”が潜んでいることが多い。筆者は、「SQLもプログラミングと同じように、レビューやバージョン管理を行い、読みやすく保つことが大事だ」と強調している。
よくあるSQLアンチパターン
記事では、特に次の6つのアンチパターンが紹介されている。
- 巨大な
CASE WHEN
を1つのビューの中に閉じ込めてしまう - 検索条件の列に関数を使ってインデックスを無効にしてしまう
SELECT *
で全列をそのまま取得するSELECT DISTINCT
で重複データをごまかす- ビューを何重にも重ねてしまう
- サブクエリを何層にもネストしてしまう
ここからは、それぞれの問題点と改善方法を見ていこう。
1) 巨大な CASE WHEN
をビューに閉じ込める
たとえば倉庫システムで、商品の在庫状態を表す「ステータスコード」が数百種類あるとする。
開発者は「コード1=在庫切れ」「コード2=入荷待ち」など、英語や数字のコードを人が読める説明に変換する必要がある。この変換を急ぎのダッシュボード向けに、ビュー(仮想テーブル)の中でCASE WHEN
文を使って直接書いてしまうことがある。
しかし、こうしたロジックを1つのビューの中だけに書くと、他の開発者が再利用できず、別のところで同じロジックをコピペするようになる。結果的に、どの定義が正しいのか分からなくなり、保守が困難になる。
改善策:変換の定義は別の「ディメンションテーブル」(変換ルールをまとめた表)として独立させるのがよい。
全員が同じ表を参照すれば、どこで使っても結果が一致するようになる。
2) インデックスを無効にする検索条件
SQL Serverなどでは、WHERE UPPER(name) = 'ABC'
のように列に関数を使って検索すると、データベースがインデックスを使えず、全件を走査してしまう。これは性能を大きく落とす原因になる。
改善策:
比較する値のほうを小文字に揃える、またはETL(Extract, Transform, Load)処理の段階で、あらかじめデータを正規化しておく方法がある。
どうしても大文字小文字を区別せずに検索したい場合は、あらかじめ大文字変換した列を追加してインデックスを張っておく方法も有効だ。
注釈:ETL(Extract, Transform, Load)
データを「抽出(Extract)→変換(Transform)→格納(Load)」する一連の処理。データ基盤の中で、形式や値を整えてから保存する際に使われる。
-- 悪い例(インデックスが効かない)
SELECT * FROM users WHERE UPPER(name) = 'ABC';
-- よい例1:比較する値を小文字にそろえる
SELECT * FROM users WHERE name = 'abc';
-- よい例2:大文字化した列を別に作ってインデックスを張る
-- 例: upper_name 列にインデックスを作成
3) ビューでの SELECT *
乱用
ビューを作るときに「とりあえず全部の列を取りたい」と思って SELECT *
を書くのは簡単だが、これは長期的に危険である。
もし元のテーブルのスキーマ(列構成)が変わると、そのビューが壊れたり、不要な列まで引き込んで処理が遅くなったりする。
改善策:必要な列を明示的に指定すること。
これにより、スキーマ変更の影響を最小限に抑えられる。
-- 悪い例
CREATE VIEW v_orders AS
SELECT * FROM orders;
-- よい例
CREATE VIEW v_orders AS
SELECT order_id, customer_id, created_at, total_amount
FROM orders;
4) SELECT DISTINCT
で重複をごまかす
JOIN
の条件が正しくないと、同じ行が重複して結果に出てしまう。
これを安易に SELECT DISTINCT
で消してしまうと、見た目は正しくても、本当の原因を隠してしまうことになる。後で別の人が同じデータを使って集計すると、数値が合わないといったトラブルが起きやすい。
改善策:なぜ重複しているのかを分析し、正しい結合条件を設定すること。
テーブル間の関係を見直し、一対一の対応が保証されるように設計する。
-- 悪い例(原因を放置)
SELECT DISTINCT o.order_id, o.customer_id
FROM orders o
JOIN order_items i ON o.order_id = i.order_id;
-- よい例(事前に集計して関係を整理)
WITH items_per_order AS (
SELECT order_id, COUNT(*) AS item_cnt
FROM order_items
GROUP BY order_id
)
SELECT o.order_id, o.customer_id, i.item_cnt
FROM orders o
LEFT JOIN items_per_order i ON o.order_id = i.order_id;
5) ビューを重ねすぎる
チーム開発では「既存のビューを再利用しよう」と考えて、その上にさらに新しいビューを重ねることがよくある。
最初は整理されているように見えるが、やがて依存関係が増えすぎて、どこで何をしているのかわからなくなる。
データベースは毎回すべてのビューを展開して実行するため、パフォーマンスも落ちる。
改善策:定期的に複雑なビューを整理し、基礎となるテーブルを作り直すこと。
特に処理の重い部分はマテリアライズして(=実際のテーブルとして保存して)再利用するのが効果的だ。
注釈:マテリアライズ(materialize)
クエリ結果を一時的または恒久的に実際のテーブルとして保存すること。
頻繁に使う重い処理を事前に保存しておくことで、読み込みを高速化できる。
6) サブクエリを深くネストしすぎる
複雑な条件を扱うときにサブクエリ(入れ子のクエリ)を使うのは便利だが、3層・4層と重ねていくと読みづらく、デバッグも難しくなる。
筆者は5000行を超えるサブクエリを見たことがあるという。こうなると、もはや誰も全体像を把握できない。
改善策:WITH
句を使って、段階ごとにクエリを分ける。
この方法はCTE(Common Table Expression)と呼ばれ、クエリを読みやすく保つのに最適だ。
注釈:CTE(共通テーブル式)
WITH
句を使い、一時的な名前付きビューを定義してからSELECT文を実行する構文。
サブクエリを整理して可読性を上げるのに役立つ。
-- よい例:CTEを使った分割
WITH base AS (
SELECT ...
FROM ...
WHERE ...
),
deduped AS (
SELECT ...
FROM base
-- 重複除去などの処理
),
final AS (
SELECT ...
FROM deduped
-- 集計やフィルタ処理
)
SELECT * FROM final;
まとめ
これらのアンチパターンは、どれも「急ぎの対応」や「一時しのぎ」から生まれることが多い。
しかし、その場しのぎの書き方が積み重なると、後でシステム全体のパフォーマンスや信頼性に悪影響を与える。
SQLを書くときは、読みやすさ・再利用性・一貫性を意識し、チーム全体で共通の設計方針を持つことが大切だ。
詳細はSQL Anti-Patterns You Should Avoidを参照していただきたい。