第1部 データベースの基礎
第2章 テーブル設計の基本 — 主キー・外部キー・リレーション
リレーショナルデータベースの心臓部は「表と表の関係」です。行を一意に特定する主キー、表同士をつなぐ外部キー、そして設計図であるER図。SQLを書き始める前に、この土台をしっかり固めます。
🎯 この章で学ぶこと
- テーブル・行・列の構造と、SQLiteの主なデータ型(INTEGER・TEXT・REAL・BLOB)
- NULLとは何か —「空文字」でも「0」でもない、「値がない」という状態
- 主キーの役割と、「氏名を主キーにしてはいけない」理由。自然キーとサロゲートキー
- 外部キーで表現する1対多の関係、中間テーブルで表現する多対多の関係
- ER図の読み方と、CREATE TABLEの制約(NOT NULL・UNIQUE・DEFAULT・CHECK・FOREIGN KEY)
2.1 テーブル・行・列とデータ型
第1章で見たとおり、リレーショナルデータベースはすべてのデータをテーブル(表)で持ちます。行(レコード)が1件のデータ、列(カラム)がデータの項目です。Excelと決定的に違うのは、列ごとに「入れられるデータの種類(データ型)」をあらかじめ決めておくことです。Excelのセルには数値でも文字でも日付でも何でも入りますが、データベースでは「salary列は整数」と宣言したら、原則としてそこには整数しか入りません。この「入り口の厳しさ」が整合性の第一歩です。
SQLiteの主なデータ型は次の4つです。
| 型 | 意味 | 例 |
|---|---|---|
| INTEGER | 整数 | 社員ID、在庫数、給与(円単位) |
| TEXT | 文字列 | 氏名、メールアドレス、'2024-04-01' のような日付文字列 |
| REAL | 浮動小数点数(小数) | 身長 172.5、割引率 0.08 |
| BLOB | バイナリデータ | 画像や添付ファイルそのもの(実務では避けることが多い) |
MySQLやPostgreSQLには VARCHAR(50)(最大50文字の文字列)、DATE(日付専用)、DECIMAL(金額計算向けの正確な小数)など、より細かい型があります。SQLiteは日付専用型を持たないため、このコースでは日付を '2024-04-01' という形式のTEXTで扱います。「列に型を決める」という考え方自体はどのDBMSでも共通です。
NULL —「値がない」という特別な状態
どの型の列にも入りうる特別な状態が NULL(ヌル)です。NULLは「0」でも「空文字」でもなく、「値がまだない・分からない」という状態そのものを表します。たとえば「配属先が未定の新入社員」の部署IDは、0でも空文字でもなくNULLであるべきです。0なら「0番という部署」に見えてしまい、空文字は数値列にはそもそも入れられません。
NULLは「値がない」状態なので、= NULL のような普通の比較では判定できません(第3章で IS NULL という専用の書き方を学びます)。NULLを軽く見ていると集計結果が狂う事故につながるため、設計段階から「この列はNULLを許すのか?」を意識してください。
2.2 主キー — 行を一意に特定する
テーブルには「この値を見れば行が1つに決まる」列が必要です。これを主キー(PRIMARY KEY)と呼びます。「社員番号3番の給与を更新して」「注文ID 1052をキャンセルして」——データベースへの依頼は、必ず主キーで対象を指定します。主キーがなければ、更新も削除も「どの行のことか」を確実に指せません。
なぜ「氏名」を主キーにしてはいけないのか
初学者が最初にやりがちな設計が「氏名を主キーにする」ことです。これは2つの理由で破綻します。
- 同姓同名 — 「田中 実」さんが2人入社した瞬間、主キーの「一意」という約束が守れなくなります。数百人規模の会社なら同姓同名は普通に起きます。
- 改姓 — 結婚などで姓が変わると主キーの値を書き換えることになります。主キーは他のテーブルから参照される「住所」のようなものなので、変更するとすべての参照先を追いかけて直す羽目になります。
つまり主キーに向くのは、①絶対に重複せず、②一度決めたら変わらない値です。氏名はどちらの条件も満たしません。
自然キーとサロゲートキー
主キーの選び方には2つの流儀があります。
| 種類 | 説明 | 例 | 注意点 |
|---|---|---|---|
| 自然キー | 業務上もともと存在する識別子をそのまま主キーにする | 社員番号、ISBN、商品コード | 「変わらない」と思っていた番号が制度変更で変わることがある |
| サロゲートキー(代理キー) | 業務とは無関係の連番をデータベース側で振る | id列(1, 2, 3, …) | 業務的な意味を持たないため、値そのものからは何も読み取れない |
実務では「意味を持たない連番なら絶対に変更する理由が生まれない」という安心感から、サロゲートキーが広く使われます。SQLiteでは INTEGER PRIMARY KEY と宣言した列に、値を指定せずINSERTすると自動で連番が振られます。
sqlite> CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
sqlite> INSERT INTO members (name) VALUES ('佐藤 花子');
sqlite> INSERT INTO members (name) VALUES ('田中 実');
sqlite> SELECT * FROM members;
1|佐藤 花子
2|田中 実
idを一切書いていないのに、1、2と自動で採番されました。MySQLの AUTO_INCREMENT、PostgreSQLの SERIAL も同じ発想の機能です。
2.3 外部キーとリレーション
第1章の社員テーブルには dept_id という列がありました。これは「部署テーブル(departments)のid列の値を入れる」列で、このように他のテーブルの主キーを参照する列を外部キー(FOREIGN KEY)と呼びます。
部署1つに対して社員は何人もいます。逆に、1人の社員が所属する部署は1つです。このような関係を1対多(いちたいた)の関係と呼び、リレーショナルデータベースで最も頻出する形です。ポイントは、外部キーは必ず「多」の側に置くこと。社員(多)の側に dept_id を持たせるのであって、部署の側に「社員1, 社員2, 社員3…」と列を増やすのではありません。
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
dept_id INTEGER REFERENCES departments(id) -- 外部キー
);
歴史的な事情で、SQLiteは外部キー制約のチェックがデフォルトで無効です。接続するたびに PRAGMA foreign_keys = ON; を実行して有効化してください。有効にすると、「存在しない部署IDを持つ社員」の登録をDBMSがエラーで拒否してくれます。MySQLやPostgreSQLでは最初から有効です。
sqlite> PRAGMA foreign_keys = ON;
sqlite> INSERT INTO departments VALUES (10, '総務部');
sqlite> INSERT INTO employees VALUES (1, '佐藤 花子', 10); -- 総務部は存在するのでOK
sqlite> INSERT INTO employees VALUES (2, '鈴木 一郎', 99); -- 99番という部署は存在しない
Runtime error: FOREIGN KEY constraint failed (19)
存在しない部署への配属が、入り口で止められました。これが外部キー制約の価値です。「あとで気づいて直す」のではなく、間違ったデータをそもそも入れないのです。
多対多の関係と中間テーブル
では「社員とプロジェクト」の関係はどうでしょう。1人の社員は複数のプロジェクトを掛け持ちし、1つのプロジェクトには複数の社員が参加します。これは多対多の関係で、外部キー1本では表現できません。そこで、2つのテーブルの間に「組み合わせだけを記録するテーブル」を挟みます。これを中間テーブル(関連テーブル)と呼びます。
CREATE TABLE projects (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
-- 中間テーブル:「誰が」「どのプロジェクトに」参加しているかの組み合わせ
CREATE TABLE employee_projects (
employee_id INTEGER NOT NULL REFERENCES employees(id),
project_id INTEGER NOT NULL REFERENCES projects(id),
PRIMARY KEY (employee_id, project_id)
);
「佐藤さんが基幹システム更改とWebサイト刷新に参加している」なら、中間テーブルに (1, 1) (1, 2) という2行を入れるだけです。多対多は「1対多を2つ組み合わせたもの」に分解できる——この発想は、受講者と講座、商品と注文など、実務のあらゆる場面で再登場します。
2.4 ER図を読む・描く
テーブル同士の関係を図にしたものが ER図(Entity-Relationship Diagram)です。実務では設計書の中心になる図で、「読める」ことはIT部門の必須スキルです。ここまでの社員・部署・プロジェクトの関係をテキストで描くと、こうなります。
+-------------+ +--------------+ +--------------------+ +-----------+
| departments | | employees | | employee_projects | | projects |
+-------------+ +--------------+ +--------------------+ +-----------+
| id (PK) |─────────<| id (PK) |─────────<| employee_id (FK) |>─────────| id (PK) |
| name | 1 多| name | 1 多| project_id (FK) |多 1 | name |
+-------------+ | dept_id (FK) | +--------------------+ +-----------+
+--------------+
読み方のポイントは線の両端です。─< のように線が枝分かれしている側(鳥の足に見えるので「カラスの足」と呼ばれます)が「多」、枝分かれしていない側が「1」です。上の図は左から「1つの部署に多くの社員」「1人の社員に多くの参加記録」「多くの参加記録が1つのプロジェクトに属する」と読めます。PKは主キー、FKは外部キーの略です。
「1つの◯◯には複数の△△がある。1つの△△が属する◯◯は1つ」と声に出して言えれば、それは1対多です。両方向とも「複数」なら多対多で、中間テーブルの出番です。図を描く前に日本語で関係を言語化する——これが遠回りに見えて最短の設計手順です。
2.5 CREATE TABLEで設計を形にする — 制約という防波堤
設計をSQLに落とすとき、主キー・外部キー以外にも「間違ったデータを入り口で止める仕組み」を宣言できます。これらを総称して制約(constraint)と呼びます。
| 制約 | 意味 | 使いどころの例 |
|---|---|---|
| NOT NULL | NULLを禁止する(必須項目にする) | 氏名、商品名など「無いと業務が成り立たない」列 |
| UNIQUE | 重複を禁止する | メールアドレス、社員番号(サロゲートキー併用時) |
| DEFAULT | 値を省略したときの既定値を決める | 登録日時、ステータスの初期値 '在職' |
| CHECK | 条件式を満たす値だけ許可する | 給与は0以上、評価は1〜5のみ |
| FOREIGN KEY | 参照先テーブルに存在する値だけ許可する | dept_id は departments に実在する部署のみ |
全部入りの例を見てみましょう。第3章から使う社員テーブルの「制約強化版」です。
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
dept_id INTEGER REFERENCES departments(id),
hire_date TEXT DEFAULT (date('now')),
salary INTEGER CHECK (salary >= 0)
);
制約に違反するデータを入れようとすると、DBMSが即座に拒否します。
sqlite> INSERT INTO employees (id, name, salary) VALUES (7, '山本 剛', -500);
Runtime error: CHECK constraint failed: salary >= 0 (275)
sqlite> INSERT INTO employees (id, salary) VALUES (8, 300000);
Runtime error: NOT NULL constraint failed: employees.name (1299)
「入力画面でチェックしているからDBの制約は要らない」という意見を現場で聞くことがあります。しかしデータベースには、Web画面以外にもバッチ処理・データ移行ツール・管理者の手作業SQLなど、複数の経路からデータが入ってきます。すべての経路が最後に通る場所はデータベースだけです。だから最後の防波堤としての制約には、アプリのチェックとは別の価値があります。
テーブル設計は、セキュリティ対策の最初の分岐点でもあります。「あとで使うかもしれないから」と、氏名・住所・電話番号・生年月日・家族構成……を1つのテーブルにとりあえず全部入れる設計は、漏えい事故が起きたときの被害を最大化します。逆に、業務に本当に必要な列だけを持ち、機微な情報(たとえば健康情報)は別テーブルに分離してアクセス経路を絞っておけば、同じ侵入を許しても漏れる範囲は小さくなります。個人情報保護の世界ではこれを「データ最小化」と呼びます。持っていないデータは絶対に漏れない——設計者が使える最強のセキュリティ対策は、実は「列を増やさない勇気」なのです。
まとめ
- 列にはデータ型を宣言する。SQLiteの主要型はINTEGER・TEXT・REAL・BLOBで、日付はTEXTで扱う。NULLは「0」でも「空文字」でもなく「値がない」状態
- 主キーは行を一意に特定する値。「重複せず、変わらない」ことが条件で、氏名は同姓同名・改姓のため不適格。実務では連番のサロゲートキーが定番
- 外部キーは他テーブルの主キーを参照する列で、1対多の「多」の側に置く。SQLiteでは PRAGMA foreign_keys = ON を忘れない
- 多対多は中間テーブルを挟んで1対多×2に分解する。ER図は「カラスの足」の側が多
- NOT NULL・UNIQUE・DEFAULT・CHECK・FOREIGN KEYの制約は「間違ったデータを入り口で止める」最後の防波堤
- 個人情報の列は必要最小限に。持っていないデータは漏れない
練習問題
ある会社で「社員のメールアドレスを主キーにしよう。全員違う値だし、必ず持っているから」という提案が出ました。この設計の問題点を、2.2節の「主キーに向く値の2条件」に照らして説明してください。
解答を見る
2条件のうち「①重複しない」は当面満たしますが、「②変わらない」を満たしません。改姓・部署異動・ドメイン変更などでメールアドレスは普通に変わります。主キーは他テーブルの外部キーから参照されるため、値を変更するとすべての参照先を更新する必要があり、更新漏れは即データ不整合につながります。また、退職者のアドレスが将来別人に再割り当てされると「①重複しない」も破られます。メールアドレスは UNIQUE 制約付きの通常の列にして、主キーは意味を持たない連番(サロゲートキー)にするのが安全です。
図書室の貸出管理システムを設計します。管理したい情報は「書籍(タイトル・著者)」「利用者(氏名・所属部署名)」「貸出(誰が・どの本を・いつ借りて・いつ返したか)」です。必要なテーブルと、それぞれの主キー・外部キーを日本語で挙げ、テーブル間の関係(1対多)を説明してください。
解答を見る
解答例:テーブルは3つです。
- books(書籍) — 主キー id。列はタイトル、著者
- users(利用者) — 主キー id。列は氏名、所属部署名
- loans(貸出) — 主キー id。外部キー book_id(books.idを参照)と user_id(users.idを参照)。列は貸出日、返却日(未返却ならNULL)
関係は「1冊の書籍に多くの貸出記録」「1人の利用者に多くの貸出記録」という1対多が2本です。書籍と利用者は貸出テーブルを介した多対多の関係になっており、loansが実質的な中間テーブル(+貸出日などの付加情報)の役割を果たしています。「返却日がNULL=いま貸出中」と表現できるのが、NULLの正しい使いどころです。
問題2-2の設計を、制約付きのCREATE TABLE文にしてください。条件:タイトル・著者・氏名は必須。貸出日は省略したら当日の日付。外部キー制約を必ず付けること。書けたらsqlite3で実際に実行し、「存在しない書籍IDの貸出」がエラーになることを確かめてください。
解答を見る
解答例:
PRAGMA foreign_keys = ON;
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL
);
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
dept_name TEXT
);
CREATE TABLE loans (
id INTEGER PRIMARY KEY,
book_id INTEGER NOT NULL REFERENCES books(id),
user_id INTEGER NOT NULL REFERENCES users(id),
loan_date TEXT NOT NULL DEFAULT (date('now')),
return_date TEXT
);
動作確認の例:
sqlite> INSERT INTO books VALUES (1, 'SQL実践入門', '技術 太郎');
sqlite> INSERT INTO users VALUES (1, '佐藤 花子', '総務部');
sqlite> INSERT INTO loans (book_id, user_id) VALUES (1, 1); -- 正常に登録できる
sqlite> INSERT INTO loans (book_id, user_id) VALUES (999, 1); -- 存在しない書籍
Runtime error: FOREIGN KEY constraint failed (19)
存在しない書籍の貸出が入り口で止まれば成功です。return_date にNOT NULLを付けていない(貸出中はNULLになる)点にも注目してください。