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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?