SQL:テーブル設計とCREATE TABLE
1学習の目的
- CREATE TABLEでテーブルそのものを作れるようになる。ここまでは既にあるテーブルを使う側だったが、ついに設計する側に回る。
- 制約(NOT NULL・UNIQUE・CHECK・外部キー)を使い、不正なデータが入らない仕組みを作れるようになる。「入れられてから直す」のではなく「そもそも入れさせない」設計ができるようになる。
2基礎解説
| やりたいこと | 書き方 | 効果 |
|---|---|---|
| 主キーにする | id INTEGER PRIMARY KEY | 重複不可・自動採番 |
| 空を禁止する | name TEXT NOT NULL | NULLの挿入を拒否 |
| 重複を禁止する | email TEXT UNIQUE | 同じ値を2回入れられない |
| 初期値を決める | status TEXT DEFAULT '有効' | 指定しないと自動で入る |
- 主キー(PRIMARY KEY)は、その行を一意に特定できる値。products の id がまさにこれで、重複できず、SQLiteでは省略すると自動採番される。
- 制約は「入れさせない」ための仕組み。NOT NULL は空を、UNIQUE は重複を、CHECK は条件を満たさない値を、それぞれ挿入の時点で拒否する。データが入った後に直すより確実。
- FOREIGN KEY(外部キー)は別テーブルのIDを参照する列に付ける制約。orders.customer_id が customers.id に存在しない値を指すことを防げる。
- ⚠ SQLiteは型を厳密にチェックしない。price INTEGER と定義した列に文字列を入れてもエラーにならないことがある。MySQLやPostgreSQLはより厳格。「型を書いたから安全」とは限らない。
- テーブルを消すのは DROP TABLE。WHEREで絞れず、テーブルごと消える。DELETEより影響範囲が大きい、最も慎重を要する操作。
現場使用例:新機能のためのテーブル設計、会員登録フォームの裏側にあるNOT NULL制約、メールアドレスの重複登録防止(UNIQUE)、注文が実在する商品を指しているかの保証(FOREIGN KEY)。
-- 基本のテーブル作成
CREATE TABLE members (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
status TEXT DEFAULT '有効',
age INTEGER CHECK (age >= 0)
);
-- NOT NULL違反:エラーになる
INSERT INTO members (name) VALUES (NULL);
-- → NOT NULL constraint failed
-- UNIQUE違反:同じメールを2回登録できない
INSERT INTO members (name, email) VALUES ('田中', 'a@b.com');
INSERT INTO members (name, email) VALUES ('鈴木', 'a@b.com');
-- → UNIQUE constraint failed
-- CHECK違反:条件を満たさない値は拒否される
INSERT INTO members (name, age) VALUES ('佐藤', -5);
-- → CHECK constraint failed
-- 外部キー:存在しない親を参照できない
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
member_id INTEGER REFERENCES members(id)
);
INSERT INTO orders (member_id) VALUES (999); -- membersに無いid
-- → FOREIGN KEY constraint failed
-- テーブルを削除する(WHEREでは絞れない)
DROP TABLE orders;
3基本ドリル(10問)
itemsテーブルが作成される
CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER);
CREATE TABLE items ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER );
これがテーブル設計の最小形。列名・型・制約をカンマで区切って並べる。id を PRIMARY KEY にすると、SQLiteでは自動採番の主キーになる。
A. 自動で空文字が入る B. エラーになり挿入が拒否される C. 0が入る D. 警告だけ出て挿入される 選択★☆☆無料
解答 == B
NOT NULL は「空を許さない」制約。
B
制約違反はエラーとして即座に拒否される。データが不正な状態のままテーブルに入ることはない。
membersテーブルが作成される
email TEXT UNIQUE のように、型の後ろに制約を書く。
CREATE TABLE members ( id INTEGER PRIMARY KEY, email TEXT UNIQUE );
UNIQUE を付けると、同じ値を2回挿入できなくなる。会員登録でメールアドレスの重複を防ぐのに使われる、実務で最も身近な制約の1つ。
CREATE TABLE members ( id INTEGER PRIMARY KEY, status TEXT ____ '有効' );
membersテーブルが作成される
「既定値」を意味する7文字のキーワード。
DEFAULT
DEFAULT を指定すると、INSERTで値を省略したときに自動で入る。会員登録時にステータスを毎回書かなくても「有効」が入る。
A. DELETE TABLE B. DROP TABLE C. REMOVE TABLE D. CLEAR TABLE 選択★☆☆無料
解答 == B
「捨てる」という意味の英単語。
B
DROP TABLE はテーブルの構造ごと消す。DELETEは行だけを消すが、DROPは列の定義もテーブル自体も消える。WHEREでは絞れない。
itemsテーブルが作成される
price INTEGER CHECK (price >= 0) のように書く。
CREATE TABLE items ( id INTEGER PRIMARY KEY, price INTEGER CHECK (price >= 0) );
CHECK は任意の条件を指定できる制約。負の価格や、範囲外の年齢など、業務ルールをテーブルの定義そのものに埋め込める。
CREATE TABLE orders ( id INTEGER PRIMARY KEY, customer_id INTEGER ____ customers(id) );
ordersテーブルが作成される
「参照する」を意味する9文字のキーワード。
REFERENCES
REFERENCES で外部キーを設定する。これにより、存在しない顧客IDを注文に登録することを防げる(外部キー制約が有効な設定の場合)。
A. 必ずエラーになる B. 型を厳密にチェックしないため挿入されてしまうことがある C. 自動的に0に変換される D. 自動的にTEXT型に列が変わる 選択★★☆無料
解答 == B
SQLiteの型システムは他のDBと比べて緩い。
B
SQLiteは「動的型付け」と呼ばれる方式で、型の定義があっても厳密には強制しない。MySQLやPostgreSQLではエラーになることが多く、環境によって挙動が違う点に注意が必要。
inventoryテーブルが作成される
CHECKとDEFAULTを両方、stock列に付ける。順番は CHECK と DEFAULT のどちらが先でもよい。
CREATE TABLE inventory ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, stock INTEGER DEFAULT 0 CHECK (stock >= 0) );
1つの列に複数の制約を重ねられる。DEFAULTで「省略時は0」、CHECKで「負の値は拒否」——2つを組み合わせることで、在庫が常に0以上の整数であることが保証される。
logsテーブルが削除される
DROP TABLE logs; と書く。
DROP TABLE logs;
DROP TABLEはバックアップが無い限り取り消せない。実務では「本当にこのテーブルを消してよいか」を必ず複数人で確認してから実行する。
4実践シナリオ(5問)
membersテーブルが作成される
各列に適切な制約を1つずつ組み合わせる。
CREATE TABLE members ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, joined_at TEXT );
会員登録フォームの裏側で動いているテーブルそのもの。名前を必須にし、メールを一意にする——これだけで「名前未入力」「メール重複登録」という2つの事故を仕組みで防げる。
order_itemsテーブルが作成される
quantity に CHECK と DEFAULT を組み合わせる。
CREATE TABLE order_items ( id INTEGER PRIMARY KEY, order_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity INTEGER DEFAULT 1 CHECK (quantity >= 1) );
数量が0や負の値になることを、テーブル自体が拒否する。アプリ側のチェックが漏れていても、データベースが最後の砦として守ってくれる。
reviewsテーブルが作成される
CHECK (rating >= 1 AND rating <= 5) のように範囲を指定する(SQL03のANDを思い出す)。
CREATE TABLE reviews ( id INTEGER PRIMARY KEY, product_id INTEGER REFERENCES products(id), rating INTEGER CHECK (rating >= 1 AND rating <= 5), comment TEXT );
SQL03で学んだANDが、CHECK制約の中でも使える。星1〜5段階のレビュー機能は、まさにこの制約で不正な評価点を防いでいる。
SQLiteでは挿入が成功する(型が緩いため)
CREATE TABLEしてからINSERTする。2本のSQLを書く。
CREATE TABLE items (
id INTEGER PRIMARY KEY,
price INTEGER
);
INSERT INTO items (price) VALUES ('abc');
SQLiteではこれが成功してしまうことがある。型を書いたからといって、それだけでデータの正しさは保証されない。CHECK制約やアプリ側の検証が別途必要になる理由がここにある。
products_v2テーブルが作成される
5つの列それぞれに適切な制約を組み合わせる。price は NOT NULL と CHECK の両方を付ける。
CREATE TABLE products_v2 ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT DEFAULT '未分類', price INTEGER NOT NULL CHECK (price >= 0), stock INTEGER DEFAULT 0 CHECK (stock >= 0) );
これまで扱ってきた products テーブルより、はるかに堅牢な設計になっている。SQL05で悩まされたNULLだらけのデータは、最初からこの制約があれば生まれなかった——制約は「後から直す」よりも「最初から防ぐ」ための道具だと分かる。
5仕上げ課題
books テーブル
・id(主キー)
・title(必須)
・author(必須)
・isbn(重複不可、TEXT)
・stock(0以上、初期値1)
rentals テーブル
・id(主キー)
・book_id(INTEGER、必須、books.id を参照)
・renter_name(必須)
・status(TEXT、初期値「貸出中」、'貸出中' または '返却済' のいずれか)
2つのテーブルを作ったあと、動作確認として次の2件を挿入せよ:
① books に「SQL入門」(author:'山田太郎', isbn:'978-001', stock:3)
② rentals に book_id:1(①で作った本のid)、renter_name:'田中'
最後に SELECT title, renter_name, status FROM rentals JOIN books ON rentals.book_id = books.id; で確認すること。
期待される確認結果:
title | renter_name | status
SQL入門 | 田中 | 貸出中
確認SELECTの結果が一致する
status の '貸出中' or '返却済' は CHECK (status IN ('貸出中', '返却済')) と書ける(SQL02のIN)。rentals の book_id は REFERENCES books(id)。INSERT時、booksのidは自動採番なので1になる想定で book_id に 1 を指定する。
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
isbn TEXT UNIQUE,
stock INTEGER DEFAULT 1 CHECK (stock >= 0)
);
CREATE TABLE rentals (
id INTEGER PRIMARY KEY,
book_id INTEGER NOT NULL REFERENCES books(id),
renter_name TEXT NOT NULL,
status TEXT DEFAULT '貸出中' CHECK (status IN ('貸出中', '返却済'))
);
INSERT INTO books (title, author, isbn, stock)
VALUES ('SQL入門', '山田太郎', '978-001', 3);
INSERT INTO rentals (book_id, renter_name)
VALUES (1, '田中');
SELECT title, renter_name, status
FROM rentals
JOIN books ON rentals.book_id = books.id;
ここまで学んだ制約がすべて1つの設計に集約された。UNIQUE(ISBNの重複防止)、CHECK(在庫と状態の範囲制限)、DEFAULT(初期値)、REFERENCES(存在する本だけ貸し出せる)、NOT NULL(必須項目の保証)。
特に status の CHECK (status IN ('貸出中', '返却済')) に注目してほしい。SQL02で学んだIN演算子が、SELECTの条件だけでなくCHECK制約の中でも使える。これにより「貸出中でも返却済でもない謎のステータス」がテーブルに入り込む余地が最初から無い。
この章で学んだ設計思想は、「アプリのコードでチェックする」より「データベースの制約で防ぐ」ほうが確実だということだ。アプリのコードにはバグが混ざりうるし、複数のアプリから同じデータベースを触ることもある。制約はデータベース自身が守ってくれるので、どの経路から書き込まれても不正なデータは入らない。
次章ではインデックスを学ぶ。テーブルの構造を決めるここまでの内容から一歩進み、大量のデータでも高速に検索できる仕組みを扱う。