コード設計とは、備品コードや予約状態のように「値で種類や個体を見分ける項目」について、体系(付け方)・桁数・値の一覧・管理方法を決めて、コード定義書にまとめることです。 コードは一度使い始めると、データにも画面にも帳票にも残ります。あとから桁を増やしたり意味を変えたりするのは手間がかかるので、最初に「どの体系で、何桁で、どこで管理するか」を決めておくことが大切です。

この記事では、コード体系5種類の比較早見表、コピーして使えるコード定義書テンプレートと記入例、桁数の見積もり計算、チェックディジットの計算をまとめました。区分値をデータベースでどう持つか(汎用コードマスタ・個別テーブル・CHECK制約・enum)は、DDLを実際に流して、規則に合わない値を入れたときのエラー文まで比べています。題材は架空の「社内の備品予約システム」です。

確認環境:Ubuntu 24.04、Python 3.11.15、PostgreSQL 16.15、MySQL 8.0.46。

コード体系5種類の比較早見表

種類 付け方 例 桁数の目安 拡張の余地 向く場面
順番コード(シーケンス) 発生した順に連番を振る 予約番号 000123 件数から計算(後述) 桁が埋まるまで追加できる。途中への挿入はできない 件数が多く、値に意味が要らないもの(予約、利用者、明細)
区分コード(ブロック) 番号の範囲をグループに割り当て、範囲内で連番 備品 1000〜1999 は映像機器、2000〜2999 は事務機器 グループ数×グループ内の最大件数 グループ内は空きがある限り追加できる。枠を超えると割り当て直し グループが少なく、件数の上限が読めるもの
桁別コード 桁ごとに意味を持たせる 設置場所 203B(棟1桁・階2桁・区画1文字) 意味の数×各桁の桁数 各桁の取りうる数まで。意味の追加は桁の追加になる 階層がはっきりしていて、上位の桁で絞り込みたいもの
表意コード(ニーモニック) 名称を連想できる略号にする 備品分類 PJ(プロジェクター)、CM(カメラ) 2〜5文字 略号が重ならない限り追加できる 種類が少なく、人が見て覚えたいもの(区分値、状態)
合成コード 上の体系を組み合わせる 備品コード PJ00123(分類+連番+検査用の1桁) 各部分の合計 部分ごとの余地に従う 人が見て種類がわかり、個体も特定したいもの

迷ったときの選び方は次のとおりです。

知りたいこと 選ぶ体系
件数が多く、増え続ける 順番コード
コードを見ただけで種類を知りたい 表意コード、または合成コード
上位の桁で検索・集計したい 桁別コード
種類が数個〜数十個の区分値 表意コード、または10刻みの順番コード
手入力や読み上げがある 合成コードにチェックディジットを付ける

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

(記載例)出典:株式会社RJC「コード設計の書き方 区分値・コード体系の一覧と定義書テンプレート」

コード設計の手順

手順 すること 決まるもの
1 コード化する項目を洗い出す コード一覧(コードID・名称)
2 使われ方を調べる(件数、増え方、手入力の有無、並べ替え、他システムとの受け渡し) 体系を選ぶ材料
3 体系を選ぶ 順番・区分・桁別・表意・合成のどれか
4 桁数と文字の種類を決める 桁数、型(文字列/数値)、チェックディジットの有無
5 値と表示名、表示順、有効期間を決める コード値定義
6 管理方法を決める コードマスタ、個別テーブル、CHECK制約、enum のどれか
7 採番・追加・廃止のルールを決める 採番の担当、欠番の扱い、廃止の手続き

コード定義書テンプレート

コード定義書は「コード一覧」と「コード値定義」の2枚に分けると見通しがよくなります。

コード一覧(どんなコードがあるか)

コードID コード名 体系 桁数 型 管理方法 採番ルール 備考
RSV_STATUS 予約状態 順番(10刻み) 2 文字列 コードマスタ 追加は管理者が申請。間の番号を使う 状態遷移図と合わせて管理
ITEM_CATEGORY 備品分類 表意 2 文字列 コードマスタ 英大文字2文字。既存と重ならない略号 備品コードの先頭2文字にも使う
ITEM_CD 備品コード 合成 7 文字列 備品テーブルの主キー 分類2文字+連番4桁+検査用1桁 連番は分類ごとに 0001 から
RSV_NO 予約番号 順番 6 文字列 予約テーブルの主キー 登録時に自動採番。欠番は再利用しない 先頭をゼロで埋める
LOCATION_CD 設置場所コード 桁別 4 文字列 個別テーブル 棟1桁+階2桁+区画1文字 例:203B
コードID   コード名    体系  桁数  型   管理方法    採番ルール   備考
RSV_STATUS  予約状態    順番(10刻み)    2   文字列 コードマスタ  追加は管理者が申請。間の番号を使う   状態遷移図と合わせて管理
ITEM_CATEGORY   備品分類    表意  2   文字列 コードマスタ  英大文字2文字。既存と重ならない略号  備品コードの先頭2文字にも使う
ITEM_CD 備品コード   合成  7   文字列 備品テーブルの主キー  分類2文字+連番4桁+検査用1桁    連番は分類ごとに 0001 から
RSV_NO  予約番号    順番  6   文字列 予約テーブルの主キー  登録時に自動採番。欠番は再利用しない  先頭をゼロで埋める
LOCATION_CD 設置場所コード 桁別  4   文字列 個別テーブル  棟1桁+階2桁+区画1文字   例:203B

コード値定義(それぞれの値と意味)

コードID 名称 値 表示名 表示順 有効開始日 有効終了日 備考
RSV_STATUS 仮予約 10 仮予約 1 2000-01-01 9999-12-31 承認待ち
RSV_STATUS 予約確定 20 確定 2 2000-01-01 9999-12-31
RSV_STATUS 貸出中 30 貸出中 3 2000-01-01 9999-12-31
RSV_STATUS 返却済み 40 返却済 4 2000-01-01 9999-12-31
RSV_STATUS 取消 90 取消 9 2000-01-01 9999-12-31 利用者または管理者が取消
ITEM_CATEGORY パソコン PC PC 1 2000-01-01 9999-12-31
ITEM_CATEGORY プロジェクター PJ プロジェクター 2 2000-01-01 9999-12-31
ITEM_CATEGORY カメラ CM カメラ 3 2000-01-01 9999-12-31
ITEM_CATEGORY ファクス FX ファクス 4 2000-01-01 2025-03-31 2025年3月末で貸出終了

表計算ソフトに貼るときは、次のタブ区切りを使います。値の列は「文字列」の書式にしてから貼ると、01 のような先頭のゼロが残ります。

コードID   名称  値   表示名 表示順 有効開始日   有効終了日   備考
RSV_STATUS  仮予約 10  仮予約 1   2000-01-01  9999-12-31  承認待ち
RSV_STATUS  予約確定    20  確定  2   2000-01-01  9999-12-31  
RSV_STATUS  貸出中 30  貸出中 3   2000-01-01  9999-12-31  
RSV_STATUS  返却済み    40  返却済 4   2000-01-01  9999-12-31  
RSV_STATUS  取消  90  取消  9   2000-01-01  9999-12-31  利用者または管理者が取消
ITEM_CATEGORY   パソコン    PC  PC  1   2000-01-01  9999-12-31  
ITEM_CATEGORY   プロジェクター PJ  プロジェクター 2   2000-01-01  9999-12-31  
ITEM_CATEGORY   カメラ CM  カメラ 3   2000-01-01  9999-12-31  
ITEM_CATEGORY   ファクス    FX  ファクス    4   2000-01-01  2025-03-31  2025年3月末で貸出終了

各列の意味と書き方です。

列 書くこと 決め方のポイント
コードID コードの種類を表す英字の名前 テーブルの列名やプログラムの定数名と対応させる
名称 正式な名前 設計書や帳票で使う言い方
値 データベースに入る値 桁数をそろえる。使い終えた値を別の意味で再利用しない
表示名 画面の選択肢に出す短い名前 選択欄の幅に収まる長さにする
表示順 選択肢の並び順 値の順と別に持つと、あとから並びだけを変えられる
有効開始日・終了日 選択肢に出す期間 廃止するときは行を消さず、終了日を入れる
備考 補足、廃止の理由、関連する仕様 「いつ・なぜ」を残す

予約状態を 10, 20, 30… と10刻みにしているのは、「貸出中」と「返却済み」の間に 35 を足す、といった追加に備えるためです。取消を 90 に離しているのは、通常の流れと例外を見分けやすくするためです。

桁数の見積もり計算

桁数は「いまの件数+増える件数」に余裕を掛け、それを表せる最小の桁数で決めます。

  • 必要件数 =(いまの件数 + 1年の増加 × 使う年数)× 余裕の係数
  • 容量 = 1桁に使える文字の種類数 の 桁数乗

備品が3,200件あり、年に400件増え、15年使う想定で計算します。

"""コードの桁数を見積もる。必要な件数から、文字の種類ごとの桁数と余裕を出す。"""
import math


def need_digits(count, kinds):
    """count 件を表すのに必要な桁数(kinds は1桁に使える文字の種類数)"""
    digits = 1
    while kinds ** digits < count:
        digits += 1
    return digits


current = 3200        # いまの件数
per_year = 400        # 1年あたりの増加
years = 15            # 使い続ける年数
margin = 1.5          # 見込み違いに備える係数

plain = current + per_year * years
required = math.ceil(plain * margin)
print(f"想定件数: {current} + {per_year} × {years} = {plain}")
print(f"余裕込み: {plain} × {margin} = {required}")

for label, kinds in [("数字のみ(10種)", 10),
                     ("英大文字のみ(26種)", 26),
                     ("数字+英大文字(36種)", 36),
                     ("紛らわしい I・O・0・1 を除く(32種)", 32)]:
    d = need_digits(required, kinds)
    capacity = kinds ** d
    print(f"{label}: {d}桁(容量 {capacity:,} 件、使用率 {required / capacity:.1%})")

print()
print("数字のみの桁数と容量")
for d in range(3, 7):
    capacity = 10 ** d
    left = (capacity - current) / per_year
    print(f"{d}桁: {capacity:>9,} 件  いまの増え方で埋まるまで {left:>7,.1f} 年")
$ python3 digits.py
想定件数: 3200 + 400 × 15 = 9200
余裕込み: 9200 × 1.5 = 13800
数字のみ(10種): 5桁(容量 100,000 件、使用率 13.8%)
英大文字のみ(26種): 3桁(容量 17,576 件、使用率 78.5%)
数字+英大文字(36種): 3桁(容量 46,656 件、使用率 29.6%)
紛らわしい I・O・0・1 を除く(32種): 3桁(容量 32,768 件、使用率 42.1%)

数字のみの桁数と容量
3桁:     1,000 件  いまの増え方で埋まるまで    -5.5 年
4桁:    10,000 件  いまの増え方で埋まるまで    17.0 年
5桁:   100,000 件  いまの増え方で埋まるまで   242.0 年
6桁: 1,000,000 件  いまの増え方で埋まるまで 2,492.0 年

数字4桁でも計算上は17年もちますが、想定の9,200件は容量の92%にあたり、増え方が少し速まると足りなくなります。余裕込みの13,800件を表せる5桁にしておくと安心です。3桁の行がマイナスになっているのは、いまの3,200件がすでに容量の1,000件を超えているためです。

合成コードの場合は部分ごとに計算します。備品コード PJ00123 なら、分類は英大文字2文字で 26×26=676 種類、連番は分類ごとに4桁で9,999件です。いちばん件数の多い分類が連番の桁に収まるかを確かめます。

文字の種類 利点 気をつける点
数字のみ テンキーで打てる。読み上げやすい 桁数が増えやすい
英大文字+数字 少ない桁で多く表せる 0 と O、1 と I を見間違えやすい
紛らわしい文字を除く 手書きや読み上げでも取り違えにくい 使える文字の表を定義書に載せる必要がある

チェックディジットの計算(モジュラス10・11)

チェックディジットは、コードの本体から計算した検査用の1桁です。末尾に付けておくと、打ち間違えたコードを入力した時点で見つけられます。代表的な3つの方式を、本体 123456 で計算します。

方式 計算(右端の桁から重みを掛ける) 合計 検査用の桁
モジュラス10 ウェイト3・1 6×3 + 5×1 + 4×3 + 3×1 + 2×3 + 1×1 45 45÷10 の余り 5 → 10−5 = 5
モジュラス10 ウェイト2・1(Luhn) 6×2=12→1+2=3、5、4×2=8、3、2×2=4、1 24 24÷10 の余り 4 → 10−4 = 6
モジュラス11 ウェイト2〜7 6×2 + 5×3 + 4×4 + 3×5 + 2×6 + 1×7 77 77÷11 の余り 0 → 11−0 = 11 → 0

モジュラス11は「11 − 余り」が 10 や 11 になることがあり、その扱いを決めておく必要があります。ここでは 10 を X、11 を 0 としています。

"""チェックディジット(検査用の1桁)を計算し、入力ミスを見つけられる割合を数える。"""
from itertools import product


def mod10_w31(body: str) -> str:
    """モジュラス10・ウェイト3と1:右端の桁から 3,1,3,1… を掛けて合計する"""
    total = sum(int(c) * (3 if i % 2 == 0 else 1) for i, c in enumerate(reversed(body)))
    return str((10 - total % 10) % 10)


def mod10_luhn(body: str) -> str:
    """モジュラス10・ウェイト2と1(Luhn):2倍して2桁になったら各桁を足す"""
    total = 0
    for i, c in enumerate(reversed(body)):
        n = int(c) * (2 if i % 2 == 0 else 1)
        total += n - 9 if n > 9 else n
    return str((10 - total % 10) % 10)


def mod11_w27(body: str) -> str:
    """モジュラス11・ウェイト2〜7:右端の桁から 2,3,…,7,2,3… を掛けて合計する。
    11 − 余り が 10 のときは X、11 のときは 0 とする"""
    total = sum(int(c) * (i % 6 + 2) for i, c in enumerate(reversed(body)))
    value = 11 - total % 11
    return {10: "X", 11: "0"}.get(value, str(value))


METHODS = [("モジュラス10 ウェイト3・1", mod10_w31),
           ("モジュラス10 ウェイト2・1(Luhn)", mod10_luhn),
           ("モジュラス11 ウェイト2〜7", mod11_w27)]

print("本体 123456 のチェックディジット")
for name, func in METHODS:
    print(f"  {name}: {func('123456')} → 末尾に付けて 123456{func('123456')}")

print()
print("4桁の本体すべて(1万通り)で、ミスを見つけられる割合")
for name, func in METHODS:
    single = single_hit = swap = swap_hit = 0
    for digits in product("0123456789", repeat=4):
        body = "".join(digits)
        cd = func(body)
        # 1桁の打ち間違い(本体のどこか1桁が別の数字になる)
        for pos in range(4):
            for other in "0123456789":
                if other == body[pos]:
                    continue
                single += 1
                if func(body[:pos] + other + body[pos + 1:]) != cd:
                    single_hit += 1
        # 隣り合う2桁の入れ替え(同じ数字どうしは除く)
        for pos in range(3):
            if body[pos] == body[pos + 1]:
                continue
            swap += 1
            swapped = body[:pos] + body[pos + 1] + body[pos] + body[pos + 2:]
            if func(swapped) != cd:
                swap_hit += 1
    print(f"  {name}: 1桁の誤り {single_hit / single:.2%}、隣の入れ替え {swap_hit / swap:.2%}")

print()
x = sum(1 for d in product("0123456789", repeat=4) if mod11_w27("".join(d)) == "X")
print(f"モジュラス11で検査桁が X になる本体: 4桁 10000 通りのうち {x} 通り")
$ python3 check_digit.py
本体 123456 のチェックディジット
  モジュラス10 ウェイト3・1: 5 → 末尾に付けて 1234565
  モジュラス10 ウェイト2・1(Luhn): 6 → 末尾に付けて 1234566
  モジュラス11 ウェイト2〜7: 0 → 末尾に付けて 1234560

4桁の本体すべて(1万通り)で、ミスを見つけられる割合
  モジュラス10 ウェイト3・1: 1桁の誤り 100.00%、隣の入れ替え 88.89%
  モジュラス10 ウェイト2・1(Luhn): 1桁の誤り 100.00%、隣の入れ替え 97.78%
  モジュラス11 ウェイト2〜7: 1桁の誤り 100.00%、隣の入れ替え 100.00%

モジュラス11で検査桁が X になる本体: 4桁 10000 通りのうち 909 通り

手計算と同じ値になりました。後半は、4桁の本体1万通りすべてについて「1桁を打ち間違えた場合」と「隣り合う2桁を入れ替えた場合」を総当たりで作り、検査用の桁が変わる(=見つけられる)割合を数えたものです。

方式 1桁の誤り 隣の入れ替え 検査用の桁に出る文字 向く場面
モジュラス10 ウェイト3・1 すべて見つかる 約89% 数字のみ 計算を簡単にしたい。数字だけで完結させたい
モジュラス10 ウェイト2・1(Luhn) すべて見つかる 約98% 数字のみ 数字だけで、入れ替えにも強くしたい
モジュラス11 ウェイト2〜7 すべて見つかる すべて見つかる 数字と X 入れ替えを確実に見つけたい。英字が混ざってもよい

モジュラス11は検出力が高い代わりに、約9%(1万通りのうち909通り)で検査用の桁が X になります。数字だけの入力欄で使うなら、X になる番号を採番しない(欠番にする)と決めるか、モジュラス10を選びます。

備品コードに当てはめると、連番 0012 のモジュラス10 ウェイト3・1 は「2×3 + 1×1 + 0×3 + 0×1 = 7 → 10−7 = 3」なので、プロジェクターの12台目は PJ00123 になります。

区分値の持ち方4種類をDDLで比べる

予約状態のような区分値をデータベースでどう持つかは、主に4つの方法があります。

方法 値の追加 表示名・表示順の保持 規則に合わない値の防止 向く場面
汎用コードマスタ INSERT だけで済む できる(全コード共通の列) 工夫が必要(後述の複合外部キー) 区分値が何十種類もあり、画面から保守したい
個別テーブル INSERT だけで済む できる(コード固有の列も持てる) 外部キーで防げる コードごとに固有の属性がある。参照の整合性を重視する
CHECK制約 制約の作り直し(ALTER TABLE) できない(プログラム側で持つ) CHECK で防げる 値が少なく、ほぼ変わらない
enum 型 型の変更(ALTER TYPE など) できない(値そのものが名前) 型で防げる 値に意味のある名前を使い、プログラムの列挙型と合わせたい

以下は PostgreSQL 16 で実行した結果です。

方法1:汎用コードマスタ

すべての区分値を1つのテーブルに入れます。コード定義書の「コード値定義」とそのまま対応します。

CREATE TABLE code_master (
    code_id    varchar(20)  NOT NULL,            -- コードID(RSV_STATUS など)
    code_value varchar(10)  NOT NULL,            -- 値
    name       varchar(50)  NOT NULL,            -- 名称
    label      varchar(20)  NOT NULL,            -- 表示名(画面の選択肢に出す短い名前)
    sort_order integer      NOT NULL,            -- 表示順
    valid_from date         NOT NULL DEFAULT DATE '2000-01-01',
    valid_to   date         NOT NULL DEFAULT DATE '9999-12-31',
    note       varchar(200),
    PRIMARY KEY (code_id, code_value),
    CONSTRAINT ck_code_master_period CHECK (valid_from <= valid_to)
);

INSERT INTO code_master (code_id, code_value, name, label, sort_order, valid_from, valid_to, note) VALUES
    ('RSV_STATUS', '10', '仮予約',   '仮予約', 1, '2000-01-01', '9999-12-31', '承認待ち'),
    ('RSV_STATUS', '20', '予約確定', '確定',   2, '2000-01-01', '9999-12-31', NULL),
    ('RSV_STATUS', '30', '貸出中',   '貸出中', 3, '2000-01-01', '9999-12-31', NULL),
    ('RSV_STATUS', '40', '返却済み', '返却済', 4, '2000-01-01', '9999-12-31', NULL),
    ('RSV_STATUS', '90', '取消',     '取消',   9, '2000-01-01', '9999-12-31', '利用者または管理者が取消'),
    ('ITEM_CATEGORY', 'PC', 'パソコン',       'PC',           1, '2000-01-01', '9999-12-31', NULL),
    ('ITEM_CATEGORY', 'PJ', 'プロジェクター', 'プロジェクター', 2, '2000-01-01', '9999-12-31', NULL),
    ('ITEM_CATEGORY', 'CM', 'カメラ',         'カメラ',       3, '2000-01-01', '9999-12-31', NULL),
    ('ITEM_CATEGORY', 'FX', 'ファクス',       'ファクス',     4, '2000-01-01', '2025-03-31', '2025年3月末で貸出終了');

使う側のテーブルに値の列だけを置くと、外部キーを張れないため、どんな値でも入ります。

CREATE TABLE reservations_a (
    id        integer PRIMARY KEY,
    item_name varchar(50) NOT NULL,
    status_cd varchar(10) NOT NULL
);
INSERT INTO reservations_a VALUES (1, 'プロジェクター1号機', '20');
INSERT INTO reservations_a VALUES (2, 'カメラ3号機', '99');      -- 定義に無い値
INSERT INTO reservations_a VALUES (3, 'ノートPC5号機', 'PC');    -- 別のコードの値

SELECT r.id, r.item_name, r.status_cd, c.label
FROM reservations_a r
LEFT JOIN code_master c ON c.code_id = 'RSV_STATUS' AND c.code_value = r.status_cd
ORDER BY r.id;
 id |      item_name      | status_cd | label 
----+---------------------+-----------+-------
  1 | プロジェクター1号機 | 20        | 確定
  2 | カメラ3号機         | 99        | 
  3 | ノートPC5号機       | PC        | 
(3 rows)

3件とも登録でき、2件は表示名が空になりました。これを防ぐには、コードIDを固定値で持つ列を足し、2列の複合外部キーにします。

CREATE TABLE reservations_a2 (
    id             integer PRIMARY KEY,
    item_name      varchar(50) NOT NULL,
    status_code_id varchar(20) NOT NULL DEFAULT 'RSV_STATUS',
    status_cd      varchar(10) NOT NULL,
    CONSTRAINT ck_reservations_a2_code_id CHECK (status_code_id = 'RSV_STATUS'),
    CONSTRAINT fk_reservations_a2_status
        FOREIGN KEY (status_code_id, status_cd) REFERENCES code_master (code_id, code_value)
);
INSERT INTO reservations_a2 (id, item_name, status_cd) VALUES (1, 'プロジェクター1号機', '20');
INSERT INTO reservations_a2 (id, item_name, status_cd) VALUES (2, 'カメラ3号機', '99');
INSERT INTO reservations_a2 (id, item_name, status_cd) VALUES (3, 'ノートPC5号機', 'PC');
INSERT 0 1
ERROR:  insert or update on table "reservations_a2" violates foreign key constraint "fk_reservations_a2_status"
DETAIL:  Key (status_code_id, status_cd)=(RSV_STATUS, 99) is not present in table "code_master".
ERROR:  insert or update on table "reservations_a2" violates foreign key constraint "fk_reservations_a2_status"
DETAIL:  Key (status_code_id, status_cd)=(RSV_STATUS, PC) is not present in table "code_master".

定義に無い 99 も、別のコードの値 PC も止まりました。列が1つ増える代わりに、汎用コードマスタのままデータベース側で値を守れます。

方法2:個別テーブル

コードごとにテーブルを作ります。そのコードだけが持つ属性(ここでは「最終状態かどうか」)を列として足せるのが利点です。

CREATE TABLE reservation_statuses (
    status_cd  char(2)     PRIMARY KEY,
    name       varchar(50) NOT NULL,
    sort_order integer     NOT NULL,
    is_final   boolean     NOT NULL DEFAULT false   -- このコードだけが持つ属性
);
INSERT INTO reservation_statuses VALUES
    ('10', '仮予約', 1, false), ('20', '予約確定', 2, false), ('30', '貸出中', 3, false),
    ('40', '返却済み', 4, true), ('90', '取消', 9, true);

CREATE TABLE reservations_b (
    id        integer PRIMARY KEY,
    item_name varchar(50) NOT NULL,
    status_cd char(2) NOT NULL REFERENCES reservation_statuses (status_cd)
);
INSERT INTO reservations_b VALUES (1, 'プロジェクター1号機', '20');
INSERT INTO reservations_b VALUES (2, 'カメラ3号機', '99');
DELETE FROM reservation_statuses WHERE status_cd = '20';
INSERT 0 1
ERROR:  insert or update on table "reservations_b" violates foreign key constraint "reservations_b_status_cd_fkey"
DETAIL:  Key (status_cd)=(99) is not present in table "reservation_statuses".
ERROR:  update or delete on table "reservation_statuses" violates foreign key constraint "reservations_b_status_cd_fkey" on table "reservations_b"
DETAIL:  Key (status_cd)=(20) is still referenced from table "reservations_b".

定義に無い値の登録と、使われている値の削除の両方を外部キーが止めています。

方法3:CHECK制約

値の一覧をテーブル定義の中に書きます。マスタとの結合が要らず、いちばん手軽です。

CREATE TABLE reservations_c (
    id        integer PRIMARY KEY,
    item_name varchar(50) NOT NULL,
    status_cd char(2) NOT NULL,
    CONSTRAINT ck_reservations_c_status CHECK (status_cd IN ('10', '20', '30', '40', '90'))
);
INSERT INTO reservations_c VALUES (1, 'プロジェクター1号機', '20');
INSERT INTO reservations_c VALUES (2, 'カメラ3号機', '99');
INSERT 0 1
ERROR:  new row for relation "reservations_c" violates check constraint "ck_reservations_c_status"
DETAIL:  Failing row contains (2, カメラ3号機, 99).

値を足すときは、制約を外して作り直します。

ALTER TABLE reservations_c DROP CONSTRAINT ck_reservations_c_status;
ALTER TABLE reservations_c ADD CONSTRAINT ck_reservations_c_status
    CHECK (status_cd IN ('10', '20', '30', '35', '40', '90'));
INSERT INTO reservations_c VALUES (2, 'カメラ3号機', '35');
ALTER TABLE
ALTER TABLE
INSERT 0 1

MySQL 8.0.46 でも同じ CHECK制約を定義でき、99 を入れると次のエラーになりました。

ERROR 3819 (HY000) at line 18: Check constraint 'ck_reservations_f_status' is violated.

方法4:enum 型

値に confirmed のような名前を使い、型として定義します。

CREATE TYPE reservation_status AS ENUM ('tentative', 'confirmed', 'lent', 'returned', 'canceled');
CREATE TABLE reservations_d (
    id        integer PRIMARY KEY,
    item_name varchar(50) NOT NULL,
    status    reservation_status NOT NULL
);
INSERT INTO reservations_d VALUES (1, 'プロジェクター1号機', 'returned'), (2, 'カメラ3号機', 'confirmed');
INSERT INTO reservations_d VALUES (3, 'ノートPC5号機', 'overdue');
INSERT 0 2
ERROR:  invalid input value for enum reservation_status: "overdue"
LINE 1: ...T INTO reservations_d VALUES (3, 'ノートPC5号機', 'overdue')...
                                                             ^

値を足すには ALTER TYPE を使います。AFTER で並びの位置も指定できます。

ALTER TYPE reservation_status ADD VALUE 'overdue' AFTER 'lent';
INSERT INTO reservations_d VALUES (3, 'ノートPC5号機', 'overdue');
INSERT INTO reservations_d VALUES (4, 'ノートPC2号機', 'tentative');
SELECT enum_range(NULL::reservation_status);
SELECT id, item_name, status FROM reservations_d ORDER BY status;
                      enum_range                      
------------------------------------------------------
 {tentative,confirmed,lent,overdue,returned,canceled}
(1 row)

 id |      item_name      |  status   
----+---------------------+-----------
  4 | ノートPC2号機       | tentative
  2 | カメラ3号機         | confirmed
  3 | ノートPC5号機       | overdue
  1 | プロジェクター1号機 | returned
(4 rows)

ORDER BY status の結果はアルファベット順ではなく、型に定義した順です。アルファベット順なら最後になる tentative が先頭に来ています。PostgreSQL 16 のドキュメントには、enum の値の順序は型を作ったときに並べた順であること、値の追加と名前の変更はできるが、既存の値の削除と並び順の変更は型を作り直さない限りできないことが書かれています。値を減らす可能性があるコードには、マスタ方式が向いています。

MySQL 8.0.46 の ENUM は、列の定義の中に値を並べます。

CREATE TABLE reservations_e (
    id        int PRIMARY KEY,
    item_name varchar(50) NOT NULL,
    status    ENUM('tentative', 'confirmed', 'lent', 'returned', 'canceled') NOT NULL
);
INSERT INTO reservations_e VALUES (1, 'プロジェクター1号機', 'returned'), (2, 'カメラ3号機', 'confirmed');
INSERT INTO reservations_e VALUES (3, 'ノートPC5号機', 'overdue');
ERROR 1265 (01000) at line 8: Data truncated for column 'status' at row 1

値を足すときは、列の定義を書き直します。

ALTER TABLE reservations_e MODIFY status
    ENUM('tentative', 'confirmed', 'lent', 'overdue', 'returned', 'canceled') NOT NULL;
INSERT INTO reservations_e VALUES (3, 'ノートPC5号機', 'overdue');
SELECT id, item_name, status, status + 0 AS idx FROM reservations_e ORDER BY status;
+----+---------------------+-----------+-----+
| id | item_name           | status    | idx |
+----+---------------------+-----------+-----+
|  2 | カメラ3号機         | confirmed |   2 |
|  3 | ノートPC5号機       | overdue   |   4 |
|  1 | プロジェクター1号機 | returned  |   5 |
+----+---------------------+-----------+-----+

(表の罫線は、全角文字の幅に合わせて整えています。)status + 0 で取り出した番号のとおり、MySQL でも並べ替えは定義した順です。エラー文は「Data truncated」で、値が定義に無いことを直接は言っていない点に気をつけます。上の結果は、初期設定の sql_mode(STRICT_TRANS_TABLES を含む)で実行したものです。

4つの方法の選び方

条件 選ぶ方法
区分値の種類が多く、表示名や並び順を画面から保守したい 汎用コードマスタ(複合外部キーつき)
コードごとに固有の属性がある、他テーブルから参照が多い 個別テーブル
値が2〜5個で、今後もほぼ増えない CHECK制約
プログラムの列挙型と名前をそろえたい、値を減らす予定がない enum 型
値の廃止が起こりうる 汎用コードマスタか個別テーブル(有効期間の列を持つ)

有効期間で「選択肢」と「過去データの表示」を分ける

廃止したコード値の行を消すと、過去のデータの表示名が出せなくなります。行は残して有効終了日を入れ、選択肢を作るときだけ日付で絞り込みます。

-- 2026-10-05 時点で選べる備品分類
SELECT code_value, label
FROM code_master
WHERE code_id = 'ITEM_CATEGORY'
  AND DATE '2026-10-05' BETWEEN valid_from AND valid_to
ORDER BY sort_order;
 code_value |     label      
------------+----------------
 PC         | PC
 PJ         | プロジェクター
 CM         | カメラ
(3 rows)

日付の条件を外すと、貸出を終えた「ファクス」も含めて取り出せます。過去の予約を一覧に出すときは、こちらの形で結合します。

 code_value |     label      |  valid_to  
------------+----------------+------------
 PC         | PC             | 9999-12-31
 PJ         | プロジェクター | 9999-12-31
 CM         | カメラ         | 9999-12-31
 FX         | ファクス       | 2025-03-31
(4 rows)

値を決めるときのルール早見表

ルール 理由 例
コードは文字列型で持つ 数値型にすると先頭のゼロが消える 01 は char(2)、000123 は char(6)
桁数をそろえる 並べ替えと目視の確認がしやすい 1, 2, 10 ではなく 01, 02, 10
順番コードは間を空ける あとから間に追加できる 10, 20, 30
「その他」「不明」は離れた値にする 通常の値が増えても位置が変わらない 99:その他
使い終えた値を別の意味で再利用しない 過去のデータの意味が変わってしまう 廃止は有効終了日で表す
1つのコードに意味を詰め込みすぎない 意味の1つが変わるとコード全体を振り直すことになる 部署や担当者のように変わりやすい情報は別の列で持つ
表示名はコードと別に持つ 名前の変更でデータを直さずに済む 値 20、表示名「確定」
英字は大文字にそろえる 大文字と小文字の取り違えを防ぐ pj ではなく PJ
2値でも将来3値になりそうならコードにする 真偽値から区分値への変更は影響が大きい 「公開/非公開」に「下書き」が加わる場合

よくある質問

Q. 区分値は数字(10, 20)と意味のある文字列(confirmed)のどちらがよいですか? A. 人がデータやログを直接読む機会が多いなら、意味のある文字列が読みやすくなります。帳票や他システムとの受け渡しで桁数が決まっている場合や、表示名を言語ごとに切り替える場合は、短い数字や略号にして表示名をマスタで持つ形が扱いやすくなります。どちらにするかをコード一覧の「体系」欄に書いて、システム全体でそろえることが大切です。

Q. 汎用コードマスタと個別テーブルは混ぜてもよいですか? A. 混ぜて問題ありません。表示名と並び順だけで足りる区分値は汎用コードマスタに、固有の属性を持つものや参照の多いものは個別テーブルに、と基準を決めてコード一覧の「管理方法」欄に書いておきます。

Q. チェックディジットはどんなコードに付けるべきですか? A. 人が手で入力する、電話で読み上げる、紙から転記する、といった使い方があるコードです。画面の選択肢から選ぶだけの区分値や、システムが自動で受け渡す番号には必要ありません。

Q. コードの桁が足りなくなりそうなときはどうしますか? A. 列の桁数を広げて、既存の値の先頭にゼロを足す方法と、新しい桁数の値を新規分から使う方法があります。どちらも画面・帳票・連携先に影響するため、桁数の見積もりに余裕の係数を入れて、最初から1桁多めにしておくのが近道です。

まとめ

  • コード体系は順番・区分・桁別・表意・合成の5種類。件数、意味の要否、手入力の有無で選ぶ
  • コード定義書は「コード一覧」と「コード値定義」の2枚に分け、値・表示名・表示順・有効期間・備考を書く
  • 桁数は(いまの件数+増加×年数)×余裕の係数を表せる最小の桁数で決める
  • チェックディジットは、数字だけならモジュラス10、入れ替えを確実に見つけたいならモジュラス11。モジュラス11は約9%で X が出る
  • 汎用コードマスタは、コードIDの固定列と複合外部キーで、定義に無い値を止められる
  • enum 型は並べ替えが定義順。PostgreSQL では値の削除ができないため、廃止がありうるコードはマスタで持つ
  • 廃止した値は行を消さず、有効終了日で選択肢から外す

参考・出典

  • PostgreSQL 16 Documentation「8.7. Enumerated Types」 https://www.postgresql.org/docs/16/datatype-enum.html

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

(記載例)出典:株式会社RJC「コード設計の書き方 区分値・コード体系の一覧と定義書テンプレート」

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