テーブル定義書とは、データベースの各テーブルについて、列の論理名・物理名・型・桁・NULLの可否・既定値・主キー・外部キー・索引を一覧にした設計書です。 1テーブルを1枚(1シート)にまとめ、全体を見渡す「テーブル一覧」を先頭に付けるのが基本の形です。

この記事では、定義書に書く項目の一覧と貼って使えるテンプレートを記入例つきで載せ、さらに CREATE TABLE(DDL)で作成済みのデータベースから定義書の表を取り出すSQL を PostgreSQL と MySQL の両方で、実行結果つきで示します。取り出した結果を CSV と Markdown の表に書き出す Python も付けました。CREATE TABLE の文法やER図の描き方には触れず、定義書の体裁とデータベースからの逆生成に絞ります。

確認環境:Ubuntu 24.04 / PostgreSQL 16.15 / MySQL 8.0.46 / Python 3.11.15(標準ライブラリのみ)

テーブル定義書の早見表

項目 書く内容 記入例 DBから取れるか
テーブル論理名 日本語の名前 予約 ○(コメントに入れておく)
テーブル物理名 実際のテーブル名 reservations ○
No 列の並び順 1 ○
論理名 列の日本語名 利用開始日 ○(コメントに入れておく)
物理名 実際の列名 start_date ○
型 データ型 DATE、VARCHAR ○
桁 長さ、または全体桁数と小数桁数 50、(10,2) ○
NULL NULLを許すか 不可/可 ○
既定値 値を省いたときに入る値 1、CURRENT_TIMESTAMP、自動採番 ○
PK 主キー。複合のときは順番 PK、PK1・PK2 ○
FK 参照先のテーブルと列 items.item_code ○
索引 索引名・種類・列の並び reservations_item_period_idx(item_code, start_date, end_date) ○
備考 コード値の意味、CHECK制約、設計の意図 0:申請中 1:承認 9:取消 △(CHECK制約は取れる。意図は人が書く)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「テーブル定義書の書き方とテンプレート DDLから自動生成するSQL」

テーブル定義書の構成と項目一覧

テーブル定義書は、次の4つの部分で構成すると読みやすくなります。

部分 内容 単位
1. テーブル一覧 全テーブルの論理名・物理名・概要 データベースに1つ
2. テーブル情報 論理名・物理名・概要・更新日 テーブルごと
3. 列一覧 No・論理名・物理名・型・桁・NULL・既定値・PK・FK・備考 テーブルごと
4. 索引一覧・制約一覧 索引名・種類・列/制約名・定義 テーブルごと

列一覧の各項目は、次のきまりで書くと表記がそろいます。

項目 書き方のきまり
No テーブルの列の並び順どおりに1から振る
論理名 画面や帳票で使う呼び名に合わせる。同じ意味の列は全テーブルで同じ名前にする
物理名 実際の列名をそのまま書く(大文字・小文字も合わせる)
型 定義書の中で表記を統一する(VARCHAR と character varying を混ぜない)
桁 文字列は長さ、数値は「全体桁数,小数桁数」。型に含めて VARCHAR(50) と書いてもよい
NULL 「不可/可」または「NOT NULL/空欄」。○×は意味が逆に読まれやすいので使い方を凡例に書く
既定値 固定値・関数・自動採番を区別して書く。無いときは空欄または「―」
PK 主キーの列に「PK」。複合主キーは PK1、PK2 と順番を書く
FK 「参照先テーブル.列」の形で書く
備考 コード値の意味、単位、CHECK制約、ほかの列との関係

コピペ用テンプレート

テーブル一覧

No 論理名 物理名 概要 備考
1 部署 departments 利用者が所属する部署 マスタ
2 備品 items 予約の対象になる備品 マスタ
3 利用者 members 備品を予約する人 マスタ
4 予約 reservations 備品の予約1件を1行で持つ トランザクション
No  論理名 物理名 概要  備考
1   部署  departments 利用者が所属する部署  マスタ
2   備品  items   予約の対象になる備品  マスタ
3   利用者 members 備品を予約する人    マスタ
4   予約  reservations    備品の予約1件を1行で持つ   トランザクション

テーブル定義(1テーブル分)の記入例:予約

架空の社内備品予約システムの「予約」テーブルの記入例です。

項目 内容
テーブル論理名 予約
テーブル物理名 reservations
概要 備品の予約1件を1行で持つ。同じ備品の予約は期間が重ならない
更新日 2026-10-05

列一覧

No 論理名 物理名 型 桁 NULL 既定値 PK FK 備考
1 予約ID reservation_id BIGINT ― 不可 自動採番 PK ―
2 備品コード item_code CHAR 6 不可 ― ― items.item_code
3 利用者ID member_id BIGINT ― 不可 ― ― members.member_id
4 利用開始日 start_date DATE ― 不可 ― ― ―
5 利用終了日 end_date DATE ― 不可 ― ― ― 利用開始日以降(CHECK)
6 数量 qty INTEGER ― 不可 1 ― ― 1以上99以下(CHECK)
7 状態 status SMALLINT ― 不可 0 ― ― 0:申請中 1:承認 9:取消
8 登録日時 created_at TIMESTAMP ― 不可 CURRENT_TIMESTAMP ― ―

索引一覧

No 索引名 種類 列(並び順) 用途
1 reservations_pkey 主キー reservation_id 予約1件の特定
2 reservations_item_period_idx 通常 item_code, start_date, end_date 備品ごとの期間の重なり検索
3 reservations_member_idx 通常 member_id 利用者ごとの予約一覧

制約一覧

No 制約名 種類 内容
1 reservations_period_check CHECK start_date <= end_date
2 reservations_qty_check CHECK qty が 1 以上 99 以下

表計算ソフトに貼れるタブ区切りです(列一覧)。

No  論理名 物理名 型   桁   NULL    既定値 PK  FK  備考
1   予約ID    reservation_id  BIGINT  ―   不可  自動採番    PK  ―   
2   備品コード   item_code   CHAR    6   不可  ―   ―   items.item_code 
3   利用者ID   member_id   BIGINT  ―   不可  ―   ―   members.member_id   
4   利用開始日   start_date  DATE    ―   不可  ―   ―   ―   
5   利用終了日   end_date    DATE    ―   不可  ―   ―   ―   利用開始日以降(CHECK)
6   数量  qty INTEGER ―   不可  1   ―   ―   1以上99以下(CHECK)
7   状態  status  SMALLINT    ―   不可  0   ―   ―   0:申請中 1:承認 9:取消
8   登録日時    created_at  TIMESTAMP   ―   不可  CURRENT_TIMESTAMP   ―   ―   

準備:題材の4テーブルを作る(PostgreSQL)

ここからは、作成済みのデータベースから定義書を取り出す方法です。論理名はテーブルと列のコメントに入れておく のがポイントで、こうしておくと定義書の「論理名」までSQLで取り出せます。

CREATE TABLE departments (
  dept_code  CHAR(3)      PRIMARY KEY,
  dept_name  VARCHAR(50)  NOT NULL
);
CREATE TABLE members (
  member_id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  dept_code   CHAR(3)      NOT NULL REFERENCES departments (dept_code),
  member_name VARCHAR(40)  NOT NULL,
  email       VARCHAR(254) NOT NULL UNIQUE,
  is_active   BOOLEAN      NOT NULL DEFAULT TRUE,
  created_at  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE items (
  item_code  CHAR(6)       PRIMARY KEY,
  item_name  VARCHAR(100)  NOT NULL,
  category   VARCHAR(10)   NOT NULL DEFAULT 'PC' CHECK (category IN ('PC', 'MONITOR', 'ROOM')),
  unit_price NUMERIC(10,2),
  note       TEXT
);
CREATE TABLE reservations (
  reservation_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  item_code   CHAR(6)   NOT NULL REFERENCES items (item_code),
  member_id   BIGINT    NOT NULL REFERENCES members (member_id),
  start_date  DATE      NOT NULL,
  end_date    DATE      NOT NULL,
  qty         INTEGER   NOT NULL DEFAULT 1 CHECK (qty BETWEEN 1 AND 99),
  status      SMALLINT  NOT NULL DEFAULT 0,
  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT reservations_period_check CHECK (start_date <= end_date)
);
CREATE INDEX reservations_item_period_idx ON reservations (item_code, start_date, end_date);
CREATE INDEX reservations_member_idx ON reservations (member_id);

COMMENT ON TABLE departments IS '部署';
COMMENT ON COLUMN departments.dept_code IS '部署コード';
COMMENT ON COLUMN departments.dept_name IS '部署名';
COMMENT ON TABLE members IS '利用者';
COMMENT ON COLUMN members.member_id IS '利用者ID';
COMMENT ON COLUMN members.dept_code IS '部署コード';
COMMENT ON COLUMN members.member_name IS '氏名';
COMMENT ON COLUMN members.email IS 'メールアドレス';
COMMENT ON COLUMN members.is_active IS '有効フラグ';
COMMENT ON COLUMN members.created_at IS '登録日時';
COMMENT ON TABLE items IS '備品';
COMMENT ON COLUMN items.item_code IS '備品コード';
COMMENT ON COLUMN items.item_name IS '備品名';
COMMENT ON COLUMN items.category IS '区分';
COMMENT ON COLUMN items.unit_price IS '購入単価';
COMMENT ON COLUMN items.note IS '備考';
COMMENT ON TABLE reservations IS '予約';
COMMENT ON COLUMN reservations.reservation_id IS '予約ID';
COMMENT ON COLUMN reservations.item_code IS '備品コード';
COMMENT ON COLUMN reservations.member_id IS '利用者ID';
COMMENT ON COLUMN reservations.start_date IS '利用開始日';
COMMENT ON COLUMN reservations.end_date IS '利用終了日';
COMMENT ON COLUMN reservations.qty IS '数量';
COMMENT ON COLUMN reservations.status IS '状態(0:申請中 1:承認 9:取消)';
COMMENT ON COLUMN reservations.created_at IS '登録日時';

PostgreSQLで定義書の表を作るSQL

テーブル一覧を取り出す

SELECT c.relname                        AS "物理名",
       obj_description(c.oid, 'pg_class') AS "論理名",
       (SELECT count(*) FROM pg_attribute a
         WHERE a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped) AS "列数"
  FROM pg_class c
  JOIN pg_namespace n ON n.oid = c.relnamespace
 WHERE n.nspname = 'public' AND c.relkind = 'r'
 ORDER BY c.relname;
    物理名    | 論理名 | 列数 
--------------+--------+------
 departments  | 部署   |    2
 items        | 備品   |    5
 members      | 利用者 |    6
 reservations | 予約   |    8
(4 rows)

列一覧(論理名・型・NULL・既定値・PK・FK)を取り出す

システムカタログ(pg_catalog)の pg_attribute を軸に、コメント・既定値・主キー・外部キーを結合します。format_type 関数を使うと、character varying(50) のように桁まで含んだ型の表記が得られます。

SELECT c.relname                              AS "テーブル",
       a.attnum                               AS "No",
       col_description(c.oid, a.attnum)       AS "論理名",
       a.attname                              AS "物理名",
       format_type(a.atttypid, a.atttypmod)   AS "型",
       CASE WHEN a.attnotnull THEN '不可' ELSE '可' END AS "NULL",
       CASE WHEN a.attidentity <> '' THEN '自動採番'
            ELSE pg_get_expr(d.adbin, d.adrelid) END AS "既定値",
       CASE WHEN pk.conrelid IS NOT NULL THEN 'PK' END AS "PK",
       (SELECT string_agg(rt.relname || '.' || ra.attname, ', ')
          FROM pg_constraint fk
          JOIN pg_class rt ON rt.oid = fk.confrelid
          JOIN pg_attribute ra ON ra.attrelid = fk.confrelid
                              AND ra.attnum = fk.confkey[array_position(fk.conkey, a.attnum)]
         WHERE fk.conrelid = c.oid AND fk.contype = 'f'
           AND a.attnum = ANY (fk.conkey))    AS "FK"
  FROM pg_class c
  JOIN pg_namespace n ON n.oid = c.relnamespace
  JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
  LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
  LEFT JOIN pg_constraint pk ON pk.conrelid = c.oid AND pk.contype = 'p'
                            AND a.attnum = ANY (pk.conkey)
 WHERE n.nspname = 'public' AND c.relkind = 'r'
 ORDER BY c.relname, a.attnum;
   テーブル   | No |             論理名             |     物理名     |             型              | NULL |         既定値          | PK |          FK           
--------------+----+--------------------------------+----------------+-----------------------------+------+-------------------------+----+-----------------------
 departments  |  1 | 部署コード                     | dept_code      | character(3)                | 不可 |                         | PK | 
 departments  |  2 | 部署名                         | dept_name      | character varying(50)       | 不可 |                         |    | 
 items        |  1 | 備品コード                     | item_code      | character(6)                | 不可 |                         | PK | 
 items        |  2 | 備品名                         | item_name      | character varying(100)      | 不可 |                         |    | 
 items        |  3 | 区分                           | category       | character varying(10)       | 不可 | 'PC'::character varying |    | 
 items        |  4 | 購入単価                       | unit_price     | numeric(10,2)               | 可   |                         |    | 
 items        |  5 | 備考                           | note           | text                        | 可   |                         |    | 
 members      |  1 | 利用者ID                       | member_id      | bigint                      | 不可 | 自動採番                | PK | 
 members      |  2 | 部署コード                     | dept_code      | character(3)                | 不可 |                         |    | departments.dept_code
 members      |  3 | 氏名                           | member_name    | character varying(40)       | 不可 |                         |    | 
 members      |  4 | メールアドレス                 | email          | character varying(254)      | 不可 |                         |    | 
 members      |  5 | 有効フラグ                     | is_active      | boolean                     | 不可 | true                    |    | 
 members      |  6 | 登録日時                       | created_at     | timestamp without time zone | 不可 | CURRENT_TIMESTAMP       |    | 
 reservations |  1 | 予約ID                         | reservation_id | bigint                      | 不可 | 自動採番                | PK | 
 reservations |  2 | 備品コード                     | item_code      | character(6)                | 不可 |                         |    | items.item_code
 reservations |  3 | 利用者ID                       | member_id      | bigint                      | 不可 |                         |    | members.member_id
 reservations |  4 | 利用開始日                     | start_date     | date                        | 不可 |                         |    | 
 reservations |  5 | 利用終了日                     | end_date       | date                        | 不可 |                         |    | 
 reservations |  6 | 数量                           | qty            | integer                     | 不可 | 1                       |    | 
 reservations |  7 | 状態(0:申請中 1:承認 9:取消) | status         | smallint                    | 不可 | 0                       |    | 
 reservations |  8 | 登録日時                       | created_at     | timestamp without time zone | 不可 | CURRENT_TIMESTAMP       |    | 
(21 rows)

結果の読み方です。

列 取り出し元 補足
論理名 col_description(テーブルのOID, 列番号) COMMENT ON COLUMN で入れた文字列
型 format_type(atttypid, atttypmod) 桁つきの表記になる
NULL pg_attribute.attnotnull true なら NOT NULL
既定値 pg_attrdef を pg_get_expr で文字列にする IDENTITY 列は attidentity で判定して「自動採番」と表示
PK pg_constraint の contype = 'p' conkey に主キーの列番号が配列で入る
FK pg_constraint の contype = 'f' confrelid が参照先テーブル、confkey が参照先の列番号

items の category の既定値が 'PC'::character varying と型つきで表示されているのは PostgreSQL の内部表現です。定義書に載せるときは 'PC' と読み替えます。

information_schema だけで取り出す

標準的なビューである information_schema.columns でも、型・桁・NULL・既定値は取れます。型名と桁が別の列に分かれている点と、コメント(論理名)は含まれない点が pg_catalog との違いです。

SELECT table_name, ordinal_position AS no, column_name, data_type,
       character_maximum_length AS len, numeric_precision AS prec, numeric_scale AS scale,
       is_nullable, column_default
  FROM information_schema.columns
 WHERE table_schema = 'public' AND table_name = 'items'
 ORDER BY ordinal_position;
 table_name | no | column_name |     data_type     | len | prec | scale | is_nullable |     column_default      
------------+----+-------------+-------------------+-----+------+-------+-------------+-------------------------
 items      |  1 | item_code   | character         |   6 |      |       | NO          | 
 items      |  2 | item_name   | character varying | 100 |      |       | NO          | 
 items      |  3 | category    | character varying |  10 |      |       | NO          | 'PC'::character varying
 items      |  4 | unit_price  | numeric           |     |   10 |     2 | YES         | 
 items      |  5 | note        | text              |     |      |       | YES         | 
(5 rows)

索引一覧とCHECK制約を取り出す

SELECT t.relname AS "テーブル",
       i.relname AS "索引名",
       CASE WHEN x.indisprimary THEN '主キー'
            WHEN x.indisunique  THEN '一意'
            ELSE '通常' END AS "種類",
       (SELECT string_agg(a.attname, ', ' ORDER BY k.ord)
          FROM unnest(x.indkey) WITH ORDINALITY AS k(attnum, ord)
          JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum) AS "列"
  FROM pg_index x
  JOIN pg_class i ON i.oid = x.indexrelid
  JOIN pg_class t ON t.oid = x.indrelid
  JOIN pg_namespace n ON n.oid = t.relnamespace
 WHERE n.nspname = 'public'
 ORDER BY t.relname, i.relname;
   テーブル   |            索引名            |  種類  |               列                
--------------+------------------------------+--------+---------------------------------
 departments  | departments_pkey             | 主キー | dept_code
 items        | items_pkey                   | 主キー | item_code
 members      | members_email_key            | 一意   | email
 members      | members_pkey                 | 主キー | member_id
 reservations | reservations_item_period_idx | 通常   | item_code, start_date, end_date
 reservations | reservations_member_idx      | 通常   | member_id
 reservations | reservations_pkey            | 主キー | reservation_id
(7 rows)

主キーと UNIQUE 制約には、テーブル名_pkey、テーブル名_列名_key という名前の索引が自動で作られていることがわかります。

SELECT t.relname AS "テーブル", con.conname AS "制約名", pg_get_constraintdef(con.oid) AS "定義"
  FROM pg_constraint con
  JOIN pg_class t ON t.oid = con.conrelid
  JOIN pg_namespace n ON n.oid = t.relnamespace
 WHERE n.nspname = 'public' AND con.contype = 'c'
 ORDER BY t.relname, con.conname;
   テーブル   |          制約名           |                                                                 定義                                                                 
--------------+---------------------------+--------------------------------------------------------------------------------------------------------------------------------------
 items        | items_category_check      | CHECK (((category)::text = ANY ((ARRAY['PC'::character varying, 'MONITOR'::character varying, 'ROOM'::character varying])::text[])))
 reservations | reservations_period_check | CHECK ((start_date <= end_date))
 reservations | reservations_qty_check    | CHECK (((qty >= 1) AND (qty <= 99)))
(3 rows)

qty BETWEEN 1 AND 99 と書いた制約が (qty >= 1) AND (qty <= 99) に、IN (...) が = ANY (ARRAY[...]) に書き換わって保存されています。定義書の備考には、読みやすい元の書き方で載せるとよいでしょう。

MySQLで定義書の表を作るSQL

題材のテーブル(MySQL版)

MySQL では、列の定義の中に COMMENT '...' を書けます。

CREATE DATABASE t358 CHARACTER SET utf8mb4;
USE t358;
CREATE TABLE departments (
  dept_code  CHAR(3)      NOT NULL COMMENT '部署コード',
  dept_name  VARCHAR(50)  NOT NULL COMMENT '部署名',
  PRIMARY KEY (dept_code)
) COMMENT = '部署';
CREATE TABLE members (
  member_id   BIGINT       NOT NULL AUTO_INCREMENT COMMENT '利用者ID',
  dept_code   CHAR(3)      NOT NULL COMMENT '部署コード',
  member_name VARCHAR(40)  NOT NULL COMMENT '氏名',
  email       VARCHAR(254) NOT NULL COMMENT 'メールアドレス',
  is_active   BOOLEAN      NOT NULL DEFAULT TRUE COMMENT '有効フラグ',
  created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '登録日時',
  PRIMARY KEY (member_id),
  UNIQUE KEY members_email_key (email),
  CONSTRAINT members_dept_fk FOREIGN KEY (dept_code) REFERENCES departments (dept_code)
) COMMENT = '利用者';
CREATE TABLE items (
  item_code  CHAR(6)       NOT NULL COMMENT '備品コード',
  item_name  VARCHAR(100)  NOT NULL COMMENT '備品名',
  category   VARCHAR(10)   NOT NULL DEFAULT 'PC' COMMENT '区分',
  unit_price DECIMAL(10,2)          COMMENT '購入単価',
  note       TEXT                   COMMENT '備考',
  PRIMARY KEY (item_code),
  CONSTRAINT items_category_check CHECK (category IN ('PC', 'MONITOR', 'ROOM'))
) COMMENT = '備品';
CREATE TABLE reservations (
  reservation_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '予約ID',
  item_code   CHAR(6)   NOT NULL COMMENT '備品コード',
  member_id   BIGINT    NOT NULL COMMENT '利用者ID',
  start_date  DATE      NOT NULL COMMENT '利用開始日',
  end_date    DATE      NOT NULL COMMENT '利用終了日',
  qty         INT       NOT NULL DEFAULT 1 COMMENT '数量',
  status      SMALLINT  NOT NULL DEFAULT 0 COMMENT '状態(0:申請中 1:承認 9:取消)',
  created_at  DATETIME  NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '登録日時',
  PRIMARY KEY (reservation_id),
  KEY reservations_item_period_idx (item_code, start_date, end_date),
  KEY reservations_member_idx (member_id),
  CONSTRAINT reservations_item_fk FOREIGN KEY (item_code) REFERENCES items (item_code),
  CONSTRAINT reservations_member_fk FOREIGN KEY (member_id) REFERENCES members (member_id),
  CONSTRAINT reservations_qty_check CHECK (qty BETWEEN 1 AND 99),
  CONSTRAINT reservations_period_check CHECK (start_date <= end_date)
) COMMENT = '予約';

テーブル一覧と列一覧を取り出す

SELECT TABLE_NAME AS `物理名`, TABLE_COMMENT AS `論理名`, ENGINE AS `エンジン`, TABLE_COLLATION AS `照合順序`
  FROM information_schema.TABLES
 WHERE TABLE_SCHEMA = 't358' AND TABLE_TYPE = 'BASE TABLE'
 ORDER BY TABLE_NAME;
+--------------+-----------+--------------+--------------------+
| 物理名       | 論理名    | エンジン     | 照合順序           |
+--------------+-----------+--------------+--------------------+
| departments  | 部署      | InnoDB       | utf8mb4_0900_ai_ci |
| items        | 備品      | InnoDB       | utf8mb4_0900_ai_ci |
| members      | 利用者    | InnoDB       | utf8mb4_0900_ai_ci |
| reservations | 予約      | InnoDB       | utf8mb4_0900_ai_ci |
+--------------+-----------+--------------+--------------------+

MySQL は information_schema.COLUMNS にコメント(COLUMN_COMMENT)と桁つきの型(COLUMN_TYPE)が入っているので、1つのビューでほぼそろいます。外部キーは KEY_COLUMN_USAGE から取ります。

SELECT c.TABLE_NAME        AS `テーブル`,
       c.ORDINAL_POSITION  AS `No`,
       c.COLUMN_COMMENT    AS `論理名`,
       c.COLUMN_NAME       AS `物理名`,
       c.COLUMN_TYPE       AS `型`,
       IF(c.IS_NULLABLE = 'NO', '不可', '可') AS `NULL`,
       IF(c.EXTRA LIKE '%auto_increment%', '自動採番', IFNULL(c.COLUMN_DEFAULT, '')) AS `既定値`,
       IF(c.COLUMN_KEY = 'PRI', 'PK', '') AS `PK`,
       IFNULL((SELECT GROUP_CONCAT(CONCAT(k.REFERENCED_TABLE_NAME, '.', k.REFERENCED_COLUMN_NAME) SEPARATOR ', ')
          FROM information_schema.KEY_COLUMN_USAGE k
         WHERE k.TABLE_SCHEMA = c.TABLE_SCHEMA AND k.TABLE_NAME = c.TABLE_NAME
           AND k.COLUMN_NAME = c.COLUMN_NAME
           AND k.REFERENCED_TABLE_NAME IS NOT NULL), '') AS `FK`
  FROM information_schema.COLUMNS c
 WHERE c.TABLE_SCHEMA = 't358'
 ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION;
+--------------+----+-------------------------------------------+----------------+---------------+--------+-------------------+----+-----------------------+
| テーブル     | No | 論理名                                    | 物理名         | 型            | NULL   | 既定値            | PK | FK                    |
+--------------+----+-------------------------------------------+----------------+---------------+--------+-------------------+----+-----------------------+
| departments  |  1 | 部署コード                                | dept_code      | char(3)       | 不可   |                   | PK |                       |
| departments  |  2 | 部署名                                    | dept_name      | varchar(50)   | 不可   |                   |    |                       |
| items        |  1 | 備品コード                                | item_code      | char(6)       | 不可   |                   | PK |                       |
| items        |  2 | 備品名                                    | item_name      | varchar(100)  | 不可   |                   |    |                       |
| items        |  3 | 区分                                      | category       | varchar(10)   | 不可   | PC                |    |                       |
| items        |  4 | 購入単価                                  | unit_price     | decimal(10,2) | 可     |                   |    |                       |
| items        |  5 | 備考                                      | note           | text          | 可     |                   |    |                       |
| members      |  1 | 利用者ID                                  | member_id      | bigint        | 不可   | 自動採番          | PK |                       |
| members      |  2 | 部署コード                                | dept_code      | char(3)       | 不可   |                   |    | departments.dept_code |
| members      |  3 | 氏名                                      | member_name    | varchar(40)   | 不可   |                   |    |                       |
| members      |  4 | メールアドレス                            | email          | varchar(254)  | 不可   |                   |    |                       |
| members      |  5 | 有効フラグ                                | is_active      | tinyint(1)    | 不可   | 1                 |    |                       |
| members      |  6 | 登録日時                                  | created_at     | datetime      | 不可   | CURRENT_TIMESTAMP |    |                       |
| reservations |  1 | 予約ID                                    | reservation_id | bigint        | 不可   | 自動採番          | PK |                       |
| reservations |  2 | 備品コード                                | item_code      | char(6)       | 不可   |                   |    | items.item_code       |
| reservations |  3 | 利用者ID                                  | member_id      | bigint        | 不可   |                   |    | members.member_id     |
| reservations |  4 | 利用開始日                                | start_date     | date          | 不可   |                   |    |                       |
| reservations |  5 | 利用終了日                                | end_date       | date          | 不可   |                   |    |                       |
| reservations |  6 | 数量                                      | qty            | int           | 不可   | 1                 |    |                       |
| reservations |  7 | 状態(0:申請中 1:承認 9:取消)            | status         | smallint      | 不可   | 0                 |    |                       |
| reservations |  8 | 登録日時                                  | created_at     | datetime      | 不可   | CURRENT_TIMESTAMP |    |                       |
+--------------+----+-------------------------------------------+----------------+---------------+--------+-------------------+----+-----------------------+

BOOLEAN と書いた is_active が tinyint(1)、既定値の TRUE が 1 になっています。MySQL では BOOLEAN が TINYINT(1) の別名として扱われるためです。

索引一覧とCHECK制約を取り出す

SELECT TABLE_NAME AS `テーブル`,
       INDEX_NAME AS `索引名`,
       CASE WHEN INDEX_NAME = 'PRIMARY' THEN '主キー'
            WHEN NON_UNIQUE = 0 THEN '一意' ELSE '通常' END AS `種類`,
       GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ', ') AS `列`
  FROM information_schema.STATISTICS
 WHERE TABLE_SCHEMA = 't358'
 GROUP BY TABLE_NAME, INDEX_NAME, NON_UNIQUE
 ORDER BY TABLE_NAME, INDEX_NAME;
+--------------+------------------------------+-----------+---------------------------------+
| テーブル     | 索引名                       | 種類      | 列                              |
+--------------+------------------------------+-----------+---------------------------------+
| departments  | PRIMARY                      | 主キー    | dept_code                       |
| items        | PRIMARY                      | 主キー    | item_code                       |
| members      | members_dept_fk              | 通常      | dept_code                       |
| members      | members_email_key            | 一意      | email                           |
| members      | PRIMARY                      | 主キー    | member_id                       |
| reservations | PRIMARY                      | 主キー    | reservation_id                  |
| reservations | reservations_item_period_idx | 通常      | item_code, start_date, end_date |
| reservations | reservations_member_idx      | 通常      | member_id                       |
+--------------+------------------------------+-----------+---------------------------------+

members に members_dept_fk という索引があります。これは DDL には書いていませんが、MySQL(InnoDB)が外部キーの列に自動で作った索引 です。PostgreSQL の結果には外部キーの列の索引は無かったので、定義書の索引一覧は実際のデータベースから取り出して確かめると確実です。

SELECT t.TABLE_NAME AS `テーブル`, t.CONSTRAINT_NAME AS `制約名`, c.CHECK_CLAUSE AS `定義`
  FROM information_schema.TABLE_CONSTRAINTS t
  JOIN information_schema.CHECK_CONSTRAINTS c
    ON c.CONSTRAINT_SCHEMA = t.CONSTRAINT_SCHEMA AND c.CONSTRAINT_NAME = t.CONSTRAINT_NAME
 WHERE t.TABLE_SCHEMA = 't358' AND t.CONSTRAINT_TYPE = 'CHECK'
 ORDER BY t.TABLE_NAME, t.CONSTRAINT_NAME;
+--------------+---------------------------+-----------------------------------------------------------------------+
| テーブル     | 制約名                    | 定義                                                                  |
+--------------+---------------------------+-----------------------------------------------------------------------+
| items        | items_category_check      | (`category` in (_utf8mb4\'PC\',_utf8mb4\'MONITOR\',_utf8mb4\'ROOM\')) |
| reservations | reservations_period_check | (`start_date` <= `end_date`)                                          |
| reservations | reservations_qty_check    | (`qty` between 1 and 99)                                              |
+--------------+---------------------------+-----------------------------------------------------------------------+

CSVとMarkdownの表に書き出すPython

SQLの結果をファイルに保存し、Python でテーブルごとの Markdown の表と、表計算ソフトで開ける CSV に変換します。データベースへの接続ライブラリは使わず、各データベースのコマンドが出力したファイルを読む形にしているので、Python の標準ライブラリだけで動きます。

まず、列一覧のSQLの結果をファイルにします。接続先やユーザーの指定は環境に合わせてください。

# PostgreSQL:CSVで出力
psql -d t358 --csv -f pg_columns.sql > columns_pg.csv

# MySQL:タブ区切りで出力
mysql --batch --default-character-set=utf8mb4 t358 < my_columns.sql > columns_mysql.tsv

変換するスクリプトです。

import csv
import sys
from itertools import groupby

def read_rows(path):
    delimiter = "\t" if path.endswith(".tsv") else ","
    with open(path, encoding="utf-8", newline="") as f:
        return list(csv.DictReader(f, delimiter=delimiter))

def cell(value):
    return (value or "").replace("|", "\|").replace("\n", " ")

def to_markdown(rows, key="テーブル"):
    lines = []
    for table, group in groupby(rows, key=lambda r: r[key]):
        group = list(group)
        headers = [h for h in group[0] if h != key]
        lines.append(f"### {table}")
        lines.append("")
        lines.append("| " + " | ".join(headers) + " |")
        lines.append("|" + "---|" * len(headers))
        for r in group:
            lines.append("| " + " | ".join(cell(r[h]) for h in headers) + " |")
        lines.append("")
    return "\n".join(lines)

def to_csv(rows, path):
    with open(path, "w", encoding="utf-8-sig", newline="") as f:   # BOMつきで表計算ソフトが文字コードを判別しやすい
        writer = csv.DictWriter(f, fieldnames=list(rows[0]))
        writer.writeheader()
        writer.writerows(rows)

if __name__ == "__main__":
    src, out_md, out_csv = sys.argv[1], sys.argv[2], sys.argv[3]
    rows = read_rows(src)
    with open(out_md, "w", encoding="utf-8") as f:
        f.write(to_markdown(rows))
    to_csv(rows, out_csv)
    tables = sorted({r["テーブル"] for r in rows})
    print(f"{src}: {len(tables)}テーブル {len(rows)}列 -> {out_md}, {out_csv}")
python3 make_docs.py columns_pg.csv tables_pg.md tables_pg_excel.csv
python3 make_docs.py columns_mysql.tsv tables_mysql.md tables_mysql_excel.csv
columns_pg.csv: 4テーブル 21列 -> tables_pg.md, tables_pg_excel.csv
columns_mysql.tsv: 4テーブル 21列 -> tables_mysql.md, tables_mysql_excel.csv

PostgreSQL から作った tables_pg.md のうち、予約テーブルの部分です。

### reservations

| No | 論理名 | 物理名 | 型 | NULL | 既定値 | PK | FK |
|---|---|---|---|---|---|---|---|
| 1 | 予約ID | reservation_id | bigint | 不可 | 自動採番 | PK |  |
| 2 | 備品コード | item_code | character(6) | 不可 |  |  | items.item_code |
| 3 | 利用者ID | member_id | bigint | 不可 |  |  | members.member_id |
| 4 | 利用開始日 | start_date | date | 不可 |  |  |  |
| 5 | 利用終了日 | end_date | date | 不可 |  |  |  |
| 6 | 数量 | qty | integer | 不可 | 1 |  |  |
| 7 | 状態(0:申請中 1:承認 9:取消) | status | smallint | 不可 | 0 |  |  |
| 8 | 登録日時 | created_at | timestamp without time zone | 不可 | CURRENT_TIMESTAMP |  |  |

MySQL から作った tables_mysql.md のうち、利用者テーブルの部分です。

### members

| No | 論理名 | 物理名 | 型 | NULL | 既定値 | PK | FK |
|---|---|---|---|---|---|---|---|
| 1 | 利用者ID | member_id | bigint | 不可 | 自動採番 | PK |  |
| 2 | 部署コード | dept_code | char(3) | 不可 |  |  | departments.dept_code |
| 3 | 氏名 | member_name | varchar(40) | 不可 |  |  |  |
| 4 | メールアドレス | email | varchar(254) | 不可 |  |  |  |
| 5 | 有効フラグ | is_active | tinyint(1) | 不可 | 1 |  |  |
| 6 | 登録日時 | created_at | datetime | 不可 | CURRENT_TIMESTAMP |  |  |

CSV は BOM つきの UTF-8(utf-8-sig)で書き出しています。表計算ソフトで開いたときに文字コードが判別されやすくなります。

型の書き方のDB別の違い早見表

同じ DDL を PostgreSQL 16 と MySQL 8.0 の両方で実行し、それぞれが型をどう表示するかを確かめた結果です。定義書をデータベースから逆生成すると、この表の「表示」の列の書き方で出てきます。

DDLに書いた型 PostgreSQL 16 の表示 MySQL 8.0 の表示
INTEGER integer int
BIGINT bigint bigint
SMALLINT smallint smallint
NUMERIC(10,2) numeric(10,2) decimal(10,2)
CHAR(3) character(3) char(3)
VARCHAR(50) character varying(50) varchar(50)
TEXT text text
BOOLEAN boolean tinyint(1)
DATE date date
TIME time without time zone time
TIMESTAMP timestamp without time zone timestamp
REAL real double
DOUBLE PRECISION double precision double

片方のデータベースだけで使える書き方も確かめました。

書き方 PostgreSQL 16 MySQL 8.0
桁を省いた VARCHAR 作成できる(表示は character varying) 構文エラー(ERROR 1064)
DATETIME エラー(type "datetime" does not exist) 作成できる。DATETIME(6) で小数秒6桁
TIMESTAMPTZ 作成できる(表示は timestamp with time zone) ―
SERIAL integer 型+既定値 nextval('テーブル名_列名_seq'::regclass) になる ―
JSONB、UUID、BYTEA 作成できる ―
INT(11) ― 作成できるが表示は int。警告1681(表示幅の指定は非推奨)が出る
TINYINT UNSIGNED、ENUM('PC','ROOM')、BLOB、LONGTEXT、JSON ― 作成できる
自動採番 GENERATED ALWAYS AS IDENTITY AUTO_INCREMENT
論理名(コメント) COMMENT ON TABLE/COMMENT ON COLUMN 文 列・テーブルの定義に COMMENT を書く

定義書の「型」は、採用するデータベースの表記にそろえるか、VARCHAR(50)・NUMERIC(10,2) のような短い表記に統一して凡例に書く と読みやすくなります。たとえば PostgreSQL の timestamp without time zone は、定義書では TIMESTAMP と書き、凡例に「タイムゾーンなし」と添える方法があります。

手書きと逆生成の使い分け

場面 おすすめの進め方
設計の初期(テーブルがまだ無い) テンプレートに手で書き、レビューする
DDL を作って開発用データベースに反映した後 SQLで逆生成し、手書きの定義書と見比べる
運用中のシステムの定義書を整える 逆生成した表を土台にして、備考と概要を人が書き足す
定義書を常に最新にしておきたい 論理名をコメントで管理し、変更のたびに逆生成する

逆生成で取り出せるのは「いまのデータベースがどうなっているか」です。なぜその型にしたのか、コード値が何を意味するのかといった設計の意図は、備考として人が書く 部分になります。コード値の意味をコメントに入れておけば(例:状態(0:申請中 1:承認 9:取消))、逆生成の結果にもそのまま出てきます。

テーブル定義書のチェックリスト

No 確認すること
1 テーブル一覧と各テーブルの定義の数が一致している
2 すべてのテーブルと列に論理名がある
3 同じ意味の列は、テーブルが違っても論理名・物理名・型・桁が同じ
4 型の表記が定義書全体で統一されている
5 NULL の凡例(不可/可)が書いてある
6 既定値は固定値・関数・自動採番の区別がつく
7 主キーがすべてのテーブルにあり、複合主キーは順番がわかる
8 外部キーの参照先と型・桁が一致している
9 索引一覧に、列の並び順と用途が書いてある
10 コード値の意味と CHECK 制約が備考にある
11 実際のデータベースから逆生成した結果と食い違いがない

よくある質問

Q. テーブル定義書とER図はどう使い分けますか? A. ER図はテーブルどうしの関係を見渡すための図、テーブル定義書は1テーブルの列を細かく確かめるための表です。関係はER図、型・桁・NULL・既定値は定義書、と役割を分けて両方を用意します。

Q. 論理名をコメントに入れていないデータベースでも逆生成できますか? A. できます。論理名の列が空になるだけで、物理名・型・NULL・既定値・PK・FK・索引は取り出せます。取り出した表に論理名を書き足し、あわせて COMMENT ON COLUMN(MySQL では列定義の COMMENT)で登録しておくと、次回からは論理名も取り出せます。

Q. 「桁」は型と分けて書くべきですか? A. どちらでも構いませんが、定義書の中で統一します。逆生成の結果は VARCHAR(50) のように型と一体で出てくるので、型に含める書き方にすると手直しが少なくなります。桁を別の列にしたい場合は、PostgreSQL・MySQL とも information_schema.columns の character_maximum_length、numeric_precision、numeric_scale から取れます。

Q. 定義書とデータベースが食い違っていたら、どちらを正にしますか? A. 動いているデータベースが現在の事実です。逆生成した結果を見て、意図した変更なら定義書を直し、意図していない差分ならデータベース側を直します。変更のたびに逆生成して見比べる手順にしておくと、差分が小さいうちに気づけます。

まとめ

  • テーブル定義書は「テーブル一覧+1テーブル1枚」 で、列一覧・索引一覧・制約一覧を持たせる
  • 列一覧の項目は No・論理名・物理名・型・桁・NULL・既定値・PK・FK・備考 の10個が基本
  • 論理名をコメントに入れておく と、定義書の表をSQLでそのまま取り出せる
  • PostgreSQL は pg_catalog と format_type、MySQL は information_schema.COLUMNS が逆生成の中心になる
  • 同じ DDL でも表示される型はデータベースごとに違う(BOOLEAN が MySQL では tinyint(1) など)ので、定義書の表記を決めて凡例に書く
  • 外部キーの列の索引は MySQL では自動で作られ、PostgreSQL では作られない。索引一覧は実際のデータベースから取り出して確かめる
  • 設計の意図とコード値の意味は人が書く。逆生成は土台づくりと食い違いの確認に使う

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「テーブル定義書の書き方とテンプレート DDLから自動生成するSQL」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?