🖼️ 主キーの図解 — 3 枚で理解する
主キーの役割を 3 つの図で視覚化する。 SVG を base64 化したインライン画像で、 外部依存なしに表示される。
図 1 は地域マスターと人口サマリの主キー−外部キー関係、 図 2 は主キーが満たすべき 3 つの制約 (一意性・非NULL・不変性)、 図 3 は自然キー (pref_code, isbn 等) と代理キー (UUID, SEQ 等) のトレードオフを示す。
「主キー」を取り巻く中核キーワード群です。 検索やインデックス作成で参照する際の手がかりにしてください。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になります。
🍰 まずはやさしく
主キーはデータの背番号のようなものです。
データを迷わず見つけるために使います。
スマホの連絡先にある個別のIDが例です。
まずは主キーの結論をまとめます。
最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:
🍰 まずはやさしく
主キーはデータの間違いを防ぐ番人です。
同じデータが混ざるのを防ぐために使います。
部活の名簿で名前が重なる時に役立ちます。
主キーをどこで使うのかを説明します。
「同じユーザーの行が 3 行ある」 「ID が NULL」 — どちらもデータが壊れているサイン。 主キーがちゃんと定義されていれば、 DB が自動で防いでくれます。
このページの読み方:まず 30秒結論 と 直感 を読み、 必要に応じて 数式 や 計算例、 落とし穴 に進んでください。
🍰 まずはやさしく
主キーは世界に一つだけの印です。
誰と誰かを正しく区別するために使います。
出席簿の学籍番号がちょうどこの例です。
主キーを選ぶコツを直感的に学びます。
クラスの出席簿に喩えると:
主キーは「絶対に重複しない」 「必ず値がある」 「後から変わらない」 ものを選ぶのが鉄則。
SSDSE-B-2026 の各レコードは「年 × 都道府県」で一意に決まる。 つまり (SSDSE 年, Code, Prefecture) の組合せが「複合主キー」になっている。 単独の Code 列 (R01000 = 北海道, R13000 = 東京都, ...) は 都道府県表のみでは主キーだが、 SSDSE-B-2026 では 12 年分 × 47 県 = 564 行あるため Code 単独では重複する。
| 候補 | 一意? | 理由 |
|---|---|---|
| Prefecture (都道府県名) | 否 | "東京都" は 2012 年・2013 年・...・2023 年で 12 回出現 |
| Code (R01000 など) | 否 | 同上。 年方向に重複 |
| (SSDSE 年, Code) | 是 | 複合主キー。 12 年 × 47 県 = 564 行すべて一意 |
| (SSDSE 年, Prefecture) | 是 | 同等に主キーとして機能 (Code と Prefecture は 1:1 対応) |
これを SQL で表現するなら PRIMARY KEY (year, code)。 pandas なら df.set_index(['SSDSE', 'Code'])。 主キーが定義されていれば、 重複削除も「年次集計のための GROUP BY」も「他テーブルとの JOIN」も安全に行える。 例えば、 同じ複合主キーを持つ別表 (A1101 だけの抜粋 vs A1303 だけの抜粋) を merge(on=['SSDSE','Code']) で結合すれば、 行ズレなく統合できる。
🍰 まずはやさしく
主キーは数学的なルールで決まります。
正しくデータを管理するために使います。
買い物のレシートにある番号のようなものです。
主キーの定義を詳しく解説します。
主キーは「関係(テーブル)$R$ の各行を一意に識別する属性集合 $K$」として、 集合論的に定義される。 記号の意味は次節で言葉に翻訳する。
属性集合 $K \subseteq \mathrm{attr}(R)$ が主キーであるとは、 次の 3 条件をすべて満たすことをいう。
$$\forall\, t_1, t_2 \in R:\quad t_1 \neq t_2 \;\Longrightarrow\; t_1[K] \neq t_2[K]$$
(一意性:異なる 2 行は $K$ の値も必ず異なる。 対偶を取れば「$t_1[K]=t_2[K] \Rightarrow t_1=t_2$」、 すなわち $K$ の値だけで行を完全に特定できる。)
$$\forall\, t \in R,\; \forall\, a \in K:\quad t[a] \neq \mathrm{NULL} \qquad (\text{非 NULL 性})$$
$$\forall\, K' \subsetneq K:\quad K' \text{ は一意性を満たさない} \qquad (\text{極小性})$$
ここで $t_1, t_2$ は $R$ の任意の行(タプル)、 $t_1[K]$ は行 $t_1$ における属性集合 $K$ の値を表す。 一意性のみを満たす集合はスーパーキー、 極小性も満たせば候補キー、 その候補キーの中から 1 つ選んだものが主キー(primary key)である。 具体的な値での計算例は後続セクションを参照。
主キー(Primary Key, PK)は、 リレーショナルデータベースのテーブルの「各行を一意に識別する属性または属性の組」。 SSDSE-B-2026.csv の場合、 (年度, 地域コード) の複合主キーが各レコードを一意に決めます。 PK は単なる「行番号」ではなく、 NULL 不可・一意性保証・自動インデックスといった3 つの絶対要件を満たします。
| キーの種類 | 定義 | SSDSE-B-2026 での例 |
|---|---|---|
| スーパーキー | 一意性を持つ属性集合(最小性は不問) | (年度, 地域コード, 都道府県) |
| 候補キー | 最小性を満たすスーパーキー | (年度, 地域コード), (年度, 都道府県) |
| 主キー(PK) | 候補キーから 1 つ選んだもの | (年度, 地域コード) |
| 代替キー | PK 以外の候補キー | (年度, 都道府県) |
| 自然キー | 業務上の意味を持つキー | (年度, 地域コード)、 マイナンバー、 ISBN |
| 代理キー(surrogate key) | 人工的な ID(AUTO_INCREMENT) | SSDSE-B には標準で含まれない、 必要なら追加 |
SSDSE-B-2026 は 47 都道府県 × 12 年(2012-2023) = 564 行。 主キー候補:
地域コード(例:R13000=東京都)は ISO 3166-2:JP 規格に対応し、 国際標準との互換性も高い。 これが「(年度, 地域コード)」を主キーに選ぶ実務的根拠。
SSDSE-B-2026 を SQLite にロードする際、 (年度, 地域コード) を主キーに指定することで、 「2023 年の東京都を取得」というクエリが O(log n) に高速化。 564 行では体感差は小さいが、 主キー設計の練習として最適。 また主キーがあれば INSERT OR REPLACE で冪等更新も可能。
| 業界 | 事例 | 主キーの選び方 | SSDSE-B-2026 との対比 |
|---|---|---|---|
| 銀行 | 口座管理 | 口座番号(10 桁) | 口座 = レコード単位、 SSDSE-B では (年度, 地域コード) |
| EC | 注文管理 | 注文 ID(UUID または AUTO_INCREMENT) | SSDSE-B も人工キー追加可能 |
| 医療 | 電子カルテ | 患者 ID(自治体ベース ID) | マイナンバーは外部 PK 候補 |
| 政府 | マイナンバー DB | マイナンバー 12 桁 | 個人レベルの一意識別子 |
| SaaS | マルチテナント DB | (テナント ID, レコード ID) 複合 PK | SSDSE-B の (年度, 地域) と類似 |
| 分析基盤 | BigQuery / Snowflake | PK 制約なし、 ロジカル PK | SSDSE-B BigQuery 取込でも同様 |
| 方式 | 例 | 長所 | 短所 |
|---|---|---|---|
| 自然キー(複合) | (年度, 地域コード) | 業務意味あり、 外部システムと連携易 | 変更困難、 JOIN コスト |
| 自然キー(単独) | マイナンバー、 ISBN | シンプル | 業務変更で揺らぐ |
| 代理キー(連番) | AUTO_INCREMENT id | シンプル・高速・JOIN 軽量 | 意味なし、 シャーディング困難 |
| 代理キー(UUID) | '550e8400-e29b-41d4-a716-...' | 分散システム可、 衝突しない | 16 byte で重い、 インデックス局所性低 |
| ULID / Snowflake ID | 時系列順 UUID | UUID 利点 + 局所性改善 | 導入コスト |
| ハッシュキー | SHA-256 of natural key | 長さ固定 | 業務トレース困難 |
都道府県 単独で PK」だと未登録が発生し NULL 混入、 一意性破綻。 NOT NULL を必ず指定。UNIQUE constraint failed。 INSERT OR REPLACE や ON CONFLICT 句で対応。1 2 3 4 5 6 7 8 | import sqlite3, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) # 日本語の列名で読む(2 行目を見出しにする) conn = sqlite3.connect(':memory:') conn.execute('''CREATE TABLE ssdse_b ( 年度 INTEGER, 地域コード TEXT, 都道府県 TEXT, 総人口 INTEGER, PRIMARY KEY (年度, 地域コード))''') df[['年度','地域コード','都道府県','総人口']].to_sql('ssdse_b', conn, if_exists='append', index=False) print(conn.execute("SELECT COUNT(*) FROM ssdse_b").fetchone()) # (564,) |
💬 (564,) が返り、主キー (年度, 地域コード) の組み合わせで 564 行すべてが重複なく入った。地域コードは 47 種類、各県が 12 年度ずつなので 47×12=564 で、どちらか一方だけを主キーにすると 2 行目以降が一意制約違反で挿入に失敗する。複合主キーは「この表の 1 行は何を表すか(県×年度)」をそのまま宣言したものと読める。
UNIQUE constraint failed エラー。 対策:(1) INSERT OR IGNORE(既存を無視)、 (2) INSERT OR REPLACE(上書き)、 (3) ON CONFLICT (年度, 地域コード) DO UPDATE(PostgreSQL 互換)。
🎯 やること:SSDSE-B-2026 を「都道府県マスタ」「地方区分マスタ」「統計事実」の 3 テーブルに正規化、 主キーと外部キーで参照整合性を確立、 UPSERT で冪等更新を実装する。
📥 入力:SSDSE-B-2026.csv 全 564 行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import sqlite3, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) # 地方区分マッピング BLOCKS = { '北海道': '北海道', '青森県':'東北','岩手県':'東北','宮城県':'東北','秋田県':'東北','山形県':'東北','福島県':'東北', '茨城県':'関東','栃木県':'関東','群馬県':'関東','埼玉県':'関東','千葉県':'関東','東京都':'関東','神奈川県':'関東', '新潟県':'中部','富山県':'中部','石川県':'中部','福井県':'中部','山梨県':'中部','長野県':'中部','岐阜県':'中部','静岡県':'中部','愛知県':'中部', '三重県':'近畿','滋賀県':'近畿','京都府':'近畿','大阪府':'近畿','兵庫県':'近畿','奈良県':'近畿','和歌山県':'近畿', '鳥取県':'中国','島根県':'中国','岡山県':'中国','広島県':'中国','山口県':'中国', '徳島県':'四国','香川県':'四国','愛媛県':'四国','高知県':'四国', '福岡県':'九州','佐賀県':'九州','長崎県':'九州','熊本県':'九州','大分県':'九州','宮崎県':'九州','鹿児島県':'九州','沖縄県':'九州', } conn = sqlite3.connect(':memory:') conn.execute('PRAGMA foreign_keys = ON') # 1. 地方区分マスタ conn.execute('''CREATE TABLE 地方区分 ( 名称 TEXT PRIMARY KEY )''') conn.executemany('INSERT INTO 地方区分 VALUES (?)', [(b,) for b in sorted(set(BLOCKS.values()))]) # 2. 都道府県マスタ conn.execute('''CREATE TABLE 都道府県マスタ ( 地域コード TEXT PRIMARY KEY, 都道府県 TEXT NOT NULL UNIQUE, 地方区分 TEXT NOT NULL, FOREIGN KEY (地方区分) REFERENCES 地方区分(名称) )''') master = df[['地域コード','都道府県']].drop_duplicates().copy() master['地方区分'] = master['都道府県'].map(BLOCKS) master.to_sql('都道府県マスタ', conn, if_exists='append', index=False) # 3. 統計事実 conn.execute('''CREATE TABLE 統計事実 ( 年度 INTEGER NOT NULL, 地域コード TEXT NOT NULL, 総人口 INTEGER, 出生数 INTEGER, PRIMARY KEY (年度, 地域コード), FOREIGN KEY (地域コード) REFERENCES 都道府県マスタ(地域コード) )''') df[['年度','地域コード','総人口','出生数']].to_sql( '統計事実', conn, if_exists='append', index=False) # 集計クエリ:地方区分別人口(3 表 JOIN) result = conn.execute(''' SELECT 区.名称 AS 地方, COUNT(*) AS 件数, SUM(s.総人口) AS 合計人口 FROM 統計事実 s JOIN 都道府県マスタ m USING (地域コード) JOIN 地方区分 区 ON m.地方区分 = 区.名称 WHERE s.年度 = 2023 GROUP BY 区.名称 ORDER BY 合計人口 DESC ''').fetchall() print(f"{'地方':4s} {'県数':>4s} {'合計人口':>12s}") print('-' * 30) for region, n, total in result: print(f"{region:4s} {n:>4d} {total:>12,}") |
📤 実行結果:
💬 結果の読み方:3 表構造(地方区分マスタ → 都道府県マスタ → 統計事実)で 3NF を満たし、 FK で参照整合性を保証。 「地方区分・地域コード」が他表に存在しない値の挿入は DB レベルで拒否される。 集計クエリは 3 表 JOIN で実現し、 関東 4,352.7 万人(全国 1 億 2,435.3 万人の 35.0%)、 近畿 2,199.0 万人、 中部 2,074.9 万人の上位 3 ブロックで全国の 69.4% を占める。 県数の欄が 7・7・9・8・6・5・1・4 で合計 47 になっているのは、 2023 年に絞った 47 行が都道府県マスタとの JOIN で 1 行も落ちなかったことの確認にもなる。 主キーと外部キーの組合せが、 業務要件と性能の両立を可能にする。
都道府県統計テーブルの主キー設計:
| パターン | 主キー | 評価 |
|---|---|---|
| A | 都道府県名 | △ 名称変更リスク |
| B | 都道府県コード(2桁) | ○ JIS 規格で安定 |
| C | (コード, 年) 複合キー | ◎ 年次データに最適 |
| D | 自動採番 id | ○ 代理キー、 シンプル |
合成データで UUID と数値 ID の衝突確率を比較する。
| ID 種別 | 空間 N | n=1M 件衝突確率 |
|---|---|---|
| 32bit 整数 | 4.3e9 | n²/(2N) = 116.4 → 1 − e^(−116.4) ≈ 1.0000 (ほぼ確実に衝突) |
| UUID v4 | 2^122 | ≈ 9.4e-26 (極小) |
1 2 3 4 5 6 7 8 9 10 | import numpy as np n = 1e6 N_32 = 2**32 N_uuid = 2**122 x32 = n**2 / (2 * N_32) # 期待衝突ペア数 n²/(2N) xuuid = n**2 / (2 * N_uuid) p32 = -np.expm1(-x32) # 衝突確率 1 - exp(-n²/(2N)) puuid = -np.expm1(-xuuid) # expm1 で極小値も桁落ちせずに計算 print(f"32bit: n²/(2N) = {x32:.1f} → 衝突確率 {p32:.4f}") print(f"UUID : n²/(2N) = {xuuid:.2e} → 衝突確率 {puuid:.2e}") |
💬 32bit 整数の空間 約 43 億に 100 万件を入れると n²/(2N) は 116.4 で、 衝突する組が平均 116 組ある計算になる。 近似式 n²/(2N) をそのまま確率と読むと 1 を超えてしまうので、 1 − e^(−116.4) で見ると衝突確率は事実上 1。 UUID v4 では 9.40e-26 で、 n²/(2N) が小さいこの領域では近似と厳密式が一致する。 32bit のランダム ID は 10 万件(n²/(2N) ≈ 1.16、 衝突確率 約 69%)でもすでに危ない。
2 つの表をキー列 $k$ で結合したときの行数は、 キーの値ごとの行数の積の和で決まる。
$$\text{結合後の行数} = \sum_{k} n_L(k)\, n_R(k)$$
$n_L(k)$ は左の表でキーが $k$ の行数、 $n_R(k)$ は右の表での行数。 結合に使う列が「主キー」なら $n_L(k)=1$ なので、 結合後の行数は右の表の行数を超えない。 主キーでない列で結合すると、 積の分だけ行が増える。
青森県・岩手県・宮城県で確かめる。 左は 2023 年度の総人口(1 県 1 行)、 右は 2022・2023 年度の出生数(1 県 2 行)。
| 地域コード | 左: 2023 年度 総人口 | 右: 出生数 2022 / 2023 | $n_L$ | $n_R$ | $n_L n_R$ |
|---|---|---|---|---|---|
| R02000 青森県 | 1,184,000 | 5,985 / 5,696 | 1 | 2 | 2 |
| R03000 岩手県 | 1,163,000 | 5,788 / 5,432 | 1 | 2 | 2 |
| R04000 宮城県 | 2,264,000 | 12,852 / 12,328 | 1 | 2 | 2 |
🎯 このコードでやること:手計算の Step 1〜5 を pandas の value_counts と merge で再現し、 予言した行数と実際の行数が一致するか確かめる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.rename(columns={'SSDSE-B-2026': '年度', 'Code': '地域コード', 'A1101': '総人口'}) tohoku = ['R02000', 'R03000', 'R04000'] # 青森・岩手・宮城 L = df[(df['年度'] == 2023) & df['地域コード'].isin(tohoku)][['地域コード', '総人口']] R = df[df['年度'].isin([2022, 2023]) & df['地域コード'].isin(tohoku)][['年度', '地域コード', 'A4101']] # Step 1: キーごとの行数 n_L(k), n_R(k) nL = L['地域コード'].value_counts().sort_index() nR = R['地域コード'].value_counts().sort_index() print(pd.DataFrame({'n_L': nL, 'n_R': nR, 'n_L×n_R': nL * nR})) # Step 2: 予言 = Σ n_L(k)·n_R(k) print('予言した行数:', int((nL * nR).sum())) # Step 3: 実際に結合して確かめる print('地域コードだけで結合:', len(L.merge(R, on='地域コード'))) # 右側を 2 年度とも持つ表どうしを地域コードだけで結合すると L2 = R[['年度', '地域コード']].copy() print('両側 2 年度・地域コードだけ:', len(L2.merge(R, on='地域コード')), '行') print('両側 2 年度・(年度, 地域コード):', len(L2.merge(R, on=['年度', '地域コード'])), '行') # 全 47 県・12 年度どうしで同じことをすると full = df[['年度', '地域コード']] print('全体・地域コードだけ:', len(full.merge(full, on='地域コード')), '行 = 47 × 12 × 12') print('全体・(年度, 地域コード):', len(full.merge(full, on=['年度', '地域コード'])), '行') |
💬 予言した 6 行と実際の結合結果 6 行が一致し、 手計算の Step 4・5 の 12 行・6,768 行・564 行もそのまま出た。 結合の前に「結合に使う列が片側で主キーになっているか」を確かめておけば、 結合後の行数を先に言い当てられる。 地域コードだけで全体どうしを結合した 6,768 行は、 同じ県の 12 年度 × 12 年度の組み合わせがすべて作られた結果で、 どの行も分析には使えない。
最小再現コード。 SSDSE-B のような実データを前提に、 4〜8 行で動く例です:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import sqlite3 conn = sqlite3.connect(':memory:') cur = conn.cursor() cur.execute('''CREATE TABLE prefecture_stats ( code CHAR(2), year INT, tfr REAL, PRIMARY KEY (code, year) )''') cur.execute("INSERT INTO prefecture_stats VALUES ('13', 2023, 0.99)") # 東京都 2023 年度の合計特殊出生率 # 重複は拒否される try: cur.execute("INSERT INTO prefecture_stats VALUES ('13', 2023, 9.99)") except sqlite3.IntegrityError as e: print('拒否:', e) |
💬 同じ ('13', 2023) の組をもう一度入れようとすると IntegrityError になり、エラー文に code と year の 2 列が並んでいるので、複合主キーの組が衝突したことが分かる。2 回目の tfr=9.99 のように値が違っても拒否されるので、主キーは「中身の重複」ではなく「同じ県・同じ年度の行が 2 つある」ことを防いでいる。値を更新したいときは INSERT ではなく UPDATE か INSERT OR REPLACE を使う。
🎯 このコードでやること:SSDSE-B-2026.csv の 564 行を SQLite に格納、 (年度, 地域コード) を PRIMARY KEY に指定し、 一意性が保たれることを確認する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd, sqlite3 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) cols = ['年度','地域コード','都道府県','総人口','出生数'] conn = sqlite3.connect(':memory:') conn.execute('''CREATE TABLE ssdse_b ( 年度 INTEGER NOT NULL, 地域コード TEXT NOT NULL, 都道府県 TEXT NOT NULL, 総人口 INTEGER, 出生数 INTEGER, PRIMARY KEY (年度, 地域コード) )''') df[cols].to_sql('ssdse_b', conn, if_exists='append', index=False) n = conn.execute('SELECT COUNT(*) FROM ssdse_b').fetchone()[0] print(f"ロード行数: {n}(期待値 47 × 12 = 564)") print(f"PK 列: {[r[1] for r in conn.execute('PRAGMA table_info(ssdse_b)') if r[5]]}") |
📤 実行結果:
💬 結果の読み方:564 行が一切重複なくロードされた=(年度, 地域コード) が確かに一意である証拠。 SQLite は PK 列に自動で B-tree インデックスを構築、 以降の「2023 年東京都」検索は O(log n) で 1〜2 ノード参照のみ。 NOT NULL 制約も同時に課されているため、 欠損値混入も防げる。
🎯 このコードでやること:既存の (2023, R13000) レコードに新値を INSERT すると PK 違反エラー。 INSERT OR REPLACE で冪等更新できることを確認。
📥 入力データ:上記 ssdse_b テーブル。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import sqlite3, pandas as pd # 上記コード 1 と同じセットアップ df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) conn = sqlite3.connect(':memory:') conn.execute('''CREATE TABLE ssdse_b ( 年度 INTEGER NOT NULL, 地域コード TEXT NOT NULL, 都道府県 TEXT, 総人口 INTEGER, 出生数 INTEGER, PRIMARY KEY (年度, 地域コード))''') df[['年度','地域コード','都道府県','総人口','出生数']].to_sql( 'ssdse_b', conn, if_exists='append', index=False) # 通常 INSERT は失敗 try: conn.execute("INSERT INTO ssdse_b VALUES (2023, 'R13000', '東京都', 14100000, 86000)") except sqlite3.IntegrityError as e: print(f"❌ 普通の INSERT: {e}") # INSERT OR REPLACE は成功 conn.execute("INSERT OR REPLACE INTO ssdse_b VALUES (2023, 'R13000', '東京都', 14100000, 86000)") row = conn.execute("SELECT 総人口 FROM ssdse_b WHERE 年度=2023 AND 地域コード='R13000'").fetchone() print(f"✅ INSERT OR REPLACE 後の総人口: {row[0]:,}") |
📤 実行結果:
💬 結果の読み方:PK 違反は UNIQUE constraint failed として明示される。 INSERT OR REPLACE(PostgreSQL では ON CONFLICT ... DO UPDATE)で冪等な upsertが可能。 これがバッチ更新の鉄板パターン。 既存値(14,086,000)が新値(14,100,000)に置換された。
🎯 このコードでやること:SSDSE-B-2026 を「都道府県マスタ」と「統計事実テーブル」に分割(3NF)、 FK で参照整合性を保証、 JOIN で元の形を復元できることを確認。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import sqlite3, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) conn = sqlite3.connect(':memory:') conn.execute('PRAGMA foreign_keys = ON') # マスタテーブル(PK = 地域コード) conn.execute('CREATE TABLE 都道府県マスタ (地域コード TEXT PRIMARY KEY, 都道府県 TEXT NOT NULL)') master = df[['地域コード','都道府県']].drop_duplicates() master.to_sql('都道府県マスタ', conn, if_exists='append', index=False) # 事実テーブル(PK = (年度, 地域コード), FK = 地域コード) conn.execute('''CREATE TABLE 統計事実 ( 年度 INTEGER, 地域コード TEXT, 総人口 INTEGER, PRIMARY KEY (年度, 地域コード), FOREIGN KEY (地域コード) REFERENCES 都道府県マスタ(地域コード))''') df[['年度','地域コード','総人口']].to_sql('統計事実', conn, if_exists='append', index=False) # 元の形を JOIN で復元 top3 = conn.execute(''' SELECT s.年度, m.都道府県, s.総人口 FROM 統計事実 s JOIN 都道府県マスタ m USING(地域コード) WHERE s.年度=2023 ORDER BY s.総人口 DESC LIMIT 3 ''').fetchall() for r in top3: print(r) |
📤 実行結果:
💬 結果の読み方:3NF への正規化が成功。 都道府県マスタ(47 行)と統計事実(564 行)に分かれ、 FK 制約で「存在しない地域コードの統計レコード挿入」を DB レベルで拒否。 JOIN で元の形が完全復元され、 2023 年人口 TOP3 は東京・神奈川・大阪。 主キーと外部キーの組合せが「整合性」と「柔軟性」を両立する仕組み。
ON UPDATE CASCADE で自動化可。MERGE 文で upsert。UPDATE sqlite_sequence SET seq=0 WHERE name='t'。 MySQL: ALTER TABLE t AUTO_INCREMENT=1。_id 自動付与(ObjectId 12 バイト)。 SSDSE-B では地域コード+年度を _id 文字列に。PRAGMA table_info(ssdse_b)(SQLite)、 \d ssdse_b(psql)、 SHOW INDEX FROM ssdse_b(MySQL)。| 年 | 出来事 | 主キーへの影響 |
|---|---|---|
| 1968 | IBM IMS(階層型 DB) | 「ポインタ識別」が主流 |
| 1970 | Codd 関係モデル論文 | 「値ベース識別」=主キー概念の創始 |
| 1979 | Oracle V2 リリース | 商用 RDBMS で PK 制約実装 |
| 1986 | SQL-86 標準化 | PRIMARY KEY 構文確立 |
| 1989 | SQL-89 | 外部キーと PK の関係制約 |
| 1996 | UUID(RFC 4122) | 分散システム向け代理キー |
| 2000 年代 | NoSQL の台頭 | 「PK 不要」議論、 ロジカル PK |
| 2016 | ULID | 時系列順 UUID |
| 2010 年代 | Snowflake ID(Twitter) | 分散 + 順序性 |
| 2020 年代 | クラウド DWH(BigQuery, Snowflake) | PK 制約なしのデファクト化 |
関係 $R$ の主キー $K \subseteq \text{attr}(R)$ は以下を満たす:
一意性のみだとスーパーキー、 最小性も満たせば候補キー、 そこから DBA が選んだ 1 つが主キー。
| RDBMS | PK 実装 | 自動採番 | 注意点 |
|---|---|---|---|
| PostgreSQL | B-tree インデックス + UNIQUE + NOT NULL | SERIAL, BIGSERIAL, IDENTITY (SQL 標準) | シーケンスは独立オブジェクト |
| MySQL (InnoDB) | クラスタ化インデックス(PK 順に物理配置) | AUTO_INCREMENT | PK の選択でストレージ局所性に影響大 |
| SQLite | INTEGER PRIMARY KEY は rowid のエイリアス | AUTOINCREMENT(rowid 再利用しない) | テキスト PK は別途インデックス必要 |
| Oracle | B-tree + 制約 | SEQUENCE + TRIGGER または IDENTITY (12c+) | — |
| SQL Server | クラスタ化インデックス(デフォルト) | IDENTITY | — |
| BigQuery | 制約なし(論理 PK のみ) | — | 重複防止は MERGE 文または ETL 側 |
| MongoDB | _id 自動付与(ObjectId 12 バイト) | 自動 | カスタム _id 可 |
| Cassandra | パーティションキー + クラスタリングキー | — | 分散システム特化設計 |
CREATE TABLE ssdse_b (
年度 INTEGER NOT NULL,
地域コード TEXT NOT NULL,
総人口 INTEGER,
PRIMARY KEY (年度, 地域コード)
);
長所:業務的意味あり、 重複自然に防止、 e-Stat と整合。 短所:JOIN 時に列が増える、 値変更困難。
CREATE TABLE ssdse_b (
id INTEGER PRIMARY KEY AUTOINCREMENT,
年度 INTEGER NOT NULL,
地域コード TEXT NOT NULL,
総人口 INTEGER,
UNIQUE (年度, 地域コード)
);
長所:単一列で軽量、 JOIN シンプル、 PK 値が業務に左右されない。 短所:人工的、 業務的意味なし、 BigQuery 等で AUTOINCREMENT 不対応。
CREATE TABLE ssdse_b (
id TEXT PRIMARY KEY, -- UUID
年度 INTEGER NOT NULL,
地域コード TEXT NOT NULL,
総人口 INTEGER,
UNIQUE (年度, 地域コード)
);
長所:分散システム対応、 衝突しない、 シャーディング容易。 短所:16 バイトで重い、 ランダム UUID では INSERT 性能低下、 人間が読めない。
-- 年度 + 地域コードのハッシュを PK に
CREATE TABLE ssdse_b (
pk_hash TEXT PRIMARY KEY, -- SHA-256(年度||地域コード)[:16]
年度 INTEGER NOT NULL,
地域コード TEXT NOT NULL,
総人口 INTEGER
);
用途:データウェアハウス(dbt 等)でステージング層から事実テーブルへの変換に。
PK には自動で B-tree インデックスが構築される。 B-tree の特性:
SSDSE-B-2026 を 3NF 正規化した場合:
-- 都道府県マスタ
CREATE TABLE 都道府県マスタ (
地域コード TEXT PRIMARY KEY,
都道府県 TEXT NOT NULL UNIQUE,
地方区分 TEXT NOT NULL
);
-- 統計事実
CREATE TABLE 統計事実 (
年度 INTEGER NOT NULL,
地域コード TEXT NOT NULL,
総人口 INTEGER,
出生数 INTEGER,
PRIMARY KEY (年度, 地域コード),
FOREIGN KEY (地域コード) REFERENCES 都道府県マスタ(地域コード)
ON UPDATE CASCADE
ON DELETE RESTRICT
);
これで:
| 選択 | 性能特性 | SSDSE-B-2026 適性 |
|---|---|---|
| INTEGER 単独 PK | 最速、 4-8 バイト | 代理キー追加なら最適 |
| 複合 PK (INT, TEXT) | JOIN 列数増加、 でも明快 | (年度, 地域コード) で標準 |
| TEXT 単独 PK | 比較遅い、 領域大 | 地域コード単独は不可(年度で重複) |
| UUID(ランダム) | INSERT 5〜10 倍遅い、 16 バイト | 分散環境で必要時のみ |
| UUID v7 / ULID | UUID + 局所性改善 | 将来拡張に推奨 |
CREATE TABLE ... PRIMARY KEY主キーは列の名前ではなく「1 行が何を表すか」で決まる。 SSDSE-B-2026 は 1 行 = 県 × 年度 の横長の表だが、 指標を縦に並べ直すと 1 行 = 県 × 年度 × 指標 になり、 主キーに指標の列が加わる。
🎯 このコードでやること:横長の表を melt で縦長に変え、 (年度, 地域コード) と (年度, 地域コード, 指標) の重複数を比べる。 pivot で元の形に戻せることも確かめる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.rename(columns={'SSDSE-B-2026': '年度', 'Code': '地域コード', 'Prefecture': '都道府県'}) key = ['年度', '地域コード'] print('横長の表:', df.shape, '/ (年度, 地域コード) の重複', int(df.duplicated(subset=key).sum())) # 縦長(1 行 = 県 × 年度 × 指標)に変換する long = df.melt(id_vars=['年度', '地域コード', '都道府県'], var_name='指標', value_name='値') print('縦長の表:', long.shape) print('(年度, 地域コード) の重複:', int(long.duplicated(subset=key).sum())) print('(年度, 地域コード, 指標) の重複:', int(long.duplicated(subset=key + ['指標']).sum())) print(long[(long['地域コード'] == 'R13000') & (long['年度'] == 2023)].head(3).to_string(index=False)) # 縦長のまま「地域コード × 年度」で横に戻すと、キーが正しければ元の形に戻る back = long.pivot(index=key, columns='指標', values='値') print('pivot で戻した形:', back.shape) |
💬 縦長にすると 564 × 109 = 61,476 行になり、 (年度, 地域コード) の重複は 60,912 件(= 61,476 − 564)に跳ね上がる。 指標を加えた 3 列の組では重複が 0 なので、 これが縦長の表の主キー。 pivot で (564, 109) に戻せたのも、 3 列の組が一意だからで、 重複があると pivot は ValueError で止まる。
主キー を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。
※ 上記は文献調査・現場経験で報告される頻度の高い注意点。 ドメインや手法のバージョンによって追加の落とし穴がある場合があります。
主キー制約の無い pandas の DataFrame では、 同じ県・同じ年度の行が 2 回入っても何も起きない。 値が訂正されて少し違っていると、 全列一致を見る drop_duplicates() ではすり抜ける。
🎯 このコードでやること:2023 年度の 47 行に、 東京都の行を「訂正版」として値を変えてもう 1 行足し、 全列での重複削除と主キーでの重複検出を比べる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.rename(columns={'SSDSE-B-2026': '年度', 'Code': '地域コード', 'Prefecture': '都道府県', 'A1101': '総人口'}) t = df[df['年度'] == 2023][['年度', '地域コード', '都道府県', '総人口']] print('正しい全国合計:', f"{t['総人口'].sum():,}") # 東京都 2023 年度の行が「訂正版」として二重に取り込まれた状況を再現(14,100,000 は説明用の仮の値) fix = t[t['地域コード'] == 'R13000'].assign(総人口=14_100_000) t2 = pd.concat([t, fix], ignore_index=True) print('行数:', len(t2)) print('drop_duplicates() 後の行数:', len(t2.drop_duplicates())) # 値が違うので消えない print('二重のまま合計:', f"{t2['総人口'].sum():,}") # 主キーで重複を検出する key = ['年度', '地域コード'] print('キーの重複:', int(t2.duplicated(subset=key).sum()), '件') try: t2.set_index(key, verify_integrity=True) except ValueError as e: print('verify_integrity:', str(e).splitlines()[0]) # 「後から来た訂正版を正とする」と決めて 1 行に絞る t3 = t2.drop_duplicates(subset=key, keep='last') print('keep=last 後:', len(t3), '行, 合計', f"{t3['総人口'].sum():,}") |
💬 48 行のうち東京都の 2 行は総人口が違うので、 drop_duplicates() を通しても 48 行のまま。 合計は 138,453,000 人で、 正しい 124,353,000 人より 1,410 万人多い。 主キー (年度, 地域コード) で見れば重複 1 件として見つかり、 verify_integrity=True はその組 (2023, R13000) を名指しで止める。 どちらを残すかは「後から来た訂正版を正とする」のような約束で決め、 keep='last' で 47 行・124,367,000 人に戻る。
主キーの値が同じに見えても、 片方が整数 2023、 もう片方が文字列 "2023" だと別の値として扱われる。 地域コードから県番号を切り出して整数にすると、 先頭の 0 が消えて "01" と 1 が一致しなくなる。
🎯 このコードでやること:年度の型が違う 2 つの表を結合したときのエラーと、 県番号の先頭の 0 が消えたときに何行が結合から漏れるかを確かめる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df.rename(columns={'SSDSE-B-2026': '年度', 'Code': '地域コード', 'Prefecture': '都道府県'}) pop = df[df['年度'] == 2023][['年度', '地域コード', 'A1101']] # 別の表を文字列のまま読んだ状況(CSV を dtype=str で読むとこうなる) birth = df[df['年度'] == 2023][['年度', '地域コード', 'A4101']].astype({'年度': str}) print(pop['年度'].dtype, birth['年度'].dtype) try: pop.merge(birth, on=['年度', '地域コード']) except ValueError as e: print('ValueError:', str(e)[:70]) # 型をそろえれば 47 行で 1 対 1 に結合できる m = pop.merge(birth.astype({'年度': int}), on=['年度', '地域コード'], validate='one_to_one') print('型をそろえた結合:', len(m), '行') # 地域コードから 2 桁の県番号を作るとき、整数にすると先頭の 0 が消える pop['県番号_int'] = pop['地域コード'].str[1:3].astype(int) pop['県番号_str'] = pop['地域コード'].str[1:3] print(pop[['地域コード', '県番号_int', '県番号_str']].head(3).to_string(index=False)) master = pd.DataFrame({'県番号': ['01', '02', '13'], '名称': ['北海道', '青森県', '東京都']}) print('文字列どうし:', len(pop.merge(master, left_on='県番号_str', right_on='県番号')), '行') print('整数を文字列にしただけ:', len(pop.assign(k=pop['県番号_int'].astype(str)).merge(master, left_on='k', right_on='県番号')), '行') print('zfill(2) で 0 を戻す:', len(pop.assign(k=pop['県番号_int'].astype(str).str.zfill(2)).merge(master, left_on='k', right_on='県番号')), '行') |
💬 年度が int64 と object(文字列)の表は、 pandas が結合の前に ValueError で止めてくれる。 型をそろえると 47 行が 1 対 1 で結合できた。 県番号は整数にすると 01 が 1 になり、 文字列に戻しただけでは "1" と "01" が一致しないため、 マスタ 3 行のうち東京都の 13 だけが結合して 1 行になる。 こちらはエラーが出ずに行が静かに消えるので、 zfill(2) で桁をそろえてから結合し、 結合後の行数を確かめる。
主キー (Primary Key) を中心に、 上位概念 (RDB スキーマ設計・関係モデル)、 並列概念 (外部キー・候補キー・代理キー・複合キー)、 制約 (一意性 + 非 NULL + 不変性)、 応用 (JOIN・正規化・SSDSE-B の SSDSE-2026 列など) を関係づけて整理する。
主キー (primary key) はリレーショナルデータベースで「行を一意に特定する 1 列または列の組合せ」で、 NOT NULL かつ UNIQUE 制約が強制される。 SSDSE-B-2026 では「年度(CSV 1 行目の列名は SSDSE-B-2026)+ 地域コード」が複合主キーに相当し、 これによって「同じ都道府県の同じ年のレコードは 1 つだけ」が保証される。 自然キー (意味のある値)・代理キー (UUID/ULID/連番) のどちらを使うか、 クラスタリングインデックスとして物理配置を決めるかなどの設計判断が、 後の JOIN 性能・データ統合・落とし穴の発生確率に直結する。
主キーは「テーブル設計 → データ整合性 → 分析」の連鎖で中心的な役割を果たし、 隣接する DB 設計概念と密接に絡む。
on= パラメータも主キー対応列を指定、 データ統合 で異なるシステム間の同一エンティティを突合する基準。主キーが設計されていないテーブルは「重複行が混入する」「JOIN 結果が膨らむ」「集計値が二重計上される」という致命的な分析バグを生み、 SSDSE-B-2026 のような分析用データセットでも (都道府県, 年) の組が暗黙の主キーとして扱われている。
「主キー」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
SSDSE-B-2026 は 47 県 × 12 年度 = 564 行。 「地域コードが主キー」と決めつけると 1 県あたり 12 行が重複扱いになり、 結合で行数が 12 倍に膨らむ。
主キーの役割を 3 つの図で視覚化する。 SVG を base64 化したインライン画像で、 外部依存なしに表示される。
図 1 は地域マスターと人口サマリの主キー−外部キー関係、 図 2 は主キーが満たすべき 3 つの制約 (一意性・非NULL・不変性)、 図 3 は自然キー (pref_code, isbn 等) と代理キー (UUID, SEQ 等) のトレードオフを示す。
下の架空のサンプル名簿(氏名・メール等はすべて作例)で、列見出しをタップ/クリックして主キー候補に指定してみてください。複数の列を選ぶと複合キーになります。選んだ瞬間に「一意性チェック(重複=赤)」と「NULL チェック(オレンジ)」が走り、主キーとして妥当かが即座に判定されます。
| ID | 氏名 | メール | 生年月日 |
|---|
一意性の判定式:選択キーの「異なり値の数」=「行数」かつ NULL 数 = 0 のとき合格。下の図は操作に合わせてリアルタイム更新されます。
主キーは行を一意に特定する背番号です。名前で選手を呼ぶと同姓同名で混乱しますが、背番号で呼べば必ず 1 人に届く。上のデモで「氏名」が赤くなるのはまさにこの混乱で、データベースは「どちらの佐藤 陽菜さんですか?」に答えられなくなります。さらに背番号は空欄不可(NULL は「不明」を意味し、不明どうしは等しいとも等しくないとも言えないため比較が破綻する)、シーズン途中で変えない(不変性)——この 3 点セットが主キーの要件です。
ここまでのセクションで主キーの 3 要件(一意・非 NULL・不変)と複合キー (年度, 地域コード) の設計を学びました。 この深化セクションでは一歩進めて、 「観測された一意性」と「設計として保証された一意性」はまったく別物であることを、 SSDSE-B-2026 の実測値で定量的に確かめます。 これは上の診断ラボ(架空名簿)で体験した「今たまたま一意」問題の、 実データ版・統計版です。
SSDSE-B-2026 の 2023 年断面(47 都道府県 = 47 行)で、 年度・地域コード・都道府県名を除く数値 109 列それぞれの「異なり値の数」を数えると、 実に 74 列(約 67.9%)が nunique = 47、 つまりその瞬間だけ見れば全行一意です。 総人口(A1101)も 47 通りで完全に一意。 では「総人口を主キーにできる」でしょうか?
答えは No。 断面を 2012〜2023 年の全パネル 564 行に広げると、 総人口の異なり値は 520 に減り、 44 行が他の行と値を共有します。 実際の衝突例(すべて実測):
主要列の「断面 vs パネル」一意性を並べると、 観測一意性がいかに脆いかが分かります:
| 列 | 2023 年断面の異なり値 (47 行中) | パネル全体の異なり値 (564 行中) | 判定 |
|---|---|---|---|
| Code(地域コード) | 47 | 47 | 断面ではキー、 パネルでは 12 重に重複 |
| A1101 総人口 | 47 | 520 | 偶然の一意性。 キー不適 |
| B4101 年平均気温 | 31 | 101 | 断面ですら 16 組が衝突 |
| A4103 合計特殊出生率 | 31 | 82 | 桁が粗く衝突多発 |
| E6101 短期大学数 | 16 | 36 | 「5 校」が 8 県で同値 |
| (年度, Code) 複合 | 47 | 564 | 設計上のキー。 全行一意 |
つまり主キーとは「手元のデータで重複が無かった」という観測報告ではなく、 「将来追加される行も含めて、 DB が衝突を拒否し続ける」という約束(制約)です。 観測はいくらでも裏切られますが、 約束はスキーマが守ります。
df['A1101'].nunique() == len(df) は 2023 年断面なら True です。 しかしこの検査は一意性の必要条件を確認しただけで、 十分条件ではありません。 翌年分を concat した瞬間に崩壊し得ます(実際、 総人口は 564 行に広げると 44 行が衝突)。 キーの資格は「その列の値が何を識別する意図で振られたか」という意味論で決めるべきものです。df.duplicated(subset=['SSDSE-B-2026','Code']).sum() は現在の SSDSE-B-2026 で 0(実測)。 もしこれが 0 でなくなったら、 それはキー設計の誤りではなく二重ロードや結合ミスの検出器が鳴ったということです。 pandas には SQL の PRIMARY KEY 制約が無いので、 set_index([...], verify_integrity=True) や merge(..., validate='one_to_one') を ETL の各段に置いて、 約束を手動で検査し続ける必要があります。「ドキュメントの無い未知のテーブルから候補キーを自動で見つける」問題は、 データプロファイリング研究でキー発見・関数従属発見と呼ばれる一分野です(代表的アルゴリズムに TANE や HyFD)。 列の組合せは $2^{n}$ 通りに爆発するため、 単独列 → 2 列 → 3 列と段階的に持ち上げる格子(lattice)探索と枝刈りが使われます。 SSDSE-B-2026 は 112 列なので全探索は $2^{112}$ 通り—— 現実には「一意率 = nunique / 行数」が高い列から優先的に組合せる、 という発想が要になります(実測:Code の一意率は 47/564 ≈ 0.083、 (年度, Code) で 1.0 に到達)。
もう 1 つの発展は「不変のはずのキーが変わる」問題です。 都道府県コードは長期に安定した良質な自然キーですが、 市町村コードは平成の大合併期に大量に改番されました。 時系列データでキーの対応関係そのものが変化する場合の管理手法は、 データウェアハウス分野で Slowly Changing Dimension(SCD)として体系化されています。 「主キーの不変性」は無料で手に入る性質ではなく、 コード表の版管理という運用努力の産物です。