Posts に戻る

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設計は地味ですが、あとから効いてきます。

最初に少し慎重に考えるだけで、未来の大きな手戻りを減らせます。