アンチパターンを学ぶ意義
「悪い設計」を知ることは「良い設計」を理解するよりも実践的な学びにつながります。アンチパターンは「一見うまくいきそうに見えるが、長期的に問題を引き起こす解決策」です。多くのアンチパターンは、初期要件では問題なく動作しますが、データ量の増加や要件の変化に対応できなくなります。
アンチパターン1:EAV(Entity-Attribute-Value)モデルの濫用
EAVは柔軟性のために属性をキーバリューで保存するパターンです。「将来どんな属性が追加されるかわからない」という状況で使われますが、クエリが複雑になりパフォーマンスが著しく低下します。
-- ❌ アンチパターン:EAVモデル
CREATE TABLE product_attributes (
product_id UUID NOT NULL,
attr_name VARCHAR(100) NOT NULL,
attr_value TEXT,
PRIMARY KEY (product_id, attr_name)
);
-- "価格が1000円以下で、色が赤い商品"のクエリが地獄化
SELECT p.id, p.name
FROM products p
WHERE p.id IN (
SELECT product_id FROM product_attributes
WHERE attr_name = 'price' AND attr_value::NUMERIC <= 1000
)
AND p.id IN (
SELECT product_id FROM product_attributes
WHERE attr_name = 'color' AND attr_value = 'red'
);
-- ✅ 改善策1:具体的なカラムを使用
ALTER TABLE products
ADD COLUMN price DECIMAL(10,2),
ADD COLUMN color VARCHAR(50),
ADD COLUMN weight DECIMAL(8,2);
-- ✅ 改善策2:カテゴリ固有の属性はJSONBで管理(PostgreSQL)
ALTER TABLE products ADD COLUMN attributes JSONB;
CREATE INDEX idx_products_attributes ON products USING GIN(attributes);
-- クエリが直感的に
SELECT id, name FROM products
WHERE (attributes->>'price')::NUMERIC <= 1000
AND attributes->>'color' = 'red';
アンチパターン2:ゴッドテーブル(God Table)
すべての情報を一つの巨大なテーブルに詰め込むパターンです。JOINを避けるために作られることが多いですが、保守性が著しく低下します。
-- ❌ アンチパターン:なんでもかんでも1テーブルに
CREATE TABLE everything (
id UUID PRIMARY KEY,
type VARCHAR(50), -- 'user', 'order', 'product'
user_name VARCHAR(100),
user_email VARCHAR(255),
order_total DECIMAL(10,2),
order_user_id UUID,
product_price DECIMAL(10,2),
product_category VARCHAR(50)
-- NULL だらけになる
);
-- ✅ 改善策:正規化されたテーブル設計
CREATE TABLE users (
id UUID PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE products (
id UUID PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10,2) NOT NULL,
category VARCHAR(50)
);
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
total DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW()
);
アンチパターン3:ステータスコードのマジックナンバー
整数値で状態を管理するパターンは、コードの可読性を下げ、新しいメンバーの理解を阻みます。
-- ❌ アンチパターン:意味不明な数値
CREATE TABLE orders (
id UUID PRIMARY KEY,
status INT -- 1=pending, 2=processing, 3=shipped, 4=delivered, 5=cancelled
);
SELECT * FROM orders WHERE status = 3; -- 3って何?
-- ✅ 改善策1:CHECK制約付きのENUM風VARCHAR
CREATE TABLE orders (
id UUID PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled'))
);
SELECT * FROM orders WHERE status = 'shipped'; -- 明確!
-- ✅ 改善策2:PostgreSQLのCREATE TYPE(ENUM)
CREATE TYPE order_status AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');
ALTER TABLE orders ADD COLUMN status order_status DEFAULT 'pending';
アンチパターン4:インデックスの過剰作成または不足
インデックスをほとんど作らない「インデックス貧乏」と、すべてのカラムにインデックスを作る「インデックス過多」の両方がアンチパターンです。
-- インデックスの使用状況を確認
SELECT
schemaname,
tablename,
indexname,
idx_scan AS times_used,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC, pg_relation_size(indexrelid) DESC;
-- 使われていないインデックス(削除候補)
-- idx_scan = 0 で size が大きいもの
-- テーブルスキャンが多い箇所(インデックス追加候補)
SELECT relname, seq_scan, idx_scan,
seq_scan - idx_scan AS seq_vs_idx
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_scan DESC;
Erdmatesで設計をレビュー
ErdmatesのERDビジュアライゼーションを使えば、ゴッドテーブルやEAVの濫用といった設計上の問題を一目で発見できます。テーブルのカラム数が異常に多い、あるいはほとんどのカラムがNULLABLEになっているといったアンチパターンのサインをERDで視覚化し、チームでレビューしましょう。
まとめ
アンチパターンは「悪意のある設計」ではなく、「当時は合理的に見えた解決策が長期的に問題を引き起こすもの」です。設計段階でのコードレビューとERDレビューを組み合わせ、アンチパターンを早期に発見・修正するプロセスを整備することが重要です。