- SQL
CREATE TABLEの書き方|主キー・外部キー・制約を例で解説
CREATE TABLEとは、テーブルの名前・列・データ型・制約を決めて、新しいテーブルを作るSQL文です。 基本形は CREATE TABLE テーブル名 (列名 データ型 制約, ...); で、列の定義をカンマで区切って並べます。
この記事では、会員・商品・注文・注文明細の4テーブルを1つのサンプルとして、6種類の制約(PRIMARY KEY・FOREIGN KEY・UNIQUE・NOT NULL・CHECK・DEFAULT)を通しで書きます。さらに、制約に違反するデータを入れたときのエラーメッセージを、MySQL 8.0.46 と PostgreSQL 16.15 の両方で実際に出して並べました。エラー文を見て「どの制約に当たったのか」をすぐ判断できるようになります。
MySQL 8.0 は2026年4月に公式サポートが終了しています。この記事の手順は MySQL 8.4/9.7 でも同じように使えます(違いがある箇所は本文で明記します)。検証は手元の 8.0.46 で行いました。
CREATE TABLEの早見表
基本構文
CREATE TABLE テーブル名 (
列名 データ型 [列制約 ...],
列名 データ型 [列制約 ...],
[CONSTRAINT 制約名] テーブル制約,
...
);
6種類の制約の早見表
| 制約 | 役割 | 列制約の書き方 | テーブル制約の書き方 |
|---|---|---|---|
| PRIMARY KEY(主キー) | 行を1つに特定する。重複もNULLも入らない。1テーブルに1つ | id BIGINT PRIMARY KEY |
PRIMARY KEY (id) |
| FOREIGN KEY(外部キー) | 別テーブルに存在する値だけを入れられる | member_id BIGINT REFERENCES members (id)(MySQLでは効かない。後述) |
FOREIGN KEY (member_id) REFERENCES members (id) |
| UNIQUE(一意) | 重複した値を入れられない。NULLは複数入る | email VARCHAR(255) UNIQUE |
UNIQUE (email) |
| NOT NULL | NULL を入れられない | name VARCHAR(100) NOT NULL |
(列制約だけ) |
| CHECK | 条件式を満たす値だけを入れられる | price INT CHECK (price >= 0) |
CHECK (start_on <= end_on) |
| DEFAULT | 値を省略したときの初期値 | status VARCHAR(20) DEFAULT 'active' |
(列ごとに指定) |
違反したときのエラー早見表
| 制約 | MySQL 8.0 | PostgreSQL 16 |
|---|---|---|
| PRIMARY KEY | ERROR 1062: Duplicate entry '1' for key 'members.PRIMARY' |
duplicate key value violates unique constraint "pk_members" |
| UNIQUE | ERROR 1062: Duplicate entry '...' for key 'members.uq_members_email' |
duplicate key value violates unique constraint "uq_members_email" |
| NOT NULL | ERROR 1048: Column 'name' cannot be null |
null value in column "name" of relation "members" violates not-null constraint |
| CHECK | ERROR 3819: Check constraint 'ck_members_status' is violated. |
new row for relation "members" violates check constraint "ck_members_status" |
| FOREIGN KEY(子の追加) | ERROR 1452: Cannot add or update a child row: a foreign key constraint fails |
insert or update on table "orders" violates foreign key constraint "fk_orders_member" |
| FOREIGN KEY(親の削除) | ERROR 1451: Cannot delete or update a parent row: a foreign key constraint fails |
update or delete on table "members" violates foreign key constraint "fk_orders_member" on table "orders" |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CREATE TABLEの書き方|主キー・外部キー・制約を例で解説」
サンプルスキーマ:4テーブルを通しで作る
題材は、会員が商品を注文するシンプルな構成です。
erDiagram
members ||--o{ orders : "注文する"
orders ||--|{ order_items : "明細を持つ"
products ||--o{ order_items : "含まれる"
members {
BIGINT id PK
VARCHAR email UK
VARCHAR name
VARCHAR status
}
orders {
BIGINT id PK
BIGINT member_id FK
}
order_items {
BIGINT order_id PK
INT line_no PK
BIGINT product_id FK
INT quantity
}
products {
BIGINT id PK
VARCHAR name
INT price
}
外部キーで参照される側(親)を先に作るのが順番のポイントです。ここでは members → products → orders → order_items の順に作ります。
MySQL 8.0 版(そのまま実行できます)
CREATE TABLE members (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_members PRIMARY KEY (id),
CONSTRAINT uq_members_email UNIQUE (email),
CONSTRAINT ck_members_status CHECK (status IN ('active', 'suspended', 'left'))
) COMMENT = '会員';
CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INT NOT NULL CHECK (price >= 0)
);
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
member_id BIGINT NOT NULL,
ordered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_orders PRIMARY KEY (id),
CONSTRAINT fk_orders_member FOREIGN KEY (member_id) REFERENCES members (id)
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
line_no INT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
CONSTRAINT pk_order_items PRIMARY KEY (order_id, line_no),
CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products (id),
CONSTRAINT ck_order_items_quantity CHECK (quantity > 0)
);
PostgreSQL 16 版(そのまま実行できます)
違うのは連番の書き方(GENERATED ALWAYS AS IDENTITY)、日時型(TIMESTAMP)、コメントの付け方の3点です。制約の部分は同じ書き方で通ります。
CREATE TABLE members (
id BIGINT GENERATED ALWAYS AS IDENTITY,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_members PRIMARY KEY (id),
CONSTRAINT uq_members_email UNIQUE (email),
CONSTRAINT ck_members_status CHECK (status IN ('active', 'suspended', 'left'))
);
COMMENT ON TABLE members IS '会員';
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price INT NOT NULL CHECK (price >= 0)
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
member_id BIGINT NOT NULL,
ordered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_orders PRIMARY KEY (id),
CONSTRAINT fk_orders_member FOREIGN KEY (member_id) REFERENCES members (id)
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
line_no INT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
CONSTRAINT pk_order_items PRIMARY KEY (order_id, line_no),
CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders (id),
CONSTRAINT fk_order_items_product FOREIGN KEY (product_id) REFERENCES products (id),
CONSTRAINT ck_order_items_quantity CHECK (quantity > 0)
);
最初のデータを入れて確かめる
INSERT INTO members (email, name) VALUES ('sato@example.com', '佐藤');
INSERT INTO products (name, price) VALUES ('ノート', 300);
INSERT INTO orders (member_id) VALUES (1);
INSERT INTO order_items (order_id, line_no, product_id) VALUES (1, 1, 1);
SELECT id, email, name, status FROM members;
実行結果(PostgreSQL 16)です。id は連番で、status は DEFAULT の 'active' が入りました。
id | email | name | status
----+------------------+------+--------
1 | sato@example.com | 佐藤 | active
(1 row)
6種類の制約と、違反したときのエラーメッセージ
ここからは制約を1つずつ、わざと違反するINSERTを実行して、返ってくるメッセージを確認します。
PRIMARY KEY(主キー):行を1つに特定する
主キーは「この値が決まれば1行に決まる」列です。重複なし・NULLなしが保証され、1テーブルに1つだけ定義できます。
INSERT INTO members (id, email, name) VALUES (1, 'x@example.com', '重複');
MySQL 8.0 の結果です。
ERROR 1062 (23000): Duplicate entry '1' for key 'members.PRIMARY'
PostgreSQL 16 では、GENERATED ALWAYS の列に値を直接指定したこと自体が先に止められます。
ERROR: cannot insert a non-DEFAULT value into column "id"
DETAIL: Column "id" is an identity column defined as GENERATED ALWAYS.
HINT: Use OVERRIDING SYSTEM VALUE to override.
ヒントのとおり OVERRIDING SYSTEM VALUE を付けると値を指定でき、そこで主キーの重複が検出されます。
ERROR: duplicate key value violates unique constraint "pk_members"
DETAIL: Key (id)=(1) already exists.
PostgreSQL は主キー違反も unique constraint と表示します。制約名に pk_ を付けておくと、メッセージだけで主キーだとわかります。 MySQL は主キーの名前が常に PRIMARY になり、CONSTRAINT pk_members と書いても名前は使われません。
1つのテーブルに PRIMARY KEY を2回書くと、次のエラーになります。
CREATE TABLE t_pk2 (a INT PRIMARY KEY, b INT PRIMARY KEY);
-- MySQL 8.0
ERROR 1068 (42000): Multiple primary key defined
-- PostgreSQL 16
ERROR: multiple primary keys for table "t_pk2" are not allowed
2列を組み合わせて主キーにしたいときは、次の複合主キーを使います。
複合主キー:2列以上の組み合わせで特定する
order_items は「注文ID+行番号」の組み合わせで1行が決まります。複合主キーは列制約では書けないので、テーブル制約で PRIMARY KEY (列1, 列2) と書きます。
CONSTRAINT pk_order_items PRIMARY KEY (order_id, line_no)
同じ組み合わせをもう一度入れた結果です。
INSERT INTO order_items (order_id, line_no, product_id) VALUES (1, 1, 1);
-- MySQL 8.0
ERROR 1062 (23000): Duplicate entry '1-1' for key 'order_items.PRIMARY'
-- PostgreSQL 16
ERROR: duplicate key value violates unique constraint "pk_order_items"
DETAIL: Key (order_id, line_no)=(1, 1) already exists.
| 主キーの形 | 向いている場面 | 書き方 |
|---|---|---|
| 連番の1列(サロゲートキー) | ほとんどのテーブル。他テーブルから参照しやすい | id BIGINT ... PRIMARY KEY |
| 複合主キー | 明細・中間テーブルなど、親のIDと組み合わせて決まるもの | PRIMARY KEY (order_id, line_no) |
| 連番の主キー+複合UNIQUE | 参照のしやすさと、組み合わせの一意性の両方がほしいとき | id ... PRIMARY KEY と UNIQUE (order_id, line_no) |
UNIQUE:重複を防ぐ
INSERT INTO members (email, name) VALUES ('sato@example.com', '鈴木');
-- MySQL 8.0
ERROR 1062 (23000): Duplicate entry 'sato@example.com' for key 'members.uq_members_email'
-- PostgreSQL 16
ERROR: duplicate key value violates unique constraint "uq_members_email"
DETAIL: Key (email)=(sato@example.com) already exists.
UNIQUE は主キーと違い、1テーブルにいくつでも定義でき、NULL を許す列なら NULL は複数行に入ります。UNIQUE 列に NULL を2行入れた結果は、MySQL 8.0・PostgreSQL 16 とも2行とも登録されました。
| 比べる点 | PRIMARY KEY | UNIQUE |
|---|---|---|
| 1テーブルに定義できる数 | 1つ | いくつでも |
| NULL | 入らない | 入る(複数行も可) |
| 用途 | 行の特定、外部キーの参照先 | メールアドレスやコードなどの重複防止 |
NOT NULL:値を必須にする
INSERT INTO members (email, name) VALUES ('suzuki@example.com', NULL); -- NULL を明示
INSERT INTO members (email) VALUES ('suzuki@example.com'); -- 列ごと省略
MySQL 8.0 は、NULL を明示した場合と、列を省略した場合でメッセージが変わります。
ERROR 1048 (23000): Column 'name' cannot be null
ERROR 1364 (HY000): Field 'name' doesn't have a default value
PostgreSQL 16 はどちらも同じメッセージで、入れようとした行の内容も表示されます。
ERROR: null value in column "name" of relation "members" violates not-null constraint
DETAIL: Failing row contains (3, suzuki@example.com, null, active, 2026-10-02 01:26:05.89012).
迷ったら NOT NULL を付けるのがおすすめです。NULL を許すと、集計や比較のときに NULL の扱いを毎回考えることになるためです。「未定」を表したい列だけ NULL を許す、と決めると設計が安定します。
CHECK:条件に合う値だけを通す
INSERT INTO members (email, name, status) VALUES ('suzuki@example.com', '鈴木', 'unknown');
INSERT INTO products (name, price) VALUES ('ペン', -1);
-- MySQL 8.0
ERROR 3819 (HY000): Check constraint 'ck_members_status' is violated.
ERROR 3819 (HY000): Check constraint 'products_chk_1' is violated.
-- PostgreSQL 16
ERROR: new row for relation "members" violates check constraint "ck_members_status"
DETAIL: Failing row contains (5, suzuki@example.com, 鈴木, unknown, 2026-10-02 01:26:05.89025).
ERROR: new row for relation "products" violates check constraint "products_price_check"
DETAIL: Failing row contains (2, ペン, -1).
1つ目は自分で名前を付けた制約、2つ目は名前を省略した制約です。名前を付けた ck_members_status のほうが、何の条件に当たったのかがひと目でわかります。
MySQL で CHECK 制約が実際に働くのは 8.0.16 以降です。それより前の版では、書けても評価されませんでした。
よく使う CHECK の形を表にまとめます。
| やりたいこと | 書き方 |
|---|---|
| 0以上 | CHECK (price >= 0) |
| 範囲 | CHECK (priority BETWEEN 1 AND 5) |
| 決まった値のどれか | CHECK (status IN ('active', 'suspended', 'left')) |
| 2列の大小関係 | CHECK (start_on <= end_on)(テーブル制約で書く) |
| 文字数 | CHECK (CHAR_LENGTH(code) = 8) |
複数の列を見る CHECK を MySQL で列制約として書くと、次のエラーになります。複数列にまたがる条件はテーブル制約で書きます。
CREATE TABLE t_col (id INT PRIMARY KEY, a INT CHECK (a < b), b INT);
ERROR 3813 (HY000): Column check constraint 't_col_chk_1' references other column.
FOREIGN KEY(外部キー):親にある値だけを入れる
orders.member_id には、members.id に存在する値だけが入ります。
INSERT INTO orders (member_id) VALUES (999); -- 存在しない会員
-- MySQL 8.0
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
(`t152`.`orders`, CONSTRAINT `fk_orders_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`))
-- PostgreSQL 16
ERROR: insert or update on table "orders" violates foreign key constraint "fk_orders_member"
DETAIL: Key (member_id)=(999) is not present in table "members".
逆に、注文から参照されている会員を消そうとすると、こちらも止まります。
DELETE FROM members WHERE id = 1;
-- MySQL 8.0
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails
(`t152`.`orders`, CONSTRAINT `fk_orders_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`))
-- PostgreSQL 16
ERROR: update or delete on table "members" violates foreign key constraint "fk_orders_member" on table "orders"
DETAIL: Key (id)=(1) is still referenced from table "orders".
1452 は「子に入れる値が親にない」、1451 は「親を消そうとしたが子が残っている」と覚えると切り分けが速くなります。親を消すときに子も一緒に消す・NULL にする、といった動きは ON DELETE CASCADE や ON DELETE SET NULL で指定できます。選び方は別記事「外部キーON DELETE CASCADE」で比較しているので、ここでは基本形に留めます。
参照されているテーブルは、DROP TABLE も止まります。
-- MySQL 8.0
ERROR 3730 (HY000): Cannot drop table 'members' referenced by a foreign key constraint 'fk_orders_member' on table 'orders'.
-- PostgreSQL 16
ERROR: cannot drop table members because other objects depend on it
DETAIL: constraint fk_orders_member on table orders depends on table members
HINT: Use DROP ... CASCADE to drop the dependent objects too.
作るときは親から、消すときは子からが基本の順番です。
MySQLでは列の後ろに書いた REFERENCES が働かない
外部キーで特に気を付けたいのがここです。標準SQLでは、列の定義に続けて REFERENCES を書けます。
CREATE TABLE t_inline (
id INT PRIMARY KEY,
member_id BIGINT REFERENCES members (id)
);
INSERT INTO t_inline VALUES (1, 999); -- 存在しない会員
PostgreSQL 16 では外部キーとして働き、INSERT は止まります。
ERROR: insert or update on table "t_inline" violates foreign key constraint "t_inline_member_id_fkey"
DETAIL: Key (member_id)=(999) is not present in table "members".
MySQL 8.0 ではエラーも警告も出ずにテーブルが作られ、INSERT もそのまま通りました。 SHOW CREATE TABLE で見ると、外部キーが定義されていないことがわかります。
CREATE TABLE `t_inline` (
`id` int NOT NULL,
`member_id` bigint DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
MySQL では、外部キーは必ずテーブル制約の形(FOREIGN KEY (列) REFERENCES 親 (列))で書きます。 作ったあとに SHOW CREATE TABLE で CONSTRAINT ... FOREIGN KEY の行があるかを確かめると確実です。
外部キーの型は親と揃える
-- members.id は BIGINT。子を INT にすると…
CREATE TABLE t_bad (
id INT PRIMARY KEY,
member_id INT,
FOREIGN KEY (member_id) REFERENCES members (id)
);
-- MySQL 8.0
ERROR 3780 (HY000): Referencing column 'member_id' and referenced column 'id' in foreign key constraint 't_bad_ibfk_1' are incompatible.
PostgreSQL 16 は INT と BIGINT の組み合わせは通りましたが、TEXT と BIGINT では次のエラーになりました。
ERROR: foreign key constraint "t_bad2_member_id_fkey" cannot be implemented
DETAIL: Key columns "member_id" and "id" are of incompatible types: text and bigint.
どちらのDBでも、子の列は親の列と同じ型にしておくのが扱いやすい形です。
MySQL 8.4 以降では、外部キーの参照先が主キーまたは UNIQUE の列であることが既定で求められます(システム変数 restrict_fk_on_non_standard_key が既定で ON。公式ドキュメントで確認)。主キーを参照する今回の書き方は、8.0 でも 8.4 以降でも同じです。
DEFAULT:省略したときの初期値
DEFAULT は厳密には「制約」というより列の属性ですが、CREATE TABLE では制約と並べて書きます。
INSERT INTO members (email, name, status) VALUES ('tanaka@example.com', '田中', DEFAULT);
INSERT INTO members (email, name, status) VALUES ('ito@example.com', '伊藤', NULL);
SELECT id, email, status FROM members;
-- 2つ目の INSERT(MySQL 8.0)
ERROR 1048 (23000): Column 'status' cannot be null
-- 2つ目の INSERT(PostgreSQL 16)
ERROR: null value in column "status" of relation "members" violates not-null constraint
id | email | status
----+--------------------+--------
1 | sato@example.com | active
6 | tanaka@example.com | active
DEFAULT が使われるのは「列を省略したとき」と「DEFAULT と書いたとき」だけです。NULL を明示すると NULL を入れようとするので、NOT NULL の列ではエラーになります。
結果の id が 1 の次に 6 になっているのは、途中でエラーになった INSERT でも連番が進むためです(MySQL でも同じように番号が飛びました)。連番は「重複しない番号」であって「欠番のない番号」ではない、と捉えておくとよいでしょう。
DEFAULT には式も書けます。
-- MySQL 8.0(式は括弧で囲む。8.0.13 以降)
CREATE TABLE t_def (
id INT PRIMARY KEY,
token CHAR(36) DEFAULT (UUID()),
due DATE DEFAULT (CURRENT_DATE + INTERVAL 7 DAY),
flag BOOLEAN DEFAULT FALSE
);
-- PostgreSQL 16
CREATE TABLE t_def (
id INT PRIMARY KEY,
token UUID DEFAULT gen_random_uuid(),
due DATE DEFAULT CURRENT_DATE + 7,
flag BOOLEAN DEFAULT FALSE
);
INSERT INTO t_def (id) VALUES (1); のあとに確認した結果(PostgreSQL 16)です。
id | len | due | flag
----+-----+------------+------
1 | 36 | 2026-10-09 | f
列制約とテーブル制約、制約名の付け方
列制約とテーブル制約の違い
| 書く場所 | 例 | 向いている場面 |
|---|---|---|
| 列制約(列の定義の後ろ) | price INT NOT NULL CHECK (price >= 0) |
1列だけで完結する短い制約 |
| テーブル制約(列の一覧の後ろ) | CONSTRAINT pk_order_items PRIMARY KEY (order_id, line_no) |
複数列にまたがる制約、名前を付けたい制約、MySQLの外部キー |
どちらで書いても働きは同じです。NOT NULL と DEFAULT は列の後ろに、それ以外は名前を付けてテーブル制約に、と決めておくと、チーム内で書き方が揃います。
名前を省略すると自動で付く
制約名を省略して作ったテーブルで、自動で付いた名前を確かめました。
CREATE TABLE t_noname (
id INT PRIMARY KEY,
code VARCHAR(10) UNIQUE,
n INT CHECK (n > 0),
member_id BIGINT,
FOREIGN KEY (member_id) REFERENCES members (id)
);
| 制約 | MySQL 8.0 の自動名 | PostgreSQL 16 の自動名 |
|---|---|---|
| PRIMARY KEY | PRIMARY(固定) |
t_noname_pkey |
| UNIQUE | code(列名) |
t_noname_code_key |
| CHECK | t_noname_chk_1 |
t_noname_n_check |
| FOREIGN KEY | t_noname_ibfk_1 |
t_noname_member_id_fkey |
自動名は DB ごとに違い、MySQL の _chk_1 や _ibfk_1 は連番なので、エラーメッセージを見ても内容が想像しにくくなります。MySQL と PostgreSQL の両方で同じ名前にしたい、あとから ALTER TABLE で外したい、というときは自分で名前を付けておくと楽です。
制約名の付け方の例
| 制約 | 命名の型 | 例 |
|---|---|---|
| PRIMARY KEY | pk_テーブル名 |
pk_members |
| UNIQUE | uq_テーブル名_列名 |
uq_members_email |
| FOREIGN KEY | fk_子テーブル名_親(または列) |
fk_orders_member |
| CHECK | ck_テーブル名_列名(や意味) |
ck_order_items_quantity |
接頭辞は pk_/uq_/fk_/ck_ のほかにも流儀がありますが、種類・テーブル・列が名前から読み取れることと、チームで1つに決めることが大切です。PostgreSQL では制約名はテーブル内で(主キーと UNIQUE は対応するインデックス名としてスキーマ内で)重ならないようにし、MySQL では外部キー名と CHECK 制約名がデータベース内で重ならないようにします。テーブル名を含めておけば自然に重なりを避けられます。
定義済みの制約は次のSQLで一覧できます。
-- MySQL 8.0
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = DATABASE() AND table_name = 'order_items';
-- PostgreSQL 16(psql なら \d order_items でも見られます)
SELECT conname, contype FROM pg_constraint
WHERE conrelid = 'order_items'::regclass;
MySQL 8.0 の実行結果です。
+-------------------------+-----------------+
| CONSTRAINT_NAME | CONSTRAINT_TYPE |
+-------------------------+-----------------+
| ck_order_items_quantity | CHECK |
| fk_order_items_order | FOREIGN KEY |
| fk_order_items_product | FOREIGN KEY |
| PRIMARY | PRIMARY KEY |
+-------------------------+-----------------+
連番の主キー:AUTO_INCREMENT と IDENTITY
| DB | 書き方 | 補足 |
|---|---|---|
| MySQL | id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY |
AUTO_INCREMENT の列はキー(主キーなど)にする必要がある |
| PostgreSQL | id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY |
標準SQLの書き方。値の直接指定は止められる |
| PostgreSQL | id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY |
値を直接指定することもできる |
| PostgreSQL | id BIGSERIAL PRIMARY KEY |
以前からある書き方。今も動く |
型は、行数が増えても余裕のある BIGINT にしておくと、あとから型を広げる作業を避けられます。PostgreSQL の serial と IDENTITY の違いや、連番がずれたときの直し方は別記事「PostgreSQL連番|serialとIDENTITYの違い」で扱っています。
IF NOT EXISTS:すでにあるときはスキップする
同じ名前のテーブルがあると、CREATE TABLE はエラーになります。
CREATE TABLE members (id INT);
-- MySQL 8.0
ERROR 1050 (42S01): Table 'members' already exists
-- PostgreSQL 16
ERROR: relation "members" already exists
IF NOT EXISTS を付けると、エラーではなく注意(Note/NOTICE)になり、処理は先へ進みます。
CREATE TABLE IF NOT EXISTS members (id INT);
-- MySQL 8.0(SHOW WARNINGS の結果)
| Note | 1050 | Table 'members' already exists |
-- PostgreSQL 16
NOTICE: relation "members" already exists, skipping
IF NOT EXISTS は「同じ名前があるか」だけを見ます。列の定義が違っていても、既存のテーブルはそのまま残ります。 上の例では (id INT) と書きましたが、members は元の5列のままです。定義を変えたいときは ALTER TABLE を使います。
作り直したいときの定型は次のとおりです(データも消えるので、開発用のDBで使います)。
DROP TABLE IF EXISTS order_items, orders, products, members; -- 子から順に
CREATE TABLE members ( ... );
既存テーブルから作る:CREATE TABLE AS と LIKE
CREATE TABLE AS SELECT:データごとコピーする
CREATE TABLE members_ctas AS
SELECT * FROM members WHERE status = 'active';
SELECT の結果がそのまま新しいテーブルになります(2行がコピーされました)。ただし、制約はほとんど引き継がれません。
MySQL 8.0 の SHOW CREATE TABLE members_ctas です。
CREATE TABLE `members_ctas` (
`id` bigint NOT NULL DEFAULT '0',
`email` varchar(255) NOT NULL,
`name` varchar(100) NOT NULL,
`status` varchar(20) NOT NULL DEFAULT 'active',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
NOT NULL と DEFAULT は残りましたが、主キー・UNIQUE・CHECK・AUTO_INCREMENT はなくなっています。 PostgreSQL 16 では NOT NULL と DEFAULT も引き継がれず、列名と型だけになりました。
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+---------
id | bigint | | |
email | character varying(255) | | |
name | character varying(100) | | |
status | character varying(20) | | |
created_at | timestamp without time zone | | |
CREATE TABLE LIKE:定義だけコピーする
-- MySQL
CREATE TABLE members_like LIKE members;
-- PostgreSQL(括弧が必要)
CREATE TABLE members_like (LIKE members);
CREATE TABLE members_like_all (LIKE members INCLUDING ALL);
データは入らず、空のテーブルができます。引き継がれる範囲を確かめた結果です。
| 引き継がれるもの | MySQL LIKE |
PG (LIKE t) |
PG (LIKE t INCLUDING ALL) |
CREATE TABLE AS(MySQL/PG) |
|---|---|---|---|---|
| 列名・型 | ○ | ○ | ○ | ○/○ |
| NOT NULL | ○ | ○ | ○ | ○/× |
| DEFAULT | ○ | × | ○ | ○/× |
| 主キー・UNIQUE | ○ | × | ○ | ×/× |
| CHECK | ○(名前は members_like_chk_1 に変わる) |
× | ○(名前はそのまま) | ×/× |
| 連番 | ○(AUTO_INCREMENT) | × | ○(IDENTITY) | ×/× |
| 外部キー | × | × | × | ×/× |
| データ | × | × | × | ○/○ |
外部キーはどの方法でも引き継がれません。 外部キーを持つ orders を MySQL で LIKE した結果を見ると、インデックス(KEY fk_orders_member)だけが残り、CONSTRAINT ... FOREIGN KEY の行はありませんでした。必要なら ALTER TABLE で付け直します。
| やりたいこと | 選ぶもの |
|---|---|
| 検証用にデータごと写したい | CREATE TABLE AS SELECT(あとで主キーを付ける) |
| 同じ構造の空テーブルがほしい | MySQL は LIKE、PostgreSQL は (LIKE t INCLUDING ALL) |
| 構造もデータも制約もそろえたい | LIKE で作ってから INSERT INTO ... SELECT でデータを入れ、外部キーを足す |
コメントを付ける
テーブルや列に説明を残しておくと、あとから見る人が意味を追いやすくなります。
-- MySQL:CREATE TABLE の中に書く
CREATE TABLE notes (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'ノートID',
title VARCHAR(100) NOT NULL COMMENT 'タイトル'
) COMMENT = 'ノート';
-- PostgreSQL:CREATE TABLE のあとに別の文で書く
COMMENT ON TABLE members IS '会員';
COMMENT ON COLUMN members.email IS 'メールアドレス';
確認方法と結果です。
-- MySQL
SELECT column_name, column_comment FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'notes';
-- PostgreSQL(psql なら \d+ members でも見られます)
SELECT obj_description('members'::regclass) AS tbl,
col_description('members'::regclass, 2) AS col;
tbl | col
------+----------------
会員 | メールアドレス
後から制約を足す:ALTER TABLE の定型文
制約なしで作ったテーブルに、あとから制約を足す定型文です。次のテーブルを出発点に、MySQL 8.0 と PostgreSQL 16 の両方ですべて実行して確かめました。
CREATE TABLE tasks (id BIGINT, title VARCHAR(100), member_id BIGINT, priority INT, code VARCHAR(10));
| やりたいこと | MySQL 8.0 | PostgreSQL 16 |
|---|---|---|
| 主キーを足す | ALTER TABLE tasks ADD CONSTRAINT pk_tasks PRIMARY KEY (id); |
同じ |
| 連番にする | ALTER TABLE tasks MODIFY id BIGINT NOT NULL AUTO_INCREMENT;(主キーの追加と同じ文で) |
ALTER TABLE tasks ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY;(先に NOT NULL が必要) |
| NOT NULL にする | ALTER TABLE tasks MODIFY title VARCHAR(100) NOT NULL; |
ALTER TABLE tasks ALTER COLUMN title SET NOT NULL; |
| UNIQUE を足す | ALTER TABLE tasks ADD CONSTRAINT uq_tasks_code UNIQUE (code); |
同じ |
| CHECK を足す | ALTER TABLE tasks ADD CONSTRAINT ck_tasks_priority CHECK (priority BETWEEN 1 AND 5); |
同じ |
| 外部キーを足す | ALTER TABLE tasks ADD CONSTRAINT fk_tasks_member FOREIGN KEY (member_id) REFERENCES members (id); |
同じ |
| DEFAULT を付ける | ALTER TABLE tasks ALTER priority SET DEFAULT 3; |
ALTER TABLE tasks ALTER COLUMN priority SET DEFAULT 3; |
| CHECK を外す | ALTER TABLE tasks DROP CONSTRAINT ck_tasks_priority; |
同じ |
| 外部キーを外す | ALTER TABLE tasks DROP FOREIGN KEY fk_tasks_member; |
ALTER TABLE tasks DROP CONSTRAINT fk_tasks_member; |
| UNIQUE を外す | ALTER TABLE tasks DROP INDEX uq_tasks_code; |
ALTER TABLE tasks DROP CONSTRAINT uq_tasks_code; |
MySQL で主キーと連番を同時に付けるときは、1つの文にまとめます。
ALTER TABLE tasks
MODIFY id BIGINT NOT NULL AUTO_INCREMENT,
ADD CONSTRAINT pk_tasks PRIMARY KEY (id);
すでに入っているデータが条件に合わないと、制約の追加は止まります。 priority に 9 が入った行がある状態で、1〜5 の CHECK を足そうとした結果です。
-- MySQL 8.0
ERROR 3819 (HY000): Check constraint 'ck_tasks_priority' is violated.
-- PostgreSQL 16
ERROR: check constraint "ck_tasks_priority" of relation "tasks" is violated by some row
先にデータを直してから制約を足すのが基本です。PostgreSQL では NOT VALID を付けると、既存の行は確認せずに、これから入る行だけを対象に制約を追加できました(あとで VALIDATE CONSTRAINT を実行して既存行も確認します)。
ALTER TABLE tasks ADD CONSTRAINT ck_tasks_priority
CHECK (priority BETWEEN 1 AND 5) NOT VALID;
列の追加や型の変更など、制約以外の ALTER TABLE は別記事「ALTER TABLEでカラム追加・型変更」にまとめています。
MySQLとPostgreSQLの書き方の違い
| 項目 | MySQL 8.0 | PostgreSQL 16 |
|---|---|---|
| 連番 | AUTO_INCREMENT |
GENERATED ... AS IDENTITY(SERIAL も可) |
| 日時型 | DATETIME/TIMESTAMP |
TIMESTAMP/TIMESTAMPTZ |
| 真偽値 | BOOLEAN(中身は TINYINT(1)。0/1 で表示) |
BOOLEAN(t/f で表示) |
| 長い文字列 | TEXT、VARCHAR(n) |
TEXT、VARCHAR(n) |
列の後ろの REFERENCES |
書けるが外部キーにならない | 外部キーになる |
| 主キーの名前 | 常に PRIMARY |
付けた名前(省略時は テーブル名_pkey) |
| DEFAULT に式 | 括弧で囲む DEFAULT (UUID()) |
そのまま DEFAULT gen_random_uuid() |
| コメント | COMMENT '...' を定義の中に |
COMMENT ON ... を別の文で |
| 定義だけコピー | CREATE TABLE b LIKE a |
CREATE TABLE b (LIKE a INCLUDING ALL) |
| 定義の確認 | SHOW CREATE TABLE t |
\d t(psql) |
| 名前を囲む記号 | バッククォート `order` |
ダブルクォート "order" |
| テーブルオプション | ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 など |
なし(文字コードはデータベース単位) |
| DDL のロールバック | できない(CREATE TABLE は暗黙にコミットされる) | できる(トランザクション内で取り消せる) |
MySQL 8.0 以降、既定のストレージエンジンは InnoDB、既定の文字セットは utf8mb4 なので、ENGINE や CHARSET は省略しても同じ結果になります。
CREATE TABLEを書くときのチェックリスト
| 確認すること | 見るポイント |
|---|---|
| 主キーがあるか | すべてのテーブルに1つ。連番なら BIGINT |
| NOT NULL を付けたか | 「未定」を表す必要がある列だけ NULL を許す |
| 重複させたくない列に UNIQUE があるか | メールアドレス、コードなど |
| 外部キーの型は親と同じか | BIGINT なら子も BIGINT |
| MySQL の外部キーはテーブル制約で書いたか | SHOW CREATE TABLE で確認 |
| 制約に名前を付けたか | pk_/uq_/fk_/ck_ +テーブル名 |
| 作る順番は親からか | 参照される側を先に作る |
| 最後の列定義の後ろに余分なカンマがないか | あると構文エラーになる |
order や user など予約語を名前にしていないか |
orders、users のように複数形にすると避けやすい |
よくある質問
Q. 主キーと UNIQUE はどう違いますか? A. 主キーは1テーブルに1つだけで NULL が入りません。UNIQUE はいくつでも定義でき、NULL を許す列なら NULL が複数行に入ります。行を特定する代表の列を主キーに、それ以外で重複させたくない列(メールアドレスなど)を UNIQUE にします。
Q. 外部キーは必ず付けたほうがよいですか? A. 付けておくと「存在しない会員の注文」のような不整合をデータベースが防いでくれるので、基本的には付けるのがおすすめです。大量データの一括投入のときだけ一時的に外す、といった運用はありますが、その場合も投入後に付け直して整合性を確かめます。
Q. CREATE TABLE でエラーになったとき、どこから見ればよいですか?
A. まずエラーメッセージに出ている位置(MySQL は near '...'、PostgreSQL は LINE と ^)の直前を見ます。多いのは、最後の列の後ろの余分なカンマ、括弧の閉じ忘れ、予約語を列名に使ったケース、外部キーの親テーブルがまだ存在しないケースです。
Q. MySQL 8.4 や 9.7 では書き方が変わりますか? A. この記事の CREATE TABLE 文と制約の書き方は、そのまま使えます。8.4 以降で変わった点として、外部キーの参照先が主キーか UNIQUE の列であることが既定で求められるようになりました(公式ドキュメントで確認)。主キーを参照していれば影響はありません。
まとめ
- CREATE TABLE は「列名 データ型 制約」をカンマで並べるのが基本で、親テーブルから先に作ります。
- 6種類の制約(PRIMARY KEY・FOREIGN KEY・UNIQUE・NOT NULL・CHECK・DEFAULT)を使い分けると、不整合なデータをデータベース側で防げます。
- 制約には
pk_/uq_/fk_/ck_で始まる名前を付けると、エラーメッセージから原因がすぐわかります。 - MySQL の外部キーはテーブル制約で書きます。 列の後ろの
REFERENCESは外部キーになりません。 - 複合主キーは
PRIMARY KEY (列1, 列2)、連番は MySQL がAUTO_INCREMENT、PostgreSQL がGENERATED ... AS IDENTITYです。 CREATE TABLE ASはデータを、LIKEは定義をコピーします。 外部キーはどちらでも引き継がれません。- あとから足すときは
ALTER TABLE ... ADD CONSTRAINT。既存データが条件に合っているかを先に確かめます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CREATE TABLEの書き方|主キー・外部キー・制約を例で解説」
株式会社RJCでは、CREATE TABLEと制約の設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。