Day25 前半のゴール
「顧客×商品×注文を“ちゃんと分けて”設計できるようになる」
Day24 では「顧客だけ」の世界でした。
Day25 は一気に現実寄りになります。テーマは 売上管理(顧客×商品×注文)。
前半のゴールはここです。
顧客・商品・注文を、1つのテーブルに押し込まず“きれいに分けて”設計できる
それぞれのテーブルの役割と、どう紐づくか(外部キーのイメージ)を理解する
SQLite で CREATE TABLE を書いて、最低限の売上管理スキーマを作れる
まだ複雑な集計や検索は後半に回して、
まずは「テーブル設計の骨格」をしっかり作ります。
まずは“全部1テーブルに入れたくなる”気持ちからスタートする
「ダメな例をあえて考えてみる」
いきなり正解を目指すより、
まず「やりがちなダメな設計」をイメージした方が理解が深まります。
例えば、こんなテーブルを想像してみてください。
sales
id | customer_name | customer_email | product_name | price | quantity | ordered_at
SQL一見、これでも売上は管理できそうです。
でも、すぐに問題が出てきます。
同じ顧客が何度も注文すると、そのたびに名前・メールアドレスが重複する
同じ商品が何度も売れると、そのたびに商品名・価格が重複する
商品価格を変更したとき、「過去の売上はどう扱うか」がぐちゃぐちゃになる
つまり、「顧客」「商品」「注文」という性質の違う情報を
1つのテーブルに押し込んでしまっているのが問題です。
ここで、正規化の話(Day20)を思い出してほしいんですが、
まさに「繰り返し」「重複」「意味の違う情報の混在」が起きています。
だからこそ、
売上管理では最低でも
顧客テーブル
商品テーブル
注文テーブル(+必要なら注文詳細テーブル)
に分ける、という発想が必要になります。
顧客テーブルは Day24 の延長で考える
「顧客は“誰が買ったか”の軸になる」
顧客テーブルは、Day24 で作ったものをそのまま使えます。
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
phone TEXT,
registered_at TEXT NOT NULL,
status INTEGER NOT NULL DEFAULT 1
);
SQL売上管理の文脈で見ると、
このテーブルは「誰が買ったか」を表す軸になります。
注文テーブルからは、
「この注文はどの顧客のものか」をcustomer_id で参照することになります。
ここで重要なのは、
顧客の情報は顧客テーブルに集約する
売上側には「顧客IDだけ」を持たせる
という分離の発想です。
商品テーブルを新しく設計する
「商品も“独立した存在”として管理する」
次に必要なのが、商品テーブルです。
最低限、こんな項目を持たせます。
商品を一意に識別する ID
商品名
単価(1個あたりの価格)
ステータス(販売中/販売停止など)
SQLite で書くと、例えばこうなります。
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL,
status INTEGER NOT NULL DEFAULT 1
);
SQLここでのポイントは、
「価格をどこに持たせるか」です。
商品テーブルに price を持たせると、
「今の販売価格」は一元管理できます。
一方で、「過去の注文時の価格」をどう扱うかは、
後で注文テーブル側で考えることになります(ここが実務的に重要なポイントです)。
注文テーブルの役割をはっきりさせる
「“いつ・誰が・いくら分の注文をしたか”の単位」
次に、注文テーブルを考えます。
ここでまず決めるべきは、「注文の単位」です。
例えば、
1回の注文で商品を3種類買った場合、
それを「1件の注文」として扱うか、
「3件の売上」として扱うか。
現実的には、
注文(ヘッダ)
注文の中の明細(行)
に分けるのが定番です。
前半では、まず「注文ヘッダ」だけを作ります。
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
ordered_at TEXT NOT NULL,
status INTEGER NOT NULL DEFAULT 1
);
SQL意味はこうです。
id
注文そのものの ID。
customer_id
どの顧客の注文か。customers.id を参照する外部キー的なカラムです(SQLite では外部キー制約を有効にする設定が必要ですが、まずは“概念”として押さえればOK)。
ordered_at
注文日時。'2025-05-02 10:30:00' のような文字列で入れる想定。
status
注文の状態。1=有効、0=キャンセル、など。
このテーブルは、
「1回の注文」という単位を表します。
注文の中身(どの商品を何個買ったか)をどう表現するか
「order_items という“明細テーブル”を用意する」
1回の注文で、商品を複数買えるようにするには、
「注文ヘッダ」とは別に「注文明細テーブル」が必要です。
典型的な形はこうです。
CREATE TABLE order_items (
id INTEGER PRIMARY KEY,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
unit_price INTEGER NOT NULL
);
SQLここが Day25 前半の一番大事なポイントです。
order_id
どの注文(orders.id)に属する明細か。
product_id
どの商品(products.id)か。
quantity
何個買ったか。
unit_price
その注文時点での単価。
特に unit_price が重要です。
商品テーブルの price は「現在の価格」です。
もし商品価格を後から変更した場合、
過去の注文の金額まで変わってしまったら困ります。
だから、
「注文時点の単価」を order_items.unit_price として保存しておきます。
これにより、
商品価格を変更しても、過去の売上金額は変わらない
「当時はいくらで売っていたか」が正確に追える
という状態を作れます。
これは、売上管理・会計・監査の観点からも非常に重要です。
テーブル同士の関係を頭の中で描いてみる
「線でつなぐと一気に理解が進む」
ここまでで出てきたテーブルを、関係で表すとこうなります。
customers
1人の顧客が
↓
orders
複数の注文を持つ(1対多)
orders
1件の注文が
↓
order_items
複数の明細を持つ(1対多)
order_items
1件の明細が
↓
products
1つの商品を指す(多対1)
頭の中で、
customers → orders → order_items → products
という矢印をイメージできればOKです。
この「テーブル同士の関係を線でつなぐ感覚」が、
売上管理のような実践的な設計ではとても大事になります。
セキュリティ・監査の視点から見たこの設計の良さ
「“後から追える”構造になっているか」
このスキーマには、セキュリティや監査の観点から見ても良い点がいくつかあります。
顧客情報と売上情報が分離されている
顧客を削除せず、ステータスで管理すれば、
「この売上は誰のものだったか」を後から追える。
商品価格の変更と、過去の売上が切り離されているorder_items.unit_price によって、
「当時いくらで売ったか」が保存される。
これはトラブル時や監査時に非常に重要。
注文の状態(status)を持っている
キャンセルされた注文を物理削除せず、
状態で管理すれば、「いつキャンセルされたか」も追える。
こういう「後から追える構造」は、
セキュリティインシデントや不正の調査にも直結します。
Day25 前半のまとめ
売上管理を「顧客×商品×注文」で考えるとき、1テーブルに全部押し込むのではなく、顧客・商品・注文(+注文明細)に分けるのが基本になる。customers は「誰が買ったか」、products は「何を売っているか」、orders は「いつ・誰が注文したか」、order_items は「その注文でどの商品を何個・いくらで売ったか」を表す。order_items.unit_price に「注文時点の単価」を保存しておくことで、商品価格を変更しても過去の売上金額が変わらず、「当時いくらで売ったか」を正確に追える。
テーブル同士の関係を「customers → orders → order_items → products」という矢印でイメージできるようになると、JOIN や集計の設計が一気に楽になる。
後半では、このスキーマに実際にデータを入れて、
顧客ごとの売上合計
商品ごとの売上数・売上金額
特定期間の売上集計
といった「売上管理らしいクエリ」を一緒に書いていきます。
