論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
主キー
Primary Key
データエンジニアリング

🔖 キーワード索引

「主キー」を取り巻く中核キーワード群です。 検索やインデックス作成で参照する際の手がかりにしてください。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になります。

主キーPrimary Key一意性NOT NULL代理キー複合キー外部キーインデックス

💡 30秒で分かる結論 — 主キー

🍰 まずはやさしく

主キーはデータの背番号のようなものです。

データを迷わず見つけるために使います。

スマホの連絡先にある個別のIDが例です。

まずは主キーの結論をまとめます。

最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:

📍 文脈 — どこで出会うか

🍰 まずはやさしく

主キーはデータの間違いを防ぐ番人です。

同じデータが混ざるのを防ぐために使います。

部活の名簿で名前が重なる時に役立ちます。

主キーをどこで使うのかを説明します。

「同じユーザーの行が 3 行ある」 「ID が NULL」 — どちらもデータが壊れているサイン。 主キーがちゃんと定義されていれば、 DB が自動で防いでくれます。

このページの読み方:まず 30秒結論 と 直感 を読み、 必要に応じて 数式 や 計算例、 落とし穴 に進んでください。

🎨 直感で掴む

🍰 まずはやさしく

主キーは世界に一つだけの印です。

誰と誰かを正しく区別するために使います。

出席簿の学籍番号がちょうどこの例です。

主キーを選ぶコツを直感的に学びます。

クラスの出席簿に喩えると:

主キーは「絶対に重複しない」 「必ず値がある」 「後から変わらない」 ものを選ぶのが鉄則。

🎨 SSDSE-B-2026 の主キー構造

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']) で結合すれば、 行ズレなく統合できる。

✅ 理解度チェック

  1. 主キーが満たすべき 3 条件(一意性・非 NULL・不変性)を説明できるか。
  2. 自然キーと代理キー(サロゲートキー)の違いと使い分けを 1 例で挙げられるか。
  3. 複合主キーが必要になる典型シナリオ(売上明細など)を 1 つ示せるか。
  4. 主キーと一意制約・インデックスの関係を区別できるか。

📐 数式または定義

🍰 まずはやさしく

主キーは数学的なルールで決まります。

正しくデータを管理するために使います。

買い物のレシートにある番号のようなものです。

主キーの定義を詳しく解説します。

主キーは「関係(テーブル)$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)である。 具体的な値での計算例は後続セクションを参照。

🔬 数式を言葉で読み解く

一意性 (Uniqueness)
主キー値が等しい 2 行は存在しない。
NOT NULL
NULL は「不明」を意味するため、 NULL の主キーは認められない。
不変性 (Stability)
更新で値が変わらないのが理想。 外部キーから参照されるため変更コストが大きい。
自然キー (Natural Key)
業務上意味のある ID(メールアドレス、 マイナンバー等)。 解釈しやすいが変更リスクあり。
代理キー (Surrogate Key)
システムが採番する連番や UUID。 変更不要、 結合パフォーマンス良い。
複合キー (Composite Key)
複数列で一意性を満たす。 例:(year, prefecture_code)。

🔬 概念を言葉で読み解く(詳細版)

主キー(Primary Key, PK)は、 リレーショナルデータベースのテーブルの「各行を一意に識別する属性または属性の組」。 SSDSE-B-2026.csv の場合、 (年度, 地域コード) の複合主キーが各レコードを一意に決めます。 PK は単なる「行番号」ではなく、 NULL 不可・一意性保証・自動インデックスといった3 つの絶対要件を満たします。

① 主キーの形式的定義

関係 R の主キー K の条件
$$\forall t_1, t_2 \in R: \quad t_1[K] = t_2[K] \implies t_1 = t_2$$
「K の値が同じ 2 行があれば、 その 2 行は同一」。 つまり K で行を完全に特定できる。 さらに K は最小(部分集合では一意性を満たさない)であること。
$t_1, t_2$
関係 R の任意の 2 行(tuple)。
$t_1[K]$
行 $t_1$ における属性集合 K の値。
NULL 不可
K のどの属性も NULL を許さない。
最小性
K の真部分集合は一意性を満たさない。

② 候補キー・代理キー・自然キー

キーの種類定義SSDSE-B-2026 での例
スーパーキー一意性を持つ属性集合(最小性は不問)(年度, 地域コード, 都道府県)
候補キー最小性を満たすスーパーキー(年度, 地域コード), (年度, 都道府県)
主キー(PK)候補キーから 1 つ選んだもの(年度, 地域コード)
代替キーPK 以外の候補キー(年度, 都道府県)
自然キー業務上の意味を持つキー(年度, 地域コード)、 マイナンバー、 ISBN
代理キー(surrogate key)人工的な ID(AUTO_INCREMENT)SSDSE-B には標準で含まれない、 必要なら追加

③ SSDSE-B-2026 の主キー設計

SSDSE-B-2026 は 47 都道府県 × 12 年(2012-2023) = 564 行。 主キー候補:

地域コード(例:R13000=東京都)は ISO 3166-2:JP 規格に対応し、 国際標準との互換性も高い。 これが「(年度, 地域コード)」を主キーに選ぶ実務的根拠。

④ B-tree インデックスとパフォーマンス

B-tree インデックスの計算量
$$T_{\text{lookup}} = O(\log_b n), \quad T_{\text{insert}} = O(\log_b n)$$
$n$ は行数、 $b$ はブランチング係数(PostgreSQL では約 100〜400)。 SSDSE-B-2026 の 564 行なら $\log_{100} 564 \approx 1.4$、 1〜2 ノード参照で目的の行に到達。 PK には自動で B-tree インデックスが張られる。

SSDSE-B-2026 を SQLite で主キー付きロード

SSDSE-B-2026 を SQLite にロードする際、 (年度, 地域コード) を主キーに指定することで、 「2023 年の東京都を取得」というクエリが O(log n) に高速化。 564 行では体感差は小さいが、 主キー設計の練習として最適。 また主キーがあれば INSERT OR REPLACE で冪等更新も可能。

🏭 産業界での活用事例(6 件)

業界事例主キーの選び方SSDSE-B-2026 との対比
銀行口座管理口座番号(10 桁)口座 = レコード単位、 SSDSE-B では (年度, 地域コード)
EC注文管理注文 ID(UUID または AUTO_INCREMENT)SSDSE-B も人工キー追加可能
医療電子カルテ患者 ID(自治体ベース ID)マイナンバーは外部 PK 候補
政府マイナンバー DBマイナンバー 12 桁個人レベルの一意識別子
SaaSマルチテナント DB(テナント ID, レコード ID) 複合 PKSSDSE-B の (年度, 地域) と類似
分析基盤BigQuery / SnowflakePK 制約なし、 ロジカル PKSSDSE-B BigQuery 取込でも同様

⚖️ 主キー設計の比較表

方式例長所短所
自然キー(複合)(年度, 地域コード)業務意味あり、 外部システムと連携易変更困難、 JOIN コスト
自然キー(単独)マイナンバー、 ISBNシンプル業務変更で揺らぐ
代理キー(連番)AUTO_INCREMENT idシンプル・高速・JOIN 軽量意味なし、 シャーディング困難
代理キー(UUID)'550e8400-e29b-41d4-a716-...'分散システム可、 衝突しない16 byte で重い、 インデックス局所性低
ULID / Snowflake ID時系列順 UUIDUUID 利点 + 局所性改善導入コスト
ハッシュキーSHA-256 of natural key長さ固定業務トレース困難

💥 失敗例から学ぶ

💥 主キーに NULL を許す
「都道府県 単独で PK」だと未登録が発生し NULL 混入、 一意性破綻。 NOT NULL を必ず指定。
💥 PK を後から変更
PK は外部キー(FK)で参照される。 変更すると参照側も書き換え必要。 SSDSE-B では「年度 4 桁 → 6 桁」化など。
💥 PK に意味付け
「東京都の id は必ず 1 番」という決め打ちは将来の拡張で破綻。 代理キーは「意味なし」が原則。
💥 UUID 主キーで INSERT 性能低下
B-tree インデックスはランダム UUID で局所性が悪化、 INSERT が 5〜10 倍遅くなることも。 ULID や Snowflake ID を選ぶ。
💥 重複データの PK 衝突
SSDSE-B-2026 を 2 回ロードすると UNIQUE constraint failed。 INSERT OR REPLACE や ON CONFLICT 句で対応。

📝 演習問題(5 問・解答付き)

  1. 問題:SSDSE-B-2026 を SQLite にロードし、 (年度, 地域コード) を主キーに指定せよ。
    ▼ 解答
    📥 入力例(skiprows=1 で日本語見出しのまま読み、全 564 行=47 都道府県 × 12 年度から 4 列を使う) 年度 地域コード 都道府県 総人口 2023 R01000 北海道 5092000 2022 R01000 北海道 5140000 2023 R13000 東京都 14086000 …(全 564 行)
    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,) が返り、主キー (年度, 地域コード) の組み合わせで 564 行すべてが重複なく入った。地域コードは 47 種類、各県が 12 年度ずつなので 47×12=564 で、どちらか一方だけを主キーにすると 2 行目以降が一意制約違反で挿入に失敗する。複合主キーは「この表の 1 行は何を表すか(県×年度)」をそのまま宣言したものと読める。

  2. 問題:SSDSE-B-2026 で「都道府県」を単独 PK にしようとするとなぜ失敗する?
    ▼ 解答
    47 都道府県 × 12 年 = 564 行。 都道府県名は 12 回ずつ重複するため一意性違反。 (年度, 都道府県) の複合キーが必要。
  3. 問題:(年度, 地域コード) と (年度, 都道府県) のどちらを PK にするか、 理由とともに述べよ。
    ▼ 解答
    (年度, 地域コード) を推奨。 理由:(1) 統計センターが振ったコードで、 表記揺れがなく他の統計表と突き合わせやすい、 (2) 整数/短文字列でインデックスが軽量、 (3) 都道府県名は表記揺れリスク(東京都 vs 東京)。
  4. 問題:SSDSE-B-2026 の主キーに重複値を INSERT したらどうなるか、 また対策は?
    ▼ 解答
    UNIQUE constraint failed エラー。 対策:(1) INSERT OR IGNORE(既存を無視)、 (2) INSERT OR REPLACE(上書き)、 (3) ON CONFLICT (年度, 地域コード) DO UPDATE(PostgreSQL 互換)。
  5. 問題:SSDSE-B-2026 から「都道府県マスタ」と「統計事実テーブル」に正規化する場合、 それぞれの PK は?
    ▼ 解答
    都道府県マスタ:PK = 地域コード(47 行、 例 R13000=東京都)。 統計事実テーブル:PK = (年度, 地域コード)、 FK = 地域コード → 都道府県マスタ。 これで 3NF を満たす。

📖 関連用語辞典(10 語)

主キー(PK)
行を一意に識別する属性集合。 NULL 不可、 一意。
外部キー(FK)
他表の PK を参照する属性。 参照整合性を保証。
候補キー
最小性を満たすスーパーキー。 PK の選択肢。
代理キー(surrogate)
人工的に振った ID(AUTO_INCREMENT)。 業務上意味なし。
自然キー(natural)
業務的意味を持つキー(マイナンバー、 ISBN)。
複合キー
2 つ以上の属性の組による PK。 SSDSE-B の (年度, 地域コード) が典型。
参照整合性
FK が参照する PK が必ず存在する状態。
UNIQUE 制約
一意性のみ要求(NULL 可)。 PK の弱い版。
B-tree インデックス
PK に自動で張られる検索構造。 計算量 O(log n)。
UUID / ULID
分散システム向けユニーク ID。 UUID は 128 ビット。

🔬 SSDSE-B-2026 で主キー設計の総合実習

実習:3NF 正規化 + FK 整合性 + UPSERT パターンの統合

🎯 やること: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,}")

📤 実行結果:

地方 県数 合計人口 ------------------------------ 関東 7 43,527,000 近畿 7 21,990,000 中部 9 20,749,000 九州 8 14,029,000 東北 6 8,318,000 中国 5 7,070,000 北海道 1 5,092,000 四国 4 3,578,000

💬 結果の読み方: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 の衝突確率を比較する。

Step 1: 公式

衝突確率 (n 件, 空間 N) ≈ 1 - exp(-n²/(2N)) 近似 (n²/(2N) が小さい時だけ): n²/(2N)

Step 2: ID 別衝突

ID 種別空間 Nn=1M 件衝突確率
32bit 整数4.3e9n²/(2N) = 116.4 → 1 − e^(−116.4) ≈ 1.0000 (ほぼ確実に衝突)
UUID v42^122≈ 9.4e-26 (極小)

🐍 Python で再現

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: n²/(2N) = 116.4 → 衝突確率 1.0000 UUID : n²/(2N) = 9.40e-26 → 衝突確率 9.40e-26

💬 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,0005,985 / 5,696122
R03000 岩手県1,163,0005,788 / 5,432122
R04000 宮城県2,264,00012,852 / 12,328122

🎯 このコードでやること:手計算の Step 1〜5 を pandas の value_counts と merge で再現し、 予言した行数と実際の行数が一致するか確かめる。

📥 入力例 SSDSE-B-2026.csv(skiprows=[1]、 564 行 × 112 列)から青森県・岩手県・宮城県の 2022・2023 年度 年度 地域コード 都道府県 総人口 出生数 2023 R02000 青森県 1184000 5696 2022 R02000 青森県 1204000 5985 2023 R04000 宮城県 2264000 12328 …
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=['年度', '地域コード'])), '行')
📤 実行例(実測) n_L n_R n_L×n_R 地域コード R02000 1 2 2 R03000 1 2 2 R04000 1 2 2 予言した行数: 6 地域コードだけで結合: 6 両側 2 年度・地域コードだけ: 12 行 両側 2 年度・(年度, 地域コード): 6 行 全体・地域コードだけ: 6768 行 = 47 × 12 × 12 全体・(年度, 地域コード): 564 行

💬 予言した 6 行と実際の結合結果 6 行が一致し、 手計算の Step 4・5 の 12 行・6,768 行・564 行もそのまま出た。 結合の前に「結合に使う列が片側で主キーになっているか」を確かめておけば、 結合後の行数を先に言い当てられる。 地域コードだけで全体どうしを結合した 6,768 行は、 同じ県の 12 年度 × 12 年度の組み合わせがすべて作られた結果で、 どの行も分析には使えない。

🧠 理解度チェック(実測値で解く)

  1. 2023 年度の 47 行だけの表で主キーになる列は何か。 564 行の表ではなぜそれが主キーにならないか。
    解答:地域コード(都道府県名でもよい)。 564 行の表では各県が 12 年度分あり、 地域コードの異なり値は 47 しかないため。
  2. 564 行の表どうしを (年度, 地域コード) ではなく年度だけで結合すると何行になるか。
    解答:各年度に 47 行ずつなので $12\times47\times47=26{,}508$ 行。
  3. 2023 年度の表に東京都の行が 2 回入ると、 総人口の合計はいくつずれるか。
    解答:東京都 14,086,000 人の分だけ多くなり、 124,353,000 人が 138,439,000 人になる(下の ⚠️ の例では 2 行目の値を変えているので 138,453,000 人)。
  4. 主キー (年度, 地域コード) を持つ横長の表を、 指標を縦に並べた縦長の表に変えたとき、 主キーはどう変わるか。
    解答:指標の列が加わり (年度, 地域コード, 指標) になる。 下の 🐍 の例では 61,476 行で重複 0。

🐍 Python での扱い

最小再現コード。 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)
📤 実行例(実測) 拒否: UNIQUE constraint failed: prefecture_stats.code, prefecture_stats.year

💬 同じ ('13', 2023) の組をもう一度入れようとすると IntegrityError になり、エラー文に code と year の 2 列が並んでいるので、複合主キーの組が衝突したことが分かる。2 回目の tfr=9.99 のように値が違っても拒否されるので、主キーは「中身の重複」ではなく「同じ県・同じ年度の行が 2 つある」ことを防いでいる。値を更新したいときは INSERT ではなく UPDATE か INSERT OR REPLACE を使う。

🐍 Python 完全コード(4 要素ナレーション付き)

コード 1:SSDSE-B-2026 を複合主キー付きで SQLite にロード

🎯 このコードでやること:SSDSE-B-2026.csv の 564 行を SQLite に格納、 (年度, 地域コード) を PRIMARY KEY に指定し、 一意性が保たれることを確認する。

📥 入力データ:

年度,地域コード,都道府県,総人口 2023,R13000,東京都,14086000 2023,R14000,神奈川県,9229000 2023,R27000,大阪府,8763000
 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(期待値 47 × 12 = 564) PK 列: ['年度', '地域コード']

💬 結果の読み方:564 行が一切重複なくロードされた=(年度, 地域コード) が確かに一意である証拠。 SQLite は PK 列に自動で B-tree インデックスを構築、 以降の「2023 年東京都」検索は O(log n) で 1〜2 ノード参照のみ。 NOT NULL 制約も同時に課されているため、 欠損値混入も防げる。

コード 2:主キー違反をハンドリング(INSERT OR REPLACE)

🎯 このコードでやること:既存の (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]:,}")

📤 実行結果:

❌ 普通の INSERT: UNIQUE constraint failed: ssdse_b.年度, ssdse_b.地域コード ✅ INSERT OR REPLACE 後の総人口: 14,100,000

💬 結果の読み方:PK 違反は UNIQUE constraint failed として明示される。 INSERT OR REPLACE(PostgreSQL では ON CONFLICT ... DO UPDATE)で冪等な upsertが可能。 これがバッチ更新の鉄板パターン。 既存値(14,086,000)が新値(14,100,000)に置換された。

コード 3:複合 PK での JOIN と FK 整合性確認

🎯 このコードでやること: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)

📤 実行結果:

(2023, '東京都', 14086000) (2023, '神奈川県', 9229000) (2023, '大阪府', 8763000)

💬 結果の読み方:3NF への正規化が成功。 都道府県マスタ(47 行)と統計事実(564 行)に分かれ、 FK 制約で「存在しない地域コードの統計レコード挿入」を DB レベルで拒否。 JOIN で元の形が完全復元され、 2023 年人口 TOP3 は東京・神奈川・大阪。 主キーと外部キーの組合せが「整合性」と「柔軟性」を両立する仕組み。

❓ FAQ 20 問

Q1. 主キーと UNIQUE の違い
PK は (1) NULL 不可、 (2) 1 表に 1 つ、 (3) 自動インデックス。 UNIQUE は NULL 可、 複数指定可。
Q2. SSDSE-B-2026 で PK を変えるべき場面
分析特化なら (都道府県, 年度) も可(文字列重いが可読)。 性能重視なら整数代理キーを追加して (id) を PK に。
Q3. 複合 PK の JOIN コストは?
列数が増えるとインデックス局所性が悪化するが、 (年度 INT, 地域コード TEXT) 程度なら無視できる。
Q4. 代理キー vs 自然キー どちらが良い?
業務的安定性なら自然キー、 性能・JOIN 軽量性なら代理キー。 SSDSE-B-2026 は自然キー(地域コード)で十分。
Q5. UUID 主キーは速い?
局所性が悪く INSERT は遅い(B-tree ページ分割多発)。 ULID/Snowflake ID なら時系列順で局所性改善。
Q6. PK 列に NULL を入れたい
不可。 NULL を許容したいなら UNIQUE 制約に切り替え。
Q7. PK の値を変更したい
技術的には UPDATE 可だが、 FK 参照側も連動更新が必要。 ON UPDATE CASCADE で自動化可。
Q8. 1 つのテーブルに複数 PK を設定できる?
不可。 PK は 1 つだけ(ただし複合 PK で複数列指定は可)。 追加の一意性なら UNIQUE 制約。
Q9. SSDSE-B-2026 を BigQuery に入れる場合の PK は?
BigQuery には PK 制約がない(論理 PK のみ)。 重複防止は ETL 側で実装。 SQL の MERGE 文で upsert。
Q10. AUTO_INCREMENT のリセット
SQLite: UPDATE sqlite_sequence SET seq=0 WHERE name='t'。 MySQL: ALTER TABLE t AUTO_INCREMENT=1。
Q11. PK のサイズはどれくらい?
短く軽量が原則。 INT 4 バイト、 BIGINT 8 バイト、 UUID 16 バイト。 索引サイズに直結。
Q12. SSDSE-B-2026 の PK 設計を MongoDB ライクに
MongoDB は _id 自動付与(ObjectId 12 バイト)。 SSDSE-B では地域コード+年度を _id 文字列に。
Q13. シャーディングと PK
PK の先頭バイトでシャード分散。 順序性のある UUID(ULID)が必須、 ランダム UUID は局所性悪化。
Q14. PK 制約を後から追加できる?
PostgreSQL/MySQL は ALTER TABLE で可。 SQLite は基本不可(テーブル再作成必要)。
Q15. PK と論理削除
deleted_at 列で論理削除する場合も PK は維持。 一意性制約は (PK, deleted_at) など複合化することも。
Q16. 暗号化と PK
PK そのものは暗号化しない(インデックス効率のため)。 機密性が必要なら別途暗号化列を追加。
Q17. PK が変わるリスク
マイナンバーは「変わらない」前提、 ISBN も「絶版なら復活不可」。 「現実世界の ID」を PK に使う際は不変性を確認。
Q18. SSDSE-B-2026 で PK を可視化するには
PRAGMA table_info(ssdse_b)(SQLite)、 \d ssdse_b(psql)、 SHOW INDEX FROM ssdse_b(MySQL)。
Q19. PK がない設計は許される?
理論上は許されない(関係モデルでは必須)。 ただし分析特化テーブルでは「ロジカル PK」だけ宣言して制約は省略するケースも。
Q20. PK 学習の次の一歩
(1) 外部キー、 (2) 正規化(1NF→3NF→BCNF)、 (3) インデックス戦略、 (4) ER 図設計、 (5) MERGE/UPSERT パターン。

📖 主キーの包括ガイド(追補編)

📜 主キー概念の進化年表

年出来事主キーへの影響
1968IBM IMS(階層型 DB)「ポインタ識別」が主流
1970Codd 関係モデル論文「値ベース識別」=主キー概念の創始
1979Oracle V2 リリース商用 RDBMS で PK 制約実装
1986SQL-86 標準化PRIMARY KEY 構文確立
1989SQL-89外部キーと PK の関係制約
1996UUID(RFC 4122)分散システム向け代理キー
2000 年代NoSQL の台頭「PK 不要」議論、 ロジカル PK
2016ULID時系列順 UUID
2010 年代Snowflake ID(Twitter)分散 + 順序性
2020 年代クラウド DWH(BigQuery, Snowflake)PK 制約なしのデファクト化

📐 関係モデルにおける主キーの形式定義

関係 $R$ の主キー $K \subseteq \text{attr}(R)$ は以下を満たす:

  1. 一意性:$\forall t_1, t_2 \in R: t_1[K] = t_2[K] \Rightarrow t_1 = t_2$
  2. 最小性:$\forall K' \subsetneq K: \exists t_1, t_2 \in R: t_1 \ne t_2 \wedge t_1[K'] = t_2[K']$
  3. 非 NULL 性:$\forall t \in R, \forall a \in K: t[a] \ne \text{NULL}$

一意性のみだとスーパーキー、 最小性も満たせば候補キー、 そこから DBA が選んだ 1 つが主キー。

⚙️ 主要 RDBMS の主キー実装

RDBMSPK 実装自動採番注意点
PostgreSQLB-tree インデックス + UNIQUE + NOT NULLSERIAL, BIGSERIAL, IDENTITY (SQL 標準)シーケンスは独立オブジェクト
MySQL (InnoDB)クラスタ化インデックス(PK 順に物理配置)AUTO_INCREMENTPK の選択でストレージ局所性に影響大
SQLiteINTEGER PRIMARY KEY は rowid のエイリアスAUTOINCREMENT(rowid 再利用しない)テキスト PK は別途インデックス必要
OracleB-tree + 制約SEQUENCE + TRIGGER または IDENTITY (12c+)—
SQL Serverクラスタ化インデックス(デフォルト)IDENTITY—
BigQuery制約なし(論理 PK のみ)—重複防止は MERGE 文または ETL 側
MongoDB_id 自動付与(ObjectId 12 バイト)自動カスタム _id 可
Cassandraパーティションキー + クラスタリングキー—分散システム特化設計

🎯 SSDSE-B-2026 の主キー戦略 4 種比較

戦略 1:自然複合 PK (年度, 地域コード)

CREATE TABLE ssdse_b (
    年度 INTEGER NOT NULL,
    地域コード TEXT NOT NULL,
    総人口 INTEGER,
    PRIMARY KEY (年度, 地域コード)
);

長所:業務的意味あり、 重複自然に防止、 e-Stat と整合。 短所:JOIN 時に列が増える、 値変更困難。

戦略 2:代理キー(連番)

CREATE TABLE ssdse_b (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    年度 INTEGER NOT NULL,
    地域コード TEXT NOT NULL,
    総人口 INTEGER,
    UNIQUE (年度, 地域コード)
);

長所:単一列で軽量、 JOIN シンプル、 PK 値が業務に左右されない。 短所:人工的、 業務的意味なし、 BigQuery 等で AUTOINCREMENT 不対応。

戦略 3:UUID

CREATE TABLE ssdse_b (
    id TEXT PRIMARY KEY,  -- UUID
    年度 INTEGER NOT NULL,
    地域コード TEXT NOT NULL,
    総人口 INTEGER,
    UNIQUE (年度, 地域コード)
);

長所:分散システム対応、 衝突しない、 シャーディング容易。 短所:16 バイトで重い、 ランダム UUID では INSERT 性能低下、 人間が読めない。

戦略 4:ハッシュ PK

-- 年度 + 地域コードのハッシュを PK に
CREATE TABLE ssdse_b (
    pk_hash TEXT PRIMARY KEY,  -- SHA-256(年度||地域コード)[:16]
    年度 INTEGER NOT NULL,
    地域コード TEXT NOT NULL,
    総人口 INTEGER
);

用途:データウェアハウス(dbt 等)でステージング層から事実テーブルへの変換に。

🔍 主キーと B-tree インデックスの内部

PK には自動で B-tree インデックスが構築される。 B-tree の特性:

📊 主キーと参照整合性(FK)

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 / ULIDUUID + 局所性改善将来拡張に推奨

🛡️ 主キーとセキュリティ

🎓 主キー学習の段階的目標

  1. 初級:1 表に PK 1 つ、 一意性・NULL 不可を理解、 SQLite で CREATE TABLE ... PRIMARY KEY
  2. 中級:複合 PK、 候補キーから PK を選ぶ判断、 SSDSE-B-2026 で (年度, 地域コード) 設計
  3. 上級:FK と参照整合性、 ON CASCADE / ON RESTRICT、 3NF 正規化
  4. 実務:性能最適化、 シャーディング、 ULID 採用、 分散システムでの PK 戦略
  5. 専門:DDD のエンティティ ID、 イベントソーシング、 CQRS パターン

🐍 表の形を変えると主キーも変わる(横長 → 縦長)

主キーは列の名前ではなく「1 行が何を表すか」で決まる。 SSDSE-B-2026 は 1 行 = 県 × 年度 の横長の表だが、 指標を縦に並べ直すと 1 行 = 県 × 年度 × 指標 になり、 主キーに指標の列が加わる。

🎯 このコードでやること:横長の表を melt で縦長に変え、 (年度, 地域コード) と (年度, 地域コード, 指標) の重複数を比べる。 pivot で元の形に戻せることも確かめる。

📥 入力例 SSDSE-B-2026.csv 全 564 行 × 112 列(年度・地域コード・都道府県 + 数値 109 列) 年度 地域コード 都道府県 A1101 A110101 … 2023 R01000 北海道 5092000 2405000 … …
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, 112) / (年度, 地域コード) の重複 0 縦長の表: (61476, 5) (年度, 地域コード) の重複: 60912 (年度, 地域コード, 指標) の重複: 0 年度 地域コード 都道府県 指標 値 2023 R13000 東京都 A1101 14086000.0 2023 R13000 東京都 A110101 6914000.0 2023 R13000 東京都 A110102 7172000.0 pivot で戻した形: (564, 109)

💬 縦長にすると 564 × 109 = 61,476 行になり、 (年度, 地域コード) の重複は 60,912 件(= 61,476 − 564)に跳ね上がる。 指標を加えた 3 列の組では重複が 0 なので、 これが縦長の表の主キー。 pivot で (564, 109) に戻せたのも、 3 列の組が一意だからで、 重複があると pivot は ValueError で止まる。

⚠️ よくある落とし穴

主キー を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。

❌ 自然キーを安易に選ぶ
メールアドレスや電話番号を主キーにすると、 ユーザーが変更したときに全関連テーブルを更新する大工事に。
❌ 複合キーの罠
便利だが、 外部キーから参照する側も複合になり JOIN が複雑化。 代理キーのほうが楽な場合多し。
❌ UUID の性能
UUID は分散システムに便利だが、 B-Tree インデックスでランダム挿入 → ページ分裂で遅くなる。 ULID 等を検討。
❌ NULL 許容に変更
後から主キー列を NULL 許容にすることは原理的に不可能。 設計時に確定を。
❌ ビジネスロジック混入
「ID の最初の桁が地域コード」のように主キーに意味を持たせると、 再編時に詰む。 主キーは単純に。

※ 上記は文献調査・現場経験で報告される頻度の高い注意点。 ドメインや手法のバージョンによって追加の落とし穴がある場合があります。

⚠️ 実データで確かめる: 二重取り込みは drop_duplicates() では消えない

主キー制約の無い pandas の DataFrame では、 同じ県・同じ年度の行が 2 回入っても何も起きない。 値が訂正されて少し違っていると、 全列一致を見る drop_duplicates() ではすり抜ける。

🎯 このコードでやること:2023 年度の 47 行に、 東京都の行を「訂正版」として値を変えてもう 1 行足し、 全列での重複削除と主キーでの重複検出を比べる。

📥 入力例 SSDSE-B-2026.csv の 2023 年度 47 行(年度・地域コード・都道府県・総人口) 年度 地域コード 都道府県 総人口 2023 R13000 東京都 14086000 + 東京都 2023 年度をもう 1 行(総人口 14100000。 説明用の仮の値)
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():,}")
📤 実行例(実測) 正しい全国合計: 124,353,000 行数: 48 drop_duplicates() 後の行数: 48 二重のまま合計: 138,453,000 キーの重複: 1 件 verify_integrity: Index has duplicate keys: MultiIndex([(2023, 'R13000')], keep=last 後: 47 行, 合計 124,367,000

💬 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 が消えたときに何行が結合から漏れるかを確かめる。

📥 入力例 SSDSE-B-2026.csv の 2023 年度 47 行 総人口の表: 年度 (int64), 地域コード, A1101 出生数の表: 年度 (文字列), 地域コード, A4101 県番号マスタ(説明用に 3 行): 01 北海道 / 02 青森県 / 13 東京都
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 ValueError: You are trying to merge on int64 and object columns for key '年度'. If y 型をそろえた結合: 47 行 地域コード 県番号_int 県番号_str R01000 1 01 R02000 2 02 R03000 3 03 文字列どうし: 3 行 整数を文字列にしただけ: 1 行 zfill(2) で 0 を戻す: 3 行

💬 年度が 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 外部キー (Foreign 候補キー (Candidat スーパーキー UUID / ULID / クラスタリングインデックス 落とし穴

主キー (primary key) はリレーショナルデータベースで「行を一意に特定する 1 列または列の組合せ」で、 NOT NULL かつ UNIQUE 制約が強制される。 SSDSE-B-2026 では「年度(CSV 1 行目の列名は SSDSE-B-2026)+ 地域コード」が複合主キーに相当し、 これによって「同じ都道府県の同じ年のレコードは 1 つだけ」が保証される。 自然キー (意味のある値)・代理キー (UUID/ULID/連番) のどちらを使うか、 クラスタリングインデックスとして物理配置を決めるかなどの設計判断が、 後の JOIN 性能・データ統合・落とし穴の発生確率に直結する。

🔗 隣接手法への橋渡し

主キーは「テーブル設計 → データ整合性 → 分析」の連鎖で中心的な役割を果たし、 隣接する DB 設計概念と密接に絡む。

主キーが設計されていないテーブルは「重複行が混入する」「JOIN 結果が膨らむ」「集計値が二重計上される」という致命的な分析バグを生み、 SSDSE-B-2026 のような分析用データセットでも (都道府県, 年) の組が暗黙の主キーとして扱われている。

🌳 手法選択フロー

「主キー」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。

  1. 1 列で行を一意に決められるか
    決められるなら単一キー。 SSDSE-B-2026 では地域コードの一意数が 47 しかなく、 564 行を識別できない。 決められないなら複合キーへ。
  2. 複数列の組で一意になるか
    地域コード + 年度の 2 列なら、 組の一意数が 564 となり全行を識別できる(実測)。 組にしても重複が残るなら、 データの粒度の理解が間違っている。
  3. 自然キーか、 代理キーか
    地域コードのように意味を持つ列は、 市町村合併などで値が変わりうる。 変更に弱いなら意味のない連番(代理キー)を立て、 自然キーには一意制約を付ける。 外部キーから参照する側も単純になる。
  4. NULL を許すか
    主キーは NULL を取れない。 未確定の値がある列は主キーにできないので、 候補から外すか、 確定するまで代理キーで運用する。

SSDSE-B-2026 は 47 県 × 12 年度 = 564 行。 「地域コードが主キー」と決めつけると 1 県あたり 12 行が重複扱いになり、 結合で行数が 12 倍に膨らむ。

🖼️ 主キーの図解 — 3 枚で理解する

主キーの役割を 3 つの図で視覚化する。 SVG を base64 化したインライン画像で、 外部依存なしに表示される。

主キーと外部キーの関係図 — pref_code を主キーとする地域マスターと、 これを外部キーで参照する人口サマリ

主キーの 3 制約 — 一意性・非NULL・不変性

自然キーと代理キーの比較

図 1 は地域マスターと人口サマリの主キー−外部キー関係、 図 2 は主キーが満たすべき 3 つの制約 (一意性・非NULL・不変性)、 図 3 は自然キー (pref_code, isbn 等) と代理キー (UUID, SEQ 等) のトレードオフを示す。

🎮 触って理解する — 主キー診断ラボ

下の架空のサンプル名簿(氏名・メール等はすべて作例)で、列見出しをタップ/クリックして主キー候補に指定してみてください。複数の列を選ぶと複合キーになります。選んだ瞬間に「一意性チェック(重複=赤)」と「NULL チェック(オレンジ)」が走り、主キーとして妥当かが即座に判定されます。

クイック選択:
ID 氏名 メール 生年月日
判定待ち…

一意性の判定式:選択キーの「異なり値の数」=「行数」かつ NULL 数 = 0 のとき合格。下の図は操作に合わせてリアルタイム更新されます。

行数 0 異なり値の数 - NULL 行数 - キー未選択 列見出しをタップしてください

🧭 遊び方のヒント(この順に試すと全パターン体験できます)

  1. 氏名だけを選ぶ → 「佐藤 陽菜」が 2 人いて赤くハイライト。同姓同名で失格。
  2. メール → 値自体は重複しないが NULL(未登録)が 1 件。NULL があれば主キー失格。
  3. 生年月日 → 2005-11-03 生まれが 2 人。日付は簡単に衝突する。
  4. 氏名+生年月日の 2 列 → 単独では失格だった列も、組合せなら全行一意になり合格(複合キーの体験)。
  5. ID → 単独で合格。これが代理キー(連番)。「行を追加」を何回押しても自動採番で衝突しない。
  6. ID を主キーにした状態で「重複 ID=1 で追加」→ UNIQUE 制約エラーで弾かれ、衝突行が点滅。「ID なしで追加」→ NOT NULL 制約エラー。
  7. 全解除してから「重複 ID=1 で追加」→ 誰も止めずに入ってしまう。主キーが無いテーブルの危うさを体感。

💡 直感 — 主キーは「背番号」

主キーは行を一意に特定する背番号です。名前で選手を呼ぶと同姓同名で混乱しますが、背番号で呼べば必ず 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(地域コード)4747断面ではキー、 パネルでは 12 重に重複
A1101 総人口47520偶然の一意性。 キー不適
B4101 年平均気温31101断面ですら 16 組が衝突
A4103 合計特殊出生率3182桁が粗く衝突多発
E6101 短期大学数1636「5 校」が 8 県で同値
(年度, Code) 複合47564設計上のキー。 全行一意

つまり主キーとは「手元のデータで重複が無かった」という観測報告ではなく、 「将来追加される行も含めて、 DB が衝突を拒否し続ける」という約束(制約)です。 観測はいくらでも裏切られますが、 約束はスキーマが守ります。

⚠️ 落とし穴(重要)

🚀 発展 — キー発見(key discovery)という研究分野

「ドキュメントの無い未知のテーブルから候補キーを自動で見つける」問題は、 データプロファイリング研究でキー発見・関数従属発見と呼ばれる一分野です(代表的アルゴリズムに 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)として体系化されています。 「主キーの不変性」は無料で手に入る性質ではなく、 コード表の版管理という運用努力の産物です。