Database Schema Design in 2026 を読んで、DB設計で後戻りしにくい判断を整理した
この記事は、以下の記事を読んで学んだ内容を、自分の理解として整理したものです。
TL;DR
DB設計では、今すぐ動くかどうかだけでなく、データが増えたあとに変更できるかを考える必要がある。
特に次の判断は、あとから直すコストが大きい。
- primary key を
BIGSERIALにするか UUID にするか - よく検索する値を JSONB に入れるか typed column にするか
- どの index を使うか
- 正規化(normalize)するか非正規化(denormalize)するか
DB移行に比べると、アプリコードは deploy で反映できる範囲が多い。一方で、DBは一度大量のデータが入ると移行が重くなる。だから、DB設計は少し慎重に判断した方がよい。
背景
DB設計を考えるとき、最初は「保存できればよい」と考えがちです。
たとえば、ECの商品データなら次のように考えるかもしれません。
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
data JSONB NOT NULL
);
この設計でも、商品情報を保存すること自体はできます。
しかし、あとから次のような要件が出ると困ります。
- 複数DBのデータを統合したい
priceで検索・並び替えしたいattributes.colorで絞り込みたい- 商品数や行数が増えて index の効き方が重要になった
- 表示高速化のために非正規化したい
ここで重要なのは、DB設計の変更はアプリコードの変更よりも重くなりやすい、ということです。
問題: DB設計はデータが増えるほど戻しにくい
アプリコードなら、関数や条件分岐を直して deploy すれば済むことが多いです。
一方で DB は、すでに保存されたデータがあります。
たとえば primary key の設計を変える場合、単に column を変えるだけでは終わりません。
- 既存データをどう変換するか
- foreign key をどう張り替えるか
- application 側の参照をどう変えるか
- migration 中に write を止める必要があるか
- rollback できるか
このように、DB設計は「データが入ったあと」に変更コストが跳ね上がります。
判断例1: BIGSERIAL と UUID v7
BIGSERIAL は、DBが 1, 2, 3... と連番を振ってくれる仕組みです。
単一DBで運用するならシンプルで扱いやすいです。
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL
);
ただし、複数DBで同じ設計を使うと注意が必要です。
たとえば DB-A と DB-B がそれぞれ注文を作ると、両方で id = 1 が発生します。
| DB | 入力 | 採番結果 |
|---|---|---|
| DB-A | 注文A | order_id = 1 |
| DB-B | 注文B | order_id = 1 |
あとでDBを統合すると、同じ order_id = 1 がぶつかります。
このように、複数DB・分散環境・将来の統合がありそうな場合は、全体で一意になりやすい UUID を検討します。
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL
);
特に UUID v7 は、時間順に近い性質を持つため、UUID v4 のようなランダム UUID より index locality の面で扱いやすくなる場合があります。
判断例2: JSONB と typed column
PostgreSQL の JSONB は柔軟です。
商品ごとに違う属性を持たせたい場合には便利です。
CREATE TABLE products (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB NOT NULL
);
たとえば attributes に次のようなデータを入れられます。
{
"color": "black",
"size": "M"
}
一方で、検索や並び替えによく使う値まで JSONB に入れると扱いづらくなります。
たとえば price は、検索・並び替え・集計に使われやすい値です。
この場合は typed column として切り出した方が扱いやすいです。
CREATE TABLE products (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL,
attributes JSONB NOT NULL
);
判断の目安は次の通りです。
| データ | 保存先 | 理由 |
|---|---|---|
price |
typed column | 検索・並び替え・集計に使うため |
attributes.color |
JSONB | 商品ごとに属性が変わりやすいため |
つまり、よく検索する重要な値は column に出す、柔軟な追加属性は JSONB に入れる、という分け方です。
判断例3: index を用途で選ぶ
index は「とりあえず付ける」ものではなく、検索パターンに合わせて選びます。
代表的には次のように考えます。
| 用途 | index | 向いている理由 |
|---|---|---|
price = 1000 / price BETWEEN ... / ORDER BY price |
B-tree | 通常の等価検索・範囲検索・並び替えに向く |
attributes @> '{"color":"black"}' のように JSONB の中身を探す |
GIN | JSONB の containment 検索に向く |
時系列ログの created_at 範囲検索 |
BRIN | 物理的な保存順と時刻が近い巨大テーブルで軽い |
例として、price には通常の B-tree index を使います。
CREATE INDEX idx_products_price ON products (price);
JSONB の中身を containment operator(@>)で検索したい場合は GIN index を検討します。
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);
一方で、attributes->>'color' = 'black' のように特定 key の値だけを等価検索するなら、expression index も候補になります。
時系列ログのように、追加順と created_at がだいたい一致する巨大テーブルでは BRIN が候補になります。
CREATE INDEX idx_logs_created_at_brin ON logs USING BRIN (created_at);
大事なのは、index 名ではなく「どの検索を速くしたいのか」から逆算することです。
判断例4: 正規化と非正規化
基本は正規化から始める方が安全です。
正規化(normalize)とは、同じデータを重複して持たず、正しい場所に1つだけ持つ設計です。
たとえば user 名は users table に持ち、注文は orders.user_id で参照します。
CREATE TABLE users (
id UUID PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id)
);
一方で非正規化(denormalize)は、表示や集計を速くするためにデータをコピーして持つ設計です。
CREATE TABLE orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
user_name TEXT NOT NULL
);
この設計では、注文一覧に現在のユーザー名を表示したいときに、users table を join しなくても user_name を表示できます。
ただし、現在のユーザー名として扱う場合は、ユーザー名が変わったときに注意が必要です。
| 更新対象 | 更新内容 |
|---|---|
users.name |
新しい名前に更新 |
orders.user_name |
コピーして持っている名前も更新が必要 |
もし orders.user_name の更新が漏れると、users では新しい名前なのに、orders では古い名前のままになります。
これが非正規化の怖いところです。
そのため、最初から何でも非正規化するのではなく、まず正規化し、計測して本当に必要な箇所だけ非正規化する方が安全です。
まとめ: DB設計は「将来の変更コスト」で考える
DB設計で大事なのは、今すぐ動くかだけではありません。
次のように考えると判断しやすくなります。
| 判断対象 | 考えること |
|---|---|
| primary key | 将来、複数DB・統合・分散があり得るか |
| JSONB / typed column | 検索・並び替え・集計に使う値か |
| index | どの query を速くしたいのか |
| 正規化 / 非正規化 | 正しさを優先するか、計測後に高速化するか |
DBは、データが増えるほど変更しづらくなります。
だからこそ、DB設計は「あとから戻せるか」「データ移行が重くならないか」を考えて選ぶ必要があります。
DB設計時のチェックリスト
DB設計時は、最低限次を確認するとよさそうです。
- その primary key は将来のデータ統合でも衝突しないか
- よく検索・並び替えする値を JSONB に閉じ込めていないか
- index は query pattern に合っているか
- 非正規化する場合、更新漏れを防ぐ仕組みがあるか
- データが増えたあとでも migration できるか
DB設計は地味ですが、あとから効いてきます。
最初に少し慎重に考えるだけで、未来の大きな手戻りを減らせます。