データベース設計は、アプリケーション開発の中で最も修正コストが高い領域の1つです。テーブル設計の誤りは後から発覚することが多く、データ移行を伴う修正は大きなリスクを伴います。筆者は10年以上のキャリアの中で、同じようなアンチパターンが異なるプロジェクトで繰り返し発生するのを目撃してきました。
本記事では、特に頻出するSQLアンチパターンを取り上げ、具体的なコード例とともに問題点と解決策を解説します。
1つのカラムにカンマ区切りで複数の値を格納するのは、最も見かけるアンチパターンの1つです。
CREATE TABLE articles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
tag_ids VARCHAR(500) -- "1,3,7,12" のようにカンマ区切りで格納
);
一見シンプルですが、この設計は多くの問題を引き起こします。特定のタグを持つ記事を検索するクエリは非常に非効率になります。
-- アンチパターン:LIKEで部分一致検索するしかない
SELECT * FROM articles WHERE tag_ids LIKE '%7%';
-- tag_id=17 や tag_id=71 も誤ってマッチする
CREATE TABLE articles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL
);
CREATE TABLE article_tags (
article_id BIGINT NOT NULL,
tag_id BIGINT NOT NULL,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
-- 効率的な検索が可能
SELECT a.* FROM articles a
JOIN article_tags at ON a.id = at.article_id
WHERE at.tag_id = 7;
中間テーブルを使うことで、インデックスが効くようになり、データの整合性も外部キー制約で保証されます。
動的にカラムを追加したいという要件に対して、すべての属性をキーバリューペアとして格納するEAVパターンが使われることがあります。
CREATE TABLE product_attributes (
product_id BIGINT NOT NULL,
attribute_name VARCHAR(100) NOT NULL,
attribute_value TEXT,
PRIMARY KEY (product_id, attribute_name)
);
-- データ例
-- (1, 'color', 'red')
-- (1, 'weight', '500')
-- (1, 'price', '2980')
この設計の問題点は、データ型の制約がかけられないこと、集計やソートが困難なこと、クエリが複雑になることです。priceを数値として扱いたくても、TEXT型に格納されているためキャストが必要になり、インデックスも効きにくくなります。
PostgreSQLを使っているなら、JSONB型がEAVの良い代替手段になります。
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
attributes JSONB DEFAULT '{}'
);
-- GINインデックスで効率的な検索が可能
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);
-- 特定の属性で検索
SELECT * FROM products
WHERE attributes @> '{"color": "red"}';
開発の初期段階ではSELECT *を多用しがちですが、これは本番環境で多くの問題を引き起こします。
-- アンチパターン
SELECT * FROM users WHERE active = true;
-- 正しいアプローチ:必要なカラムだけを明示的に指定
SELECT id, name, email, created_at
FROM users
WHERE active = true;
SELECT *の問題点は、不要なデータの転送によるパフォーマンス低下、テーブルにカラムが追加された時の予期せぬ動作変更、そしてカバリングインデックスが使えなくなることです。特に大量のBLOBカラムやTEXTカラムがある場合、不要なデータの転送は深刻なパフォーマンス問題になります。
「クエリが遅いからインデックスを追加しよう」という安易な判断で、あらゆるカラムにインデックスを貼りまくるパターンです。
-- 過剰なインデックス
CREATE INDEX idx_users_name ON users(name);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created ON users(created_at);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_name_email ON users(name, email);
CREATE INDEX idx_users_email_name ON users(email, name);
インデックスはSELECTを高速化する一方で、INSERT・UPDATE・DELETEのたびに更新が必要になるため、書き込み性能を低下させます。ディスク使用量も増加します。
-- まずクエリの実行計画を確認
EXPLAIN ANALYZE
SELECT id, name FROM users
WHERE status = 'active' AND created_at > '2025-01-01'
ORDER BY created_at DESC
LIMIT 20;
-- 実行計画を見て本当に必要なインデックスだけを追加
CREATE INDEX idx_users_status_created ON users(status, created_at DESC);
これらのアンチパターンに共通するのは、開発初期には問題が顕在化しにくいという点です。データ量が少ないうちはどんな設計でもそれなりに動いてしまいます。しかし、データが増えるにつれて性能問題が深刻化し、修正コストも跳ね上がります。
DB設計のレビューをコードレビューと同じ重要度で実施すること、そしてEXPLAINを日常的に使うことを習慣化すれば、これらのアンチパターンの多くを事前に防げます。データベースは一度作ったら簡単には変更できません。設計段階での慎重さが、将来の自分たちを救うことになるでしょう。