Day29 前半のゴール
「バラバラに学んだ機能を、“1つのミニ業務システム”としてつなげる」
Day29 は総合課題です。 テーマは「顧客管理」「注文処理」「分析クエリ」「JSONデータ処理」を全部まとめて実装すること。 前半では、まず「どんなテーブル設計にするか」「基本的なINSERT/SELECTができる状態にすること」をゴールにします。
ここまで学んだ正規化・インデックス・JSON・分析系関数を、「小さなECサイト(通販)」をイメージしながら一つにまとめていきます。
全体シナリオを決める
「小さなECサイトをPostgreSQLで支える、という設定にする」
いきなりテーブルを作る前に、「どんな世界をDBで表現するか」を言葉にしておきます。 ここでは「小さなECサイト」を想定します。
顧客が会員登録をする。 顧客が商品をカートに入れて、注文を確定する。 注文には複数の商品が含まれる。 管理画面では、「売上ランキング」「顧客ごとの購入回数」「期間別売上」などを分析したい。 一部の柔軟な情報(顧客の属性タグ、注文時のメタ情報など)はJSONで持ちたい。
このシナリオを支えるために、最低限必要なテーブルはこういうイメージになります。
顧客を管理するテーブル(customers) 商品を管理するテーブル(products) 注文のヘッダ(orders) 注文の明細(order_items)
そして、顧客や注文に「柔軟な追加情報」を持たせるための JSONB カラムをいくつか仕込んでおきます。
顧客管理テーブルの設計
「正規化しつつ、“柔らかい情報”はJSONに逃がす」
まずは顧客テーブルです。 基本的な情報は普通のカラムで持ち、変化しやすい・項目が増えがちな情報は JSONB に逃がす、というハイブリッド構成にします。
CREATE TABLE customers (
id bigserial PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
extra_info jsonb NOT NULL DEFAULT '{}'::jsonb
);
SQLここでのポイントを深掘りします。
id は bigserial の主キー。 name, email はよく使うので普通のカラム。 email に UNIQUE 制約を付けて、「同じメールアドレスで複数登録できない」ようにしている。 created_at, updated_at はトリガーで自動更新してもよいが、ここではシンプルに DEFAULT now() から始める。 extra_info は jsonb 型で、「顧客の属性タグ」「マーケティング用フラグ」「任意のメモ」などを柔軟に入れられる。
例えば、extra_info にはこんなJSONを入れられます。
{
"tags": ["premium", "newsletter"],
"age_range": "30-39",
"favorite_category": "books"
}
こうしておくと、「基本的な構造は保ちつつ、ビジネス側の“あとから増える要望”に耐えられる顧客テーブル」になります。
商品テーブルの設計
「価格や在庫は普通のカラム、属性はJSONで拡張可能に」
次に商品テーブルです。 ここも同じく、「よく使うものはカラム」「変わりやすいものはJSONB」という構成にします。
CREATE TABLE products (
id bigserial PRIMARY KEY,
name text NOT NULL,
sku text NOT NULL UNIQUE,
price_cents integer NOT NULL,
stock_qty integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
attributes jsonb NOT NULL DEFAULT '{}'::jsonb
);
SQL重要なポイントはこうです。
sku は「商品コード」のイメージで、UNIQUE 制約を付けている。 price_cents は「金額を整数(円・セント)で持つ」ためのカラム。浮動小数ではなく整数にしておくと安全。 stock_qty は在庫数。ここから在庫管理のロジックを組み立てられる。 attributes は JSONB で、「色」「サイズ」「ブランド」「タグ」などを柔軟に持てる。
例えば、attributes にはこんなJSONを入れられます。
{
"color": "red",
"size": "M",
"categories": ["fashion", "t-shirt"]
}
こうしておくと、「分析クエリで attributes->>’color’ = ‘red’ のような条件を使う」こともできます。
注文ヘッダ(orders)の設計
「顧客と紐づけ、状態や合計金額を持たせる」
注文は、「ヘッダ」と「明細」に分けるのが定番です。 まずはヘッダ側(orders)を作ります。
CREATE TABLE orders (
id bigserial PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers(id),
status text NOT NULL,
total_cents integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
meta jsonb NOT NULL DEFAULT '{}'::jsonb
);
SQLここでの重要ポイントを整理します。
customer_id は customers.id への外部キー。 「この注文はどの顧客のものか」を表す。 status は ‘pending’, ‘paid’, ‘shipped’, ‘cancelled’ などの状態を文字列で持つ(本格的にはENUMや別テーブルでもよい)。 total_cents は「注文全体の合計金額」。明細から集計して入れる。 meta は JSONB で、「配送メモ」「クーポン情報」「決済プロバイダのレスポンス」などを柔軟に保存できる。
例えば、meta にはこんなJSONが入ります。
{
"payment_method": "credit_card",
"coupon_code": "WELCOME10",
"note": "玄関前に置き配希望"
}
これにより、「注文の基本情報+柔軟なメタ情報」を1つのテーブルで扱えます。
注文明細(order_items)の設計
「1つの注文に複数商品をぶら下げる“子テーブル”」
注文明細は、「どの注文に、どの商品が、いくつ、いくらで入っているか」を表すテーブルです。
CREATE TABLE order_items (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
product_id bigint NOT NULL REFERENCES products(id),
quantity integer NOT NULL,
unit_price_cents integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
SQLここでのポイントはこうです。
order_id は orders.id への外部キーで、「どの注文の明細か」を表す。 product_id は products.id への外部キーで、「どの商品か」を表す。 quantity は数量。 unit_price_cents は「そのときの単価」。 ここが重要で、「後から商品価格が変わっても、注文時の価格は変わらないようにする」ために、明細にコピーしておく。
この構造により、
1つの注文(orders.id)に対して、複数の order_items がぶら下がる。 顧客1人に対して、複数の注文がぶら下がる。
という「1対多」「多対多(顧客と商品)」の関係を、正規化された形で表現できます。
サンプルデータを入れて“動くイメージ”を掴む
「顧客1人・商品2つ・注文1件を手で入れてみる」
設計だけだとイメージがぼやけるので、実際にINSERTしてみます。
顧客を1人登録します。
INSERT INTO customers (name, email, extra_info)
VALUES (
'山田太郎',
'taro@example.com',
'{"tags": ["premium"], "favorite_category": "books"}'
)
RETURNING id;
SQLここで返ってきた id をメモしておきます(仮に1とします)。
商品を2つ登録します。
INSERT INTO products (name, sku, price_cents, stock_qty, attributes)
VALUES
('PostgreSQL入門本', 'BOOK-001', 2500 * 100, 100, '{"category": "books"}'),
('ロゴ入りTシャツ', 'TSHIRT-001', 1500 * 100, 50, '{"category": "fashion", "size": "M"}')
RETURNING id;
SQLここで返ってきた id を、仮に 1(本)、2(Tシャツ)とします。
次に、注文ヘッダを1件作ります。
INSERT INTO orders (customer_id, status, total_cents, meta)
VALUES (
1,
'paid',
2500 * 100 + 1500 * 100,
'{"payment_method": "credit_card"}'
)
RETURNING id;
SQLこれで orders.id が例えば 1 だとします。
最後に、注文明細を2行入れます。
INSERT INTO order_items (order_id, product_id, quantity, unit_price_cents)
VALUES
(1, 1, 1, 2500 * 100),
(1, 2, 2, 1500 * 100);
SQLこれで、「山田太郎さんが、本を1冊とTシャツを2枚買った」という注文が、 customers/products/orders/order_items の4テーブルにまたがって表現されました。
基本的な結合クエリで“全体像”を確認する
「顧客+注文+明細+商品を1本のSELECTで見る」
ここまで来たら、「ちゃんとつながっているか」をSELECTで確認してみます。
SELECT
c.name AS customer_name,
o.id AS order_id,
o.status AS order_status,
o.total_cents AS order_total_cents,
p.name AS product_name,
oi.quantity AS quantity,
oi.unit_price_cents AS unit_price_cents
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.id = 1;
SQLこの結果は、例えばこんなイメージになります。
顧客名:山田太郎 注文ID:1 ステータス:paid 合計金額:550000(=5500円を100倍した値) 商品名:PostgreSQL入門本、数量1、単価250000 商品名:ロゴ入りTシャツ、数量2、単価150000
この1本のSELECTで、「顧客→注文→明細→商品」という関係が全部つながっていることが確認できます。
ここまでできれば、「顧客管理」「注文処理」の基本部分は実装できたと言えます。
JSONBを使った簡単なフィルタの例
「“タグがpremiumの顧客の注文だけ”を取る」
最後に、JSONBを使った簡単な例を1つだけ前半に入れておきます。 顧客の extra_info.tags に “premium” が含まれている人の注文だけを取りたいとします。
SELECT
c.name,
o.id,
o.total_cents
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE c.extra_info -> 'tags' ? 'premium';
SQLここで使っているポイントはこうです。
c.extra_info -> 'tags' で、JSONBから tags 配列を取り出している。 ? 'premium' は、「そのJSON配列(またはオブジェクト)の中に ‘premium’ という要素(キー)があるか」を判定する演算子。
これにより、「premiumタグが付いている顧客の注文だけ」を簡単に絞り込めます。
Day29 後半では、ここからさらに「分析クエリ(集計・ランキング)」「JSONBのインデックス」「もう少し複雑な条件」を扱っていきます。
Day29 前半のまとめ
Day29 前半では、「小さなECサイト」を題材にして、顧客(customers)、商品(products)、注文ヘッダ(orders)、注文明細(order_items)という4つのテーブルを正規化しつつ、顧客の属性や商品の属性、注文のメタ情報など“変化しやすい情報”を JSONB カラム(extra_info, attributes, meta)に逃がすハイブリッド設計を行い、実際に顧客1人・商品2つ・注文1件をINSERTしてから、JOINを使って「顧客→注文→明細→商品」を1本のSELECTで確認し、さらに extra_info -> 'tags' ? 'premium' のようなJSONBフィルタで「premiumタグの顧客の注文だけ」を絞り込むところまでを通して、「顧客管理+注文処理+JSONデータ処理」が1つのスキーマとしてつながる感覚を掴むことができた、という状態がゴールになります。
