Python | DB・SQL:DB 設計

Python
スポンサーリンク

DB 設計って何?まずはゴールのイメージから

DB 設計は、「アプリで扱う情報を、テーブル・カラム・関係に分解して、矛盾なく・拡張しやすく・速く扱える形にする作業」です。
言い換えると、「現実の世界(ユーザー・商品・注文…)を、データベースの世界にきれいに写し取る作業」です。

ここが雑だと、あとから必ず苦しくなります。
カラムが足りない、同じ情報があちこちに重複する、変更のたびにバグる、などが起きます。
逆に、ここがしっかりしていると、アプリのコードも自然にシンプルになります。

ここでは、初心者でもイメージしやすい「タスク管理アプリ」を例に、
DB 設計の考え方を一歩ずつかみ砕いていきます。


例題:シンプルなタスク管理アプリを題材にする

まずは「何を管理したいか」を言葉で書き出す

いきなりテーブルを考えずに、「アプリとして何をしたいか」を日本語で書き出します。

ユーザーがいる
ユーザーはタスクをいくつも持てる
タスクにはタイトルと説明と締切日がある
タスクには完了フラグがある

このくらいの粒度で十分です。
ここから「登場人物」と「関係」を抜き出します。

登場人物(エンティティ)
ユーザー
タスク

関係
ユーザー 1 人は、複数のタスクを持てる(1 対多)

この「エンティティ」と「関係」が、テーブルとリレーションの元になります。

エンティティをテーブルに落とす

ユーザー → users テーブル
タスク → tasks テーブル

という対応を考えます。
まずは素直に、こんなテーブルをイメージします。

CREATE TABLE users (
    id       SERIAL PRIMARY KEY,
    name     TEXT NOT NULL,
    email    TEXT NOT NULL UNIQUE
);

CREATE TABLE tasks (
    id          SERIAL PRIMARY KEY,
    user_id     INTEGER NOT NULL,
    title       TEXT NOT NULL,
    description TEXT,
    due_date    DATE,
    is_done     BOOLEAN NOT NULL DEFAULT FALSE
);
SQL

ここで重要なのは、tasks.user_id です。
「このタスクはどのユーザーのものか」を表す外部キーになります。


主キーと外部キーをしっかり理解する

主キー(PRIMARY KEY)は「その行を一意に特定する ID」

users.id や tasks.id のようなカラムが主キーです。
役割はシンプルで、「このテーブルの中で、この行を一意に識別するための番号」です。

Python から見ると、主キーがあることで、

特定のユーザーを 1 人だけ取る
特定のタスクを 1 件だけ更新する

といった操作がやりやすくなります。

SELECT * FROM users WHERE id = 1;
UPDATE tasks SET is_done = TRUE WHERE id = 10;
SQL

主キーは、基本的に「変わらない値」にするのが鉄則です。
名前やメールアドレスを主キーにすると、変更が大変になるので避けます。

外部キー(FOREIGN KEY)は「別テーブルとのつながり」

tasks.user_id は、「このタスクは users テーブルのどの行に属するか」を表すカラムです。
SQL としては、こう定義できます。

CREATE TABLE tasks (
    id          SERIAL PRIMARY KEY,
    user_id     INTEGER NOT NULL REFERENCES users(id),
    title       TEXT NOT NULL,
    ...
);
SQL

これで、「tasks.user_id は必ず users.id のどれかでなければならない」という制約が付きます。
つまり、「存在しないユーザーに紐づくタスク」は作れなくなります。

DB 設計では、

各テーブルに主キーを必ず持たせる
テーブル同士の関係は外部キーで表現する

という 2 点がとても重要です。


正規化の基本を「やりがちな失敗」から理解する

失敗例:1 テーブルに全部詰め込む

初心者がやりがちなのが、「とりあえず 1 テーブルに全部入れる」パターンです。

CREATE TABLE users_and_tasks (
    user_id     INTEGER,
    user_name   TEXT,
    user_email  TEXT,
    task_id     INTEGER,
    task_title  TEXT,
    task_done   BOOLEAN
);
SQL

一見シンプルですが、すぐに問題が出ます。

同じユーザーにタスクが 3 つあると、ユーザー情報が 3 行に重複する
ユーザー名を変更したいとき、3 行全部を更新しないといけない
タスクがないユーザーを表現しづらい

こういう「同じ情報が何度も出てくる」「一箇所の変更が複数行にまたがる」状態を避けるのが、正規化の基本的な考え方です。

分けるべきものはテーブルに分ける

さっきの users と tasks のように、

ユーザーの情報は users テーブルに 1 行だけ
タスクの情報は tasks テーブルに 1 行ずつ
両者の関係は user_id で表現

という形に分けることで、

ユーザー名を変えたいときは users の 1 行だけ更新すればよい
タスクを追加しても、ユーザー情報は重複しない

という「スッキリした状態」になります。

正規化を難しく考えすぎなくてよくて、
初心者のうちはまず、

同じ意味の情報を、複数の場所に重複して持たない
「もの」と「ものの関係」は分けて考える

この 2 つを意識するだけで、かなりマシな設計になります。


DB 設計から Python コードへのつながりをイメージする

テーブル設計がそのまま ORM モデルになる

さっきの users / tasks を SQLAlchemy のモデルにすると、こうなります。

from sqlalchemy import Column, Integer, String, Boolean, Date, ForeignKey
from sqlalchemy.orm import declarative_base, relationship

Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String, nullable=False)
    email = Column(String, nullable=False, unique=True)

    tasks = relationship("Task", back_populates="user")

class Task(Base):
    __tablename__ = "tasks"

    id = Column(Integer, primary_key=True, autoincrement=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    title = Column(String, nullable=False)
    description = Column(String)
    due_date = Column(Date)
    is_done = Column(Boolean, nullable=False, default=False)

    user = relationship("User", back_populates="tasks")
Python

DB 設計で決めたことが、そのままコードに反映されています。

users テーブル → User クラス
tasks テーブル → Task クラス
主キー → primary_key=True
外部キー → ForeignKey(“users.id”)
1 対多の関係 → relationship

つまり、「DB 設計がきれいだと、ORM モデルもきれいになる」ということです。

クエリも自然な形になる

この設計だと、Python からの操作も自然になります。

ユーザーを作る:

user = User(name="Taro", email="taro@example.com")
session.add(user)
session.commit()
Python

タスクを作る:

task = Task(
    user=user,
    title="レポートを書く",
    description="金曜までに提出",
)
session.add(task)
session.commit()
Python

ユーザーとそのタスクを読む:

u = session.query(User).filter_by(email="taro@example.com").first()
for t in u.tasks:
    print(t.title, t.is_done)
Python

この「コードが自然に書けるかどうか」は、
元の DB 設計が素直かどうかにかなり依存します。


DB 設計で特に意識してほしいポイント

「今」だけでなく「少し先」も想像する

最初から完璧な設計は無理ですが、
「ちょっと先の拡張」を想像しておくと、後悔が減ります。

例えば、タスク管理アプリで、

今は「個人用」だけど、将来「チームで共有」したくなるかもしれない
今は「完了フラグ」だけど、将来「ステータス(未着手・進行中・完了)」にしたくなるかもしれない

こういう未来を少しだけ意識しておくと、

ステータスを boolean ではなく文字列や別テーブルにする
ユーザーとチームの関係を表すテーブルを用意しておく

といった設計の選択肢が見えてきます。

「1 テーブルに詰め込みすぎない」「分けすぎもしない」

何でもかんでも 1 テーブルに詰め込むと、重複と矛盾だらけになります。
逆に、細かく分けすぎると、JOIN だらけで扱いづらくなります。

初心者のうちは、

明らかに「別のもの」はテーブルを分ける(ユーザーとタスク、商品と注文など)
「同じものの属性」は 1 テーブルにまとめる(ユーザーの名前・メール・パスワードなど)

このくらいの感覚で十分です。


まとめ(DB 設計は「現実をきれいに分解して、矛盾なくつなぐ作業」)

DB 設計を初心者目線で整理すると、こうなります。

まず「アプリで扱うもの(エンティティ)」と「それらの関係」を日本語で整理する。
エンティティごとにテーブルを作り、各テーブルに主キーを持たせる。
テーブル同士の関係は外部キーで表現し、「1 対多」「多対多」を意識して設計する。
同じ情報を複数の場所に重複させないように分ける(正規化の基本)。
その設計が、そのまま ORM モデルや Python コードの形に直結する。

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