データベース

DB設計のアンチパターン一覧|SQLアンチパターン第2版27章と現場の定番失敗

DB設計のアンチパターン一覧|SQLアンチパターン第2版27章と現場の定番失敗

DB設計のアンチパターンを調べると、日本語の解説記事はほぼすべて2013年刊の『SQLアンチパターン』初版を下敷きにしています。その初版は2025年7月11日に第2版へ改訂され、章がまるごと2つ外れ、4つ増えました。この記事では第2版の27章構成を一次ソースの目次で確認したうえで、論理設計で特に事故が多いパターンを実行検証済みのSQLとともに整理します。あわせて、書籍が扱っていないのに日本の現場では毎回もめる削除フラグ・履歴テーブル・中間テーブルの3つも扱います。

まとめ

DB設計のアンチパターンは、『SQLアンチパターン 第2版』(オライリー・ジャパン、2025年7月11日発行、400ページ)の27章が事実上の共通語彙です。論理設計にあたる第I部の8章(ジェイウォーク、ナイーブツリー、IDリクワイアド、キーレスエントリ、EAV、ポリモーフィック関連、マルチカラムアトリビュート、メタデータトリブル)を押さえれば、テーブル定義のレビューで指摘すべき箇所のほとんどは拾えます。

混同されやすいのが継承まわりです。具象テーブル継承とクラステーブル継承はアンチパターンではありません。第2版の目次で6.5.2と6.5.3に置かれているとおり、EAVというアンチパターンに対する解決策の選択肢です。「クラステーブル継承はアンチパターン」と書かれた解説を見かけたら、その記事は原典を参照していないと判断して構いません。

書籍の27章だけでは足りません。削除フラグ、履歴テーブル、中間テーブルの3つは原典に対応する章がなく、それでいて日本語圏の設計レビューで最も議論になる論点です。以下、判断軸から順に、章ごとの見分け方と現場定番の失敗を見ていきます。

アンチパターンの定義と、原典が各章で示す5つの判断軸

アンチパターンとは、広く使われているのに結果として問題を生む設計や実装のことです。単なる「悪い書き方」との違いは、一度は合理的に見える点にあります。カンマ区切りで複数IDを1列に入れる設計は、テーブルが1つで済むぶん最初は確かに速く作れます。問題が出るのは、データが増え、検索条件が複雑になり、担当者が入れ替わったあとです。

第2版はこの構造を章のフォーマットとして固定しています。アンチパターンを扱う2章から25章が「目的」「アンチパターン」「アンチパターンの見つけ方」「アンチパターンを用いてもよい場合」「解決策」の5節で構成されており、たとえば4章IDリクワイアドなら4.1から4.5がそのまま5節に対応します。導入の1章と、外部キーの誤用を箇条で列挙する第V部の26章・27章はこの形を取りません。

実務で効くのは4番目の「用いてもよい場合」です。アンチパターンは禁止事項ではなく、条件付きで正解になります。管理画面のログ表示にしか使わないカンマ区切り列を正規化するのは、多くの場合は工数の無駄です。レビューで指摘するときは「これはアンチパターンだから直す」ではなく、「この列に対する検索と集計が今後必要になるか」を先に確認してください。必要にならないなら、直さない判断が正しい判断です。

SQLアンチパターン第2版で入れ替わった章(2025年7月11日刊)

第2版は初版から12年ぶりの改訂です。原書は Pragmatic Bookshelf の SQL Antipatterns, Volume 1、訳は児島修氏、監訳は和田卓人氏。既存の日本語解説記事はほぼ初版ベースなので、章番号と章名を引用するときはどちらの版かを確認する必要があります。

初版25章から第2版27章への再編と、目次から消えた2章

オライリー・ジャパンが公開している両版の目次を突き合わせると、増減がはっきりします。

項目 初版 第2版
発行日 2013年1月26日 2025年7月11日
ページ数 352 400
章数 25章 27章
ISBN 978-4-87311-589-4 978-4-8144-0074-4

目次から消えたのは、初版24章のマジックビーンズ(魔法の豆)と、初版25章の砂の城の2つです。代わりに、初版では章として立っていなかった「アンチパターンとは何か?」が1章になり、25章スタンダード・オペレーティング・プロシージャ(さびついた開発標準)と、第V部ボーナスの26章・27章(標準SQLとMySQLにおける外部キーの誤った使い方)が加わりました。25から2を引いて4を足すと27章で、公称の章数と一致します。

この2章の行方は対照的です。マジックビーンズは第2版から完全に削除された一方、砂の城はオライリー・ジャパンが日本語版付録として全9ページのPDFで公開しており、著者は奥野幹也氏です。原典の章立てを引用する記事を書くなら、この付録の扱いまで確認しておくと版の取り違えを避けられます。

章末に加わったミニ・アンチパターン15本と、参照時に生じる章番号のずれ

第2版では多くの章の末尾に短い「ミニ・アンチパターン」が付きました。目次上で数えると2.6、3.6、4.6、8.6、11.6、13.6、14.6、15.6、16.6、20.6、21.6、22.6、23.6、24.6、25.6の15本です。BIGINTは十分に大きいか、予約語、NOT IN (NULL)、ハッシュ文字列をVARCHAR型で格納する、といった単体で1章にするほどではないが実際によく踏む論点が拾われています。第V部「ボーナス:外部キーのミニ・アンチパターン」の26章・27章はこの15本とは別枠で、外部キーの誤用を17項目に分けて列挙しています。

参照時に注意が必要なのは章番号です。1章が新設されたぶん、第2版に残った既存23章は1つ後ろにずれました(初版1章から23章が、第2版2章から24章へ移動)。サーティワンフレーバーは初版10章から第2版11章へ、シー・ノー・エビル(臭いものに蓋)は22章から23章へ、ディプロマティック・イミュニティ(外交特権)は23章から24章へ移っています。社内の設計ガイドラインで「SQLアンチパターン10章参照」のように書いている場合、第2版を配ると別の章を指してしまいます。章番号ではなく章名で参照してください。

論理設計のアンチパターン8種と、テーブル定義から見分ける方法

第I部に置かれた8章が、DB設計のアンチパターンの中核です。8つに共通する症状は、同じ事実が複数の場所に持たれる冗長データと、そこから生まれるデータ不整合にあります。実務では次の表の「見分け方」の列をレビュー用チェックリストとして使えます。

章 名称 やってしまうこと テーブル定義からの見分け方
2 ジェイウォーク カンマ区切りリストを1列に格納 複数形の名前を持つ文字列型の列
3 ナイーブツリー 親IDだけで階層を持つ 自テーブルを参照する parent_id
4 IDリクワイアド 全テーブルに機械的な id 列 使われていない id と、別途ある一意制約
5 キーレスエントリ 外部キー制約を張らない _id で終わる列に REFERENCES が無い
6 EAV 汎用の属性テーブルに何でも入れる attr_name と attr_value の組
7 ポリモーフィック関連 1つの外部キーで複数の親を指す target_type と target_id の組
8 マルチカラムアトリビュート tag1, tag2, tag3 と列を並べる 末尾が連番の同型列
9 メタデータトリブル 年ごとにテーブルや列を複製 名前に年月を含むテーブルや列

この8つのうち、実際のレビューで指摘頻度が高いのはジェイウォーク、キーレスエントリ、EAV、ポリモーフィック関連の4つです。以下では影響の大きい順に見ていきます。テーブルの構造そのものを図で確認したい場合は、ER図とは?書き方・エンティティやリレーション・カーディナリティを実装目線で解説もあわせて参照してください。

ジェイウォーク:カンマ区切りリストが検索も集計も壊す仕組み

製品ごとに担当アカウントIDをカンマ区切りで持つ設計を考えてみます。この形が破綻する理由は「検索できない」ことではありません。検索できているように見えて誤った行を返すからです。以降のコードとコメントの実行結果は、SQLite 3.51.0 のコマンドライン(sqlite3)にそのまま流せば再現します。

CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, account_ids TEXT);
INSERT INTO products VALUES
  (1,'DB Designer','12,34'),
  (2,'Log Viewer','123'),
  (3,'ETL Kit','5,12');

SELECT id, name FROM products WHERE account_ids LIKE '%12%';
-- 実行結果: 1 DB Designer / 2 Log Viewer / 3 ETL Kit  ← 3行すべて返る

アカウント12を担当しているのは製品1と3だけですが、’123′ の部分文字列に12が含まれるため製品2も一致します。回避しようとすると、区切り文字で両端を埋める書き方が必要になります。

SELECT id, name FROM products WHERE ','||account_ids||',' LIKE '%,12,%';
-- 実行結果: 1 DB Designer / 3 ETL Kit  ← 正しい2行

正しい結果は得られますが、列に関数を当てているためインデックスが使えません。さらに「アカウントごとの担当製品数」のような集計は、この形のままでは1クエリで書けません。解決策は第2版2.5が示す交差テーブルです。

PRAGMA foreign_keys = ON;

CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO accounts VALUES (5,'tanaka'),(12,'sato'),(34,'suzuki'),(123,'ito');

CREATE TABLE product_accounts (
  product_id INTEGER NOT NULL REFERENCES products(id),
  account_id INTEGER NOT NULL REFERENCES accounts(id),
  PRIMARY KEY (product_id, account_id)
);
INSERT INTO product_accounts VALUES (1,12),(1,34),(2,123),(3,5),(3,12);

SELECT pa.account_id, COUNT(*) FROM product_accounts pa
GROUP BY pa.account_id ORDER BY 1;
-- 実行結果: 5|1  12|2  34|1  123|1

INSERT INTO product_accounts VALUES (99,12);
-- 実行結果: FOREIGN KEY constraint failed

集計が素直に書けるうえ、存在しない製品IDの投入も止まります。ただしSQLiteで検証する場合は注意点があります。SQLiteの外部キー制約は既定で無効で、PRAGMA foreign_keys の既定値は0です。有効化せずに存在しない product_id を入れても素通りし、有効化すると FOREIGN KEY constraint failed で拒否されます。「制約を書いたのに効かない」という相談の多くはこれが原因です。

IDリクワイアドとキーレスエントリ:主キーの規約と外部キー制約

この2章は逆方向の失敗ですが、どちらも「制約をどこに置くか」の判断です。IDリクワイアド(4章)は、全テーブルに機械的にid列を付けた結果、本来の一意性が守られなくなる問題を扱います。交差テーブルに連番idを足して (product_id, account_id) の複合主キーをやめると、同じ組み合わせが何行でも入ります。この章の解決策4.5.1は「わかりやすい列名にする」で、機械的なidだけでなく、1つの列に2つの意味を持たせたダブルミーニングの列名もここに含まれる論点です。status が申請状態と決裁状態の両方を指している列を思い浮かべてください。値の意味を読み手が推測するしかなくなります。

キーレスエントリ(5章)は外部キー制約を張らない設計です。「アプリケーション側でチェックするから不要」「制約はオーバーヘッドになる」という理由が典型で、第2版5.5.2はこれに「オーバーヘッド……にはなりません」と正面から答えています。制約を外すと、参照先が消えた孤児行を検出するバッチを別途書き、それを定期実行し、検出後の復旧手順まで用意することになります。制約1行より確実に高くつく選択です。

外部キーを張るなら、参照される側の列の値の散らばり方がインデックス効率を左右します。この観点はカーディナリティとは?意味・高低の見分け方とインデックス設計を実装目線で解説で扱っています。

マルチカラムアトリビュートとメタデータトリブル:列とテーブルの複製

tag1、tag2、tag3のように同じ意味の列を並べるのがマルチカラムアトリビュート(8章)です。4つ目のタグが必要になった時点で ALTER TABLE が発生し、その列を参照する全クエリの修正が連鎖します。解決策は8.5の従属テーブルで、ジェイウォークと同じ形に着地します。

メタデータトリブル(9章)は、orders_2024、orders_2025のようにテーブルを年ごとに複製する設計です。年をまたぐ集計のたびにUNIONの本数が増え、列を1つ足すときは全テーブルにALTER TABLEをかけることになります。第2版9.5が示す解決策は「パーティショニングと正規化を行う」で、9.5.1が水平パーティショニング、9.5.2が垂直パーティショニングです。

水平パーティショニングは行を条件で物理的に分割する仕組みで、テーブル名は1つのまま保たれます。orders_2024とorders_2025に分ける方式との決定的な違いは、アプリケーション側のクエリを一切変えなくてよい点です。分割の判断そのものについてはデータベース正規化とは?第1〜第3正規形の手順と非正規化の判断を解説で詳しく扱っています。

ポリモーフィック関連:1つの外部キーで複数の親を指す設計

コメントテーブルが記事とも商品とも紐づくように、target_type と target_id の2列で参照先を切り替える設計がポリモーフィック関連(7章)です。RailsやLaravelのORMが標準機能として持っているため、フレームワーク経由で自然に入ってきます。

問題は外部キー制約を宣言できないことに尽きます。target_id の参照先が行によって違うテーブルなので、REFERENCES を書けません。結果としてキーレスエントリと同じ状態になり、参照先が削除された孤児行がデータベース側では検出できなくなります。

第2版7.5の解決策は2つで、7.5.1が参照を逆向きにする方法、7.5.2が共通の基底テーブルを作る方法です。判断基準を1つ挙げるなら、参照先の種類が今後増える見込みがあるかどうかです。増える見込みがなく2種類で固定されるなら、コメントテーブルを article_comments と product_comments に分けて、それぞれに正しい外部キーを張るのが最も安く済みます。ORMの機能があるという理由だけで採用するのは避けてください。

EAVとサブタイプ設計:シングル・具象・クラステーブル継承の選び分け

アンチパターンなのは、継承ではなくその手前のEAV(エンティティ・アトリビュート・バリュー)です。第2版6.5「解決策:サブタイプのモデリングを行う」の下には、6.5.1シングルテーブル継承、6.5.2具象テーブル継承、6.5.3クラステーブル継承、6.5.4半構造化データ、6.5.5後処理の5つが並びます。継承3方式はこのうちの3つ、つまりEAVから抜け出すための道具です。

EAVで型と必須制約が効かなくなる仕組み

EAVは属性名と属性値を行として持つ設計です。列を増やさずに任意の属性を追加できる柔軟さと引き換えに、データベースが持つ検証機能をほぼ全部手放します。

CREATE TABLE issue_attributes (
  issue_id   INTEGER NOT NULL,
  attr_name  TEXT NOT NULL,
  attr_value TEXT,
  PRIMARY KEY (issue_id, attr_name)
);
INSERT INTO issue_attributes VALUES
  (1,'severity','high'), (1,'reported_at','2026-08-01'),
  (2,'severity','三'),   (2,'reported_at','いつか');

SELECT issue_id FROM issue_attributes
WHERE attr_name='reported_at' AND attr_value > '2026-07-01';
-- 実行結果: 1 と 2 の2行が返る

attr_value が TEXT である以上、’いつか’ は文字列として ‘2026-07-01’ より大きいと判定されます。日付として不正な値がそのまま入り、日付比較の結果にも紛れ込みます。severity に ‘high’ でも ‘low’ でもない ‘三’ が入っている点も同じ理由です。

必須属性を強制できない点はさらに深刻です。reported_at の行を入れ忘れても、NOT NULL 制約に相当するものが存在しません。データベースは「行が無いこと」を検知できないからです。整合性をアプリケーション側に丸ごと移す設計だと理解したうえで選ぶなら構いませんが、属性の種類が事前に分かっているなら選ぶ理由はありません。

継承3方式の比較と、選び分けの分岐点

サブタイプごとに固有の属性を持たせたい場合、選択肢は3つです。

方式 テーブル構成 NOT NULL 全件横断クエリ 向く場面
シングルテーブル継承 1つに全列 付けられない 最も簡単 差分が数列
具象テーブル継承 サブタイプごと独立 付けられる UNIONが必要 横断が不要
クラステーブル継承 共通+サブタイプ 付けられる JOINで可能 共通属性が多い

選び分けの判断は単純です。全サブタイプを横断する一覧画面があるならクラステーブル継承、サブタイプごとに完全に独立した画面しかないなら具象テーブル継承、固有属性が2、3列しかないならシングルテーブル継承で十分です。JOINが増えることを理由にクラステーブル継承を避ける判断は、主キー同士の結合であるためほとんどの場合は根拠になりません。

クラステーブル継承のDDLと、NOT NULL制約を残せる理由

クラステーブル継承は、共通属性の基底テーブルと、サブタイプごとのテーブルを主キー共有で結ぶ構成です。

PRAGMA foreign_keys = ON;

CREATE TABLE issues (
  issue_id    INTEGER PRIMARY KEY,
  reported_by INTEGER NOT NULL,
  summary     TEXT NOT NULL,
  issue_type  TEXT NOT NULL CHECK (issue_type IN ('bug','feature'))
);
CREATE TABLE bugs (
  issue_id INTEGER PRIMARY KEY REFERENCES issues(issue_id),
  severity TEXT NOT NULL,
  version_affected TEXT
);
CREATE TABLE feature_requests (
  issue_id INTEGER PRIMARY KEY REFERENCES issues(issue_id),
  sponsor  TEXT NOT NULL
);

INSERT INTO issues VALUES (1,10,'ログイン後に500','bug'),(2,11,'CSV出力が欲しい','feature');
INSERT INTO bugs VALUES (1,'high','3.2.1');
INSERT INTO feature_requests VALUES (2,'sales');

SELECT i.issue_id, i.summary, b.severity, f.sponsor
FROM issues i
LEFT JOIN bugs b USING (issue_id)
LEFT JOIN feature_requests f USING (issue_id)
ORDER BY i.issue_id;
-- 実行結果: 1|ログイン後に500|high|
--           2|CSV出力が欲しい||sales

bugs.severity に NOT NULL を宣言できる点が、シングルテーブル継承との決定的な差です。1つのテーブルに全列を集めるシングルテーブル継承では、severity は feature の行では NULL になるため NOT NULL を付けられません。バグには必ず深刻度が必要という業務ルールを、データベース側で担保できなくなります。

物理設計とアプリケーション開発で頻出するアンチパターン

第II部と第IV部から、テーブル設計のレビューで実際に指摘の多い3つを取り上げます。第III部のクエリのアンチパターン(14章から19章)は、テーブル構造ではなくSQLの書き方の問題なのでここでは扱いません。

サーティワンフレーバー:列定義に埋め込んだ限定値の追加コスト

第2版11章です。ステータスや種別の取りうる値を CHECK 制約や ENUM で列定義に直接書くと、値を1つ増やすだけでテーブル定義の変更になります。前掲の issues テーブルで確認できます。

INSERT INTO issues VALUES (3,12,'色を変えたい','question');
-- 実行結果: CHECK constraint failed: issue_type IN ('bug','feature')

ALTER TABLE issues ADD CONSTRAINT chk CHECK (issue_type IN ('bug','feature','question'));
-- 実行結果: near "CONSTRAINT": syntax error (SQLiteはCHECK制約の後付け変更に非対応)

SQLiteの場合、制約を変えるにはテーブルを作り直して全行を移すしかありません。第2版11.5の解決策である参照テーブル方式なら、値の追加はマスタへの INSERT 1行で終わります。

PRAGMA foreign_keys = ON;

CREATE TABLE issue_types (issue_type TEXT PRIMARY KEY);
INSERT INTO issue_types VALUES ('bug'),('feature');
CREATE TABLE issues2 (
  id INTEGER PRIMARY KEY,
  issue_type TEXT NOT NULL REFERENCES issue_types(issue_type)
);

INSERT INTO issues2 VALUES (1,'question');
-- 実行結果: FOREIGN KEY constraint failed
INSERT INTO issue_types VALUES ('question');
INSERT INTO issues2 VALUES (1,'question');
-- 実行結果: 成功

値の追加が想定されないもの、たとえば性別コードや論理値の代替なら、列定義に書いたままで構いません。運用中に値が増える種別かどうかが分岐点です。

シー・ノー・エビル:戻り値と組み立て後SQLの確認漏れ

第2版23章、副題は「臭いものに蓋」です。データベースAPIの戻り値や例外を確認せず、動的に組み立てたSQL文字列も出力しないまま開発を進める習慣を指します。23.5.2「デバッグには実際に構築されたSQLクエリを使用する」が示すとおり、見るべきは組み立て後のSQLであって、組み立て前のテンプレート文字列ではありません。

ORMを使っている場合、この章の指摘はそのまま発行SQLのログ出力設定に落ちます。開発環境で発行SQLをログに出していないプロジェクトは、この章に該当していると考えて構いません。

ディプロマティック・イミュニティ:バージョン管理とテストの外にあるSQL

第2版24章で、アンチパターンの中身は24.2「アンチパターン:SQLを特別扱いする」の一文に尽きます。アプリケーションコードにはバージョン管理もテストもコードレビューもあるのに、スキーマ定義とSQLだけがその対象外になっている状態です。24.5の解決策は文書化、バージョン管理、テスティング、複数ブランチの扱いの4つで、要はアプリケーションコードと同じ規律を適用するということです。

実務的には、マイグレーションファイルをリポジトリに入れているかどうかが最初の分岐になります。本番のスキーマ変更を手作業のSQL実行で済ませているなら、この章に該当します。スキーマ変更を含むデプロイの安全性はトランザクションの扱いにも直結するため、トランザクションとは?ACID・分離レベル・commit/rollbackを実装目線で解説【2026年版】もあわせて確認してください。

書籍に対応する章がない、日本の現場で定番の失敗

ここからは第2版の27章に対応する章がない論点です。日本語圏の設計レビューでは書籍のどのパターンよりも頻繁に議論になるにもかかわらず、原典を要約しただけの解説記事では扱われません。

削除フラグ:一意制約の破綻と、全クエリに増える未削除条件

物理削除せず deleted_at や is_deleted で消したことにする、いわゆる論理削除です。最初に壊れるのは一意制約です。

CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL, deleted_at TEXT);
CREATE UNIQUE INDEX ux_users_email ON users(email);

INSERT INTO users VALUES (1,'[email protected]',NULL);
UPDATE users SET deleted_at='2026-08-12' WHERE id=1;
INSERT INTO users VALUES (2,'[email protected]',NULL);
-- 実行結果: UNIQUE constraint failed: users.email

退会したユーザーと同じメールアドレスで再登録できません。行が残り続ける以上、一意制約から見れば削除は起きていないからです。実務的な解は部分ユニークインデックスで、全件対象の索引を落とし、削除されていない行だけを制約の対象にします。

DROP INDEX ux_users_email;
CREATE UNIQUE INDEX ux_users_email_active
  ON users(email) WHERE deleted_at IS NULL;

INSERT INTO users VALUES (2,'[email protected]',NULL);
-- 実行結果: 成功(論理削除済みの行は制約の対象外)

INSERT INTO users VALUES (3,'[email protected]',NULL);
-- 実行結果: UNIQUE constraint failed: users.email(有効な行の重複は従来どおり拒否)

PostgreSQLでもこの部分インデックスが使えます。避けるべきなのは、部分インデックスの代わりに UNIQUE(email, deleted_at) の複合制約で済ませようとする設計です。PostgreSQLのUNIQUE制約は既定でNULL同士を異なる値として扱うため、deleted_at が NULL の有効行どうしが衝突せず、同じメールアドレスの有効行が何件でも入ります。この既定を反転させる NULLS NOT DISTINCT はPostgreSQL 15で追加されたもので、14のドキュメントには存在しません。複合制約で代用するなら、運用中のバージョンを確認してください。

制約を回避できても、もう1つの代償は残ります。参照する全クエリに deleted_at IS NULL が必要になり、1箇所でも書き忘れれば削除済みデータが画面に出ます。判断としては、削除済みデータを業務要件として参照する必要がないなら、論理削除は採用すべきではありません。監査目的なら別途アーカイブテーブルへ移す設計のほうが、通常クエリを汚さずに済みます。削除フラグを残す場合に一意制約の破綻を防ぐ部分一意インデックスのDB別の書き方は論理削除で一意制約と集計が壊れる問題の実装対策を解説した記事で扱っています。

履歴テーブル:現在値と全世代の同居による一意制約の喪失

価格や住所の変更履歴を残そうとして、現行テーブルに valid_from と valid_to を足して全世代を1つのテーブルに入れる設計です。破綻の仕方は削除フラグと同型で、「現在の値」を取るのに毎回 valid_to IS NULL のような条件が要り、商品IDに対する一意制約が張れなくなります。

実務では、現在値のテーブルと履歴テーブルを分けるほうが扱いやすくなります。現在値テーブルには商品IDの主キーと NOT NULL 制約をそのまま張れ、参照側のクエリは条件なしで書けます。履歴テーブルは追記専用にし、更新も削除もしません。

期間を持つテーブルを1本で管理する設計そのものが誤りというわけではなく、複数世代を横断する分析が主用途なら統合されている形が正解です。分岐点は、参照回数の大半が「現在の値」なのか「ある時点の値」なのかにあります。前者が9割を超えるなら分けてください。

中間テーブル:業務属性の追加でエンティティへ変わる分岐点

ジェイウォークの解決策として作った交差テーブルが、時間とともに劣化するパターンです。最初は (product_id, account_id) の複合主キーだけだったものに、担当開始日、担当区分、備考、承認者と列が増えていきます。

この時点で、それはもう「関連の記録」ではなく「担当」という独立したエンティティです。判断基準は明快で、交差テーブルの属性に対して更新履歴を残したくなったら分割の合図です。担当区分の変更履歴が必要になった瞬間、複合主キーのままでは同じ組み合わせの過去分を保持できません。assignments のような名前のテーブルに切り出し、独自の主キーを持たせてください。

逆に、属性が1つも増えていない交差テーブルに機械的なid列を足す必要はありません。それはIDリクワイアド(4章)そのものです。

設計レビューでアンチパターンを潰す進め方

アンチパターンの知識を成果に変えるには、レビューの手順に埋め込む必要があります。効果が高い順に並べると次のようになります。

  • テーブル定義のレビューで、_id で終わる列に REFERENCES が付いているかを機械的に確認する
  • 複数形の名前を持つ文字列型の列と、末尾が連番の同型列を検索して洗い出す
  • attr_name と attr_value の組、target_type と target_id の組をスキーマ全体から検索する
  • 名前に年月を含むテーブルと列を一覧化し、パーティショニングで置き換えられないか検討する
  • 削除フラグを持つテーブルについて、部分ユニークインデックスの有無を確認する

いずれもスキーマ定義ファイルへの grep で機械的に拾えます。設計書のレビューよりも、CREATE TABLE 文そのものを見るほうが確実です。

そのうえで、検出したものを全部直そうとしないでください。第2版の2章から25章が「アンチパターンを用いてもよい場合」の節を欠かさず持っているのは、条件次第で正解になるからです。検出したパターンごとに、そのデータに対する検索・集計・一意性の要件が今後発生するかだけを確認し、発生しないものは記録に残して放置する判断が正しい判断になります。データベースの選定段階から見直す場合は、データベースとは?種類・DBMS・RDBとNoSQLの選び方を実装目線で解説を参照してください。

よくある質問

アンチパターンとはどういう意味ですか?

広く使われているにもかかわらず、結果として問題を生む設計や実装のパターンを指します。単なる誤りとの違いは、採用した時点では合理的に見える点です。『SQLアンチパターン 第2版』でアンチパターンを扱う2章から25章は「目的」「アンチパターン」「見つけ方」「用いてもよい場合」「解決策」の5節構成で、禁止事項ではなく条件付きで判断すべきものとして扱われています。24章すべてに「用いてもよい場合」が置かれているのは、そのためです。

クラステーブル継承はアンチパターンですか?

アンチパターンではありません。『SQLアンチパターン 第2版』の目次では、クラステーブル継承は6章EAVの「解決策:サブタイプのモデリングを行う」の下、6.5.3に置かれています。同じ階層にシングルテーブル継承(6.5.1)と具象テーブル継承(6.5.2)が並びます。アンチパターンとされているのは、これらの手前にある汎用属性テーブル、つまりEAVのほうです。

SQLアンチパターンの初版と第2版はどちらを読むべきですか?

これから買うなら第2版です。2025年7月11日発行、400ページ、全27章で、初版(2013年1月26日、352ページ、25章)から章の入れ替えがあります。初版24章のマジックビーンズと25章の砂の城が目次から外れ、25章スタンダード・オペレーティング・プロシージャと第V部ボーナスの26章・27章が加わりました。砂の城はオライリー・ジャパンが日本語版付録として9ページのPDFで公開しています。

削除フラグはアンチパターンとして扱われていますか?

『SQLアンチパターン 第2版』の27章に、削除フラグに対応する章はありません。ただし実装上の問題は明確で、一意制約が論理削除された行にも効き続けるため、退会したユーザーと同じメールアドレスでの再登録が拒否されます。回避には部分ユニークインデックスが必要です。参照側の全クエリに未削除の条件が要る点も含め、削除済みデータを業務で参照しないなら採用しない判断が妥当です。

DB設計のアンチパターンは、まずどれから確認すべきですか?

論理設計にあたる第I部の8章から確認してください。物理設計やクエリのアンチパターンは後からでも修正できますが、テーブル構造そのものの誤りはデータが蓄積するほど移行コストが上がります。レビューでは、外部キー制約が無い _id 列(5章キーレスエントリ)、attr_name と attr_value の組(6章EAV)、target_type と target_id の組(7章ポリモーフィック関連)の3つを最初に探すのが効率的です。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応
  2. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】
  3. 2026.10.23 テックブログ Splunk 脆弱性CVE-2026-76299:影響版の確認と10.2.8などへの更新手順
  4. 2026.10.22 テックブログ Tomcatのバージョン一覧とサポート期限【2026年10月】最新版11.0.27、9.0系は2027年3月末終了・9.1系は2030年末まで
  5. 2026.10.21 テックブログ ユミルリンク不正アクセス第二報で延べ5,162万件:Cuenote FC利用企業と受信者の確認点

RELATED POSTS 関連記事

目次