- SQLアンチパターンは、クエリやデータパイプラインの保守を難しくし、想定以上に遅い性能を招く
- CASE WHENの乱用、インデックス列への関数適用、SELECT *、DISTINCTの乱用、ネストしたビューとサブクエリ、深い依存構造などが代表例
- 問題の多くは、速度や締め切りの圧力による場当たり的な解決策から生じ、長期的にはデータの信頼性と開発速度の両方を損なう
- 解決策として、明確なJOIN定義、ディメンションテーブルの活用、不要なネストの除去、定期的なビュー整理などが必要
- SQLは単なるスクリプトではなく、チーム単位で管理される本番コードとして扱うべきであり、可読性を考慮した初期設計が手戻りを減らす
紹介
- 今日は、SQLアンチパターンの中でもよく発生し、影響の大きい事例をいくつか中心に説明する
- こうした問題は、データの信頼性低下やクエリ開発速度の低下といった悪循環を引き起こす
- 以下の一覧はすべての事例を網羅するものではなく、さらに深く理解したい場合は Bill Karwinの著書 をおすすめする
過度に複雑なCASE WHEN文
- 大規模システムでは、状態コード(例: 1=在庫切れ)を人が読める形に変換するため、CASE WHEN文がよく使われる
- ダッシュボードやレポートを素早く開発するために、1つのViewにだけそのCASE WHENロジックを追加するのは、長期的にはアンチパターンに当たる
- これは重複ロジックのコピー&ペーストや解釈の不一致を招き、クエリ環境全体を雑然としたものにする
- 解決策は、状態コードを変換するディメンションテーブル(dimension table)または共通ビューを別途作成し、再利用性を確保すること
インデックス列での関数使用
WHERE UPPER(name) = 'ABC' のように、インデックスが張られた列に関数を適用すると、インデックスの効率が失われる
- SQL Serverなどでは、その結果 不要なフルテーブルスキャン(full table scan) が発生する
- 解決策は、関数適用後の列を別途インデックス化するか、入力値を変換してクエリ条件を単純化すること
ビューでのSELECT *使用
- ビュー開発時に SELECT * を使うと便利に見えるが、構造(スキーマ)が変更されるとビューが簡単に壊れることがある
- 不要な列まで含まれて 意図しない依存関係や性能問題 が生じるため、列は明示的に選択すべき
DISTINCT乱用による重複の“解決”
- 誤ったJOINによって重複結果が発生したとき、SELECT DISTINCT で一時的に対処するのは、データ整合性の問題を隠すことになる
- 根本原因はJOIN条件の不完全さや、関係定義(1:1、1:Nなど)の誤りにある
- 正しい解決策は、JOINロジックを補強して関係定義を明確にし、集計やレポートの前に関係の整合性を確保すること
ビューのネスト(Excessive View Layer Stacking)
- 複数チームが既存ビューを再利用しながら新しいビューを積み重ね続けると、依存チェーンが複雑化し、性能が急激に低下する
- デバッグが難しくなり、クエリの拡張が「考古学的発掘」レベルで困難になる
- 定期的に変換ロジックを フラット化(flatten) し、複雑な演算は 明確なベースビューやテーブルとしてマテリアライズ(materialize) する戦略が必要
過度に深いサブクエリ
- 3〜4段階以上に 深くネストしたサブクエリ は、可読性を下げ、デバッグを難しくする
- CTE(Common Table Expression) を活用すると、論理的な段階分けがしやすくなり、クエリの 明確さ(readability) が高まる
結論
- SQL は表面的には単純に見えるが、システムが大きくなるほど複雑性が増大する性質がある
- 多くの アンチパターン は悪意からではなく、「早く結果を出す」ための妥協(速度、締め切り、その場しのぎ) の結果として生まれる
- SQLをコードのように管理(バージョン管理、コードレビュー、明確な設計) すれば、長期的に生産性と信頼性を同時に確保できる
- 初期設計に数分投資して、明確さと一貫性を意識すれば、今後の手戻りや混乱を大きく減らせる
5件のコメント
今は時間がないから、とりあえず目先の火だけ消して後で書き直そうと言っていたものが、積み重なると恐ろしいクエリ地獄になりますよね。私もいくつも作ってきました。あれらを書き直す「後で」なんて永遠に来ないと分かっていながら、そうしてしまうのです。
うっ…
「ほとんどの問題は、速度や締め切りのプレッシャーによる場当たり的な解決策から生まれる」
うう..
Hacker Newsのコメント
sqlite,mysql,postgresqlのいずれでも)SELECT x FROM tクエリでDISTINCTを安全に省略してよいかどうか、どう判断できるのかと尋ねたい。tのスキーマでxにPRIMARYやUNIQUE制約があるのを見たとしても、誰かがすぐにUNIQUE制約を外してしまうかもしれない。すると重複が発生し、なぜそうなったのか悩むことになる。SQLは集合(set)言語ではなくbag言語だ。実行時にrel tと属性xを見つけたら、そのまま返すだけ。重複が出ることもあるし、型が変わることもある。もしSetが欲しいなら、必ずDISTINCTを明示しなければならない。クエリプランナーは実行時にUNIQUEやPRIMARYならdeduplicationをしないようになるhttps://kb.databasedesignbook.com/posts/systematic-design-of-join-queries/
!=やNOT IN (...)を使う場合は、ほとんどが非効率になる(ただし他の条件で結果集合が十分に絞られているなら問題ないこともある)。それに、DBがnullをどう扱うかを理解するのは重要だ。nullと空文字列が同じなのか、null == nullなのかは、DBごとに異なることがあるWHERE col IS NULLのクエリはcolにインデックスがあっても非効率に動く。本当に必要なら、colのnull有無を示すchar(1)やbitカラムを作ってそのフィールドにインデックスを張る方法を勧める!=やNOT IN (...)がほぼ常に非効率だと言っていたけれど、なぜなのか気になる。右辺が定数ならハッシュテーブルのルックアップになって一般には効率的そうだし、もっと効率のよい代替があるのか知りたいhttps://en.wikipedia.org/wiki/Sargable
https://www.brentozar.com/blitzcache/non-sargable-predicates/
「sargable」という単語は「Search ARGument ABLE」のかばん語(portmanteau)だ
インデックス列に関数を使うのは、クエリがsargableでない兆候だ
DISTINCTの乱用の代わりに、JOIN由来のファンアウトの中からテーブル粒度に合わせてde-dupeするクエリとしては のような形が有用だ。一部のDBではQUALIFYという機能があり、クエリがずっときれいになる
sargableの説明
QUALIFY in Redshift
UPPER(name)列へのインデックス追加)は、少なくともMS SQL Serverでは最善ではない。他のDBでの対応は分からないが、もっと良い解決策は、最初からCOLLATEを使って大文字小文字を区別しない計算列を作ることだ (好みに応じて調整)最も重要なものが抜けていますね。