SQLite | ゼロからはじめるSQL、30日で習得するSQLite:実践 - Day29 総合課題

SQL SQLite
スポンサーリンク

Day29 前半のゴール

「“ECサイト風”の世界を、テーブル設計に落とし込めるようになる」

いよいよ Day29、総合課題です。
テーマは 小規模データベースをゼロから設計(ECサイト風)

前半のゴールはこうです。

ECサイトに必要な“現実の要素”を洗い出す
それをテーブル・カラム・関係(リレーション)に落とし込む
正規化・JOIN・インデックスの知識を「設計の判断」に使えるようになる

ここでは、まだ SQL をガリガリ書くのではなく、
「どういうテーブル構成にするか」をじっくり組み立てていきます。


ECサイトの“現実”を言葉で整理する

「テーブルの前に、“登場人物”と“出来事”をはっきりさせる」

いきなりテーブル名を考え始めると、ほぼ確実に迷子になります。
先に「現実世界で何が起きているか」を言葉で整理します。

ECサイト風の世界には、だいたいこんな要素があります。

ユーザー(お客さん)
商品
カート(買い物途中の状態)
注文(購入が確定したもの)
注文の中の明細(どの商品を何個買ったか)
支払い状況
配送先住所

ここで大事なのは、「テーブル名」ではなく「役割」です。

ユーザーは「誰が」
商品は「何を」
カート/注文は「いつ・何を・いくつ欲しい/買ったか」
配送先は「どこに送るか」

この“現実の登場人物と出来事”を、
あとでテーブルに変換していきます。


まずは“絶対に独立しているもの”からテーブルにする

「ユーザーと商品は、ほぼ迷わず独立テーブル」

最初に決めやすいのは、「それ単体で意味があるもの」です。
EC なら、まずはこの2つ。

ユーザー(users)
商品(products)

これは Day24〜25 の「顧客」「商品」とほぼ同じ発想です。

ユーザーテーブルのイメージはこうです。

CREATE TABLE users (
  id            INTEGER PRIMARY KEY,
  name          TEXT    NOT NULL,
  email         TEXT    NOT NULL UNIQUE,
  password_hash TEXT    NOT NULL,
  registered_at TEXT    NOT NULL,
  status        INTEGER NOT NULL DEFAULT 1
);
SQL

ここでのポイントは、

ログイン用の情報(email, password_hash)
プロフィール的な情報(name, registered_at, status)

を分けて意識することです。
セキュリティ的には、本当は認証情報を別テーブルに分ける設計もありますが、
ここではシンプルな形にしておきます(後で「改善案」として話せるとベスト)。

商品テーブルはこう。

CREATE TABLE products (
  id          INTEGER PRIMARY KEY,
  name        TEXT    NOT NULL,
  description TEXT,
  price       INTEGER NOT NULL,
  status      INTEGER NOT NULL DEFAULT 1
);
SQL

商品は「今いくらで売っているか」が重要なので、price を持たせます。
過去の注文時の価格は、Day25 と同じく「注文明細側にコピーする」方針にします。


カートと注文をどう分けるか

「“まだ買ってない”状態と“買った後”の状態」

ECサイトならではのポイントがここです。
「カート」と「注文」をどう扱うか。

カート
ユーザーが「入れているだけ」で、まだ購入確定していない状態
中身は変わる(追加・削除・数量変更)

注文
購入が確定した状態
お金・在庫・配送など、現実世界の処理が動き始める

この2つは、似ているようで役割が違います。

設計としては、

カート用のテーブル(cart_items)
注文用のテーブル(orders, order_items)

を分けるのが素直です。

カートは「今この瞬間の状態」だけ持てばよく、
注文は「過去の履歴として永遠に残る」ものです。


カートのテーブル設計

「ユーザー×商品×数量、シンプルに割り切る」

カートは、1ユーザーに対して複数の商品が入ります。
なので、「カート明細」だけをテーブルにしてしまうのがシンプルです。

CREATE TABLE cart_items (
  id         INTEGER PRIMARY KEY,
  user_id    INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  quantity   INTEGER NOT NULL
);
SQL

ここでは、あえて「カートID」を作っていません。
ユーザーごとに「今のカートの中身」があればよい、という割り切りです。

この設計の意味はこうです。

1人のユーザーは、複数の cart_items を持てる
1つの cart_items は、「あるユーザーの、ある商品、ある数量」を表す

カートは「途中経過」であり、
注文のように「確定した履歴」ではないので、
これくらいシンプルで十分です。


注文と注文明細のテーブル設計

「Day25 の売上管理を、EC 用に少しリッチにする」

注文は、Day25 でやった「orders」「order_items」とほぼ同じ構造です。
ただし、EC では「配送先」「支払い状態」なども絡んできます。

まずは注文ヘッダ。

CREATE TABLE orders (
  id            INTEGER PRIMARY KEY,
  user_id       INTEGER NOT NULL,
  ordered_at    TEXT    NOT NULL,
  status        INTEGER NOT NULL,      -- 例: 1=受付, 2=発送済, 3=キャンセル
  total_amount  INTEGER NOT NULL,      -- 合計金額(冗長だが利便性のために持つ案もアリ)
  shipping_name TEXT    NOT NULL,
  shipping_zip  TEXT    NOT NULL,
  shipping_addr TEXT    NOT NULL
);
SQL

ここでのポイントは二つです。

ユーザーID(user_id)で「誰の注文か」を紐づける
配送先情報を注文ごとにコピーして持つ

配送先を「ユーザーの住所テーブル」にだけ持たせる設計もありますが、
現実には「今回だけ別の住所に送る」ことも多いので、
注文時点の配送先を orders にコピーしておく方が実務的です。

次に、注文明細。

CREATE TABLE order_items (
  id         INTEGER PRIMARY KEY,
  order_id   INTEGER NOT NULL,
  product_id INTEGER NOT NULL,
  product_name TEXT   NOT NULL,
  unit_price INTEGER NOT NULL,
  quantity   INTEGER NOT NULL
);
SQL

ここで重要なのは、

product_nameunit_price を「注文時点の値」としてコピーしている

という点です。

商品テーブルの nameprice を後から変えても、
過去の注文の内容は変わってはいけません。

だから、

商品テーブル
「今どう売っているか」の情報

注文明細
「そのときどう売ったか」の情報

と役割を分けています。
これは Day25 でやった「売上管理の設計」を、EC に持ってきた形です。


支払い情報・配送情報をどこまで分けるか

「最初は“注文に埋め込む”、あとで“分割案”を考えられればOK」

EC をガチでやると、

支払い方法(クレカ/銀行振込/コンビニ払い…)
支払いステータス(未払い/支払い済み/返金済み…)
配送ステータス(準備中/発送済み/配達完了…)

など、いくらでもテーブルを増やせます。

ただ、Day29 の総合課題としては、
まずは「orders に最低限の状態を持たせる」くらいで十分です。

例えば、

status に「注文全体の状態」を入れる
支払い方法は payment_method カラム(TEXT)で持つ
支払いステータスは payment_status カラム(INTEGER)で持つ

といった形です。

ALTER TABLE orders
  ADD COLUMN payment_method TEXT;

ALTER TABLE orders
  ADD COLUMN payment_status INTEGER NOT NULL DEFAULT 1; -- 1=未払い, 2=支払い済み など
SQL

そのうえで、

「支払い方法をマスタテーブルに分けるならどうする?」
「配送ステータスを別テーブルにした方がいい場面は?」

といった“設計改善の余地”を自分で語れると、
Day26 の正規化の知識とつながってきます。


インデックスをどこに張るかを“設計段階で意識する”

「あとからでも張れるけど、“検索の軸”は最初から考えておく」

Day28 でやったインデックスの話も、
ここで設計に組み込みます。

EC 風のデータベースで、よく使われそうな検索は何か?
それに対応するカラムはどれか?

例えば、

ユーザーをメールアドレスで探す
users.email に UNIQUE(=インデックス)

あるユーザーの注文履歴を出す
orders.user_id にインデックス

期間別に注文を集計する
orders.ordered_at にインデックス

商品別に売上を集計する
order_items.product_id にインデックス

といった具合です。

設計段階で、

「このテーブルは、こういうクエリでよく使われるだろう」
「だから、このカラムはインデックス候補だな」

と考えられるようになると、
パフォーマンスを“後付け”ではなく“設計の一部”として扱えるようになります。


セキュリティの視点から見た EC 風設計のポイント

「どこに“センシティブな情報”があるかを、最初から意識する」

EC 風のデータベースには、
個人情報・認証情報・お金に関わる情報が詰まります。

設計段階で、少なくとも次は意識しておきたいです。

パスワードは平文で持たない(password_hash にする)
クレジットカード番号は原則として DB に保存しない(外部決済サービスを使う前提)
住所・氏名・メールアドレスがどのテーブルにあるかを明確にする
「認証情報」「プロフィール」「注文履歴」を、将来的に分割できるような構造にしておく

Day29 の時点では、
「完全に安全な本番設計」を作る必要はありません。

ただ、

「このカラムはセンシティブだな」
「このテーブルにアクセスできる人は、かなり強い権限が必要だな」

と意識しながら設計できるかどうかで、
エンジニアとしての“目線”が変わってきます。


Day29 前半のまとめ

EC サイト風の世界をテーブルに落とすときは、まず「ユーザー」「商品」「カート」「注文」「注文明細」「配送先」といった“現実の登場人物と出来事”を言葉で整理するところから始める。
ユーザー(users)と商品(products)は独立テーブルにし、注文は「orders(ヘッダ)+order_items(明細)」で表現し、商品名・単価は「注文時点の値」を明細側にコピーしておくことで、過去の履歴が変わらないようにする。
カートは「今の状態」だけ分かればよいので、cart_items(user_id, product_id, quantity) のようなシンプルな構造で十分であり、注文のような“履歴”とは役割を分けて考える。
支払い方法・配送ステータスなどは、最初は orders テーブルのカラムとして持たせておき、後からマスタテーブルに分割する余地を残しておくと、Day26 の正規化の考え方とつながる。
設計段階で「どのカラムが検索の軸になるか(email, user_id, ordered_at, product_id など)」を意識し、インデックス候補として頭に置いておくと、パフォーマンスとセキュリティ(どこにセンシティブ情報があるか)の両面を踏まえた“現実的な設計”に近づく。

後半では、この設計をもとに実際にテーブルを定義し、
サンプルデータを入れて、
「ユーザー別注文履歴」「商品別売上」「日別売上」などのクエリを通して、
“自分で設計したDBを自分で叩く”ところまで一緒にやっていきます。

タイトルとURLをコピーしました