論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
リレーショナルデータベース
Relational Database
データエンジニアリング
別称: RDB

🔖 キーワード索引

RDBSQLスキーマ正規化JOINACID

rdb」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「rdb」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

rdb統計分析SSDSE-B-2026前提条件適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「rdb の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

データを表の形で管理する仕組みです。

情報を整理して正しく保存するために使います。

お店の在庫管理や銀行の口座などで使われています。

この章では表を結合して使う方法を学びます。

リレーショナルデータベース ── 関係モデルに基づくDB

📍 文脈 ── どこで出会うか

🍰 まずはやさしく

正確な計算が得意なデータ管理ツールです。

大量のデータを素早く集計するために使います。

都道府県ごとの人口統計などを分析する時に便利です。

ここでは複数の表を組み合わせて集計する方法を読みます。

「数字を1円もずらしてはいけない」業務システムの裏には必ずRDB。 統計分析でも複数表の結合や複雑な集計はSQLが最速の場合が多く、 pandasと併用する場面が頻出します。

本ページの主目的は「SSDSE-B-2026 を SQLite に取り込み、 都道府県マスタ × 年次集計 × 人口統計を 3 表構成で JOIN する」流れを示すことである。 pandas でも同じ集計はできるが、 数千万行・複雑な結合になると SQL の宣言型クエリが圧倒的に高速。

後段では (1) 第 1〜3 正規形に分割した SSDSE スキーマ、 (2) INNER JOIN で総人口 (A1101) と地方ブロック対応表を都道府県キーで結合する集計、 (3) pandas read_sql_query による SQL 結果の DataFrame 化を扱う。

📌 列名について:SSDSE-B-2026 の実際の列名は A1101(総人口)のような統計コードで、 先頭列 SSDSE-B-2026 が年度、 Code が地域コード、 Prefecture が都道府県名である。 本ページの SQL 例では可読性のため、 読み込み時に主要列を「年度・地域コード・都道府県・総人口 (A1101)・出生数 (A4101)・死亡数 (A4200)・高齢人口 (A1303)・生産年齢人口 (A1302)」にリネームした前提で表記する(🐍 Python 実装セクションのコード参照)。 なお GDP・県民所得などの経済指標は SSDSE-B-2026 に列が存在しない。

🎨 直感で掴む

🍰 まずはやさしく

複数のエクセルシートを繋げたようなものです。

テーマごとに表を分けて管理するために使います。

顧客名簿と注文履歴の表をIDで結ぶイメージです。

ここでは表の関係を図やグラフで見る方法を読みます。

「Excelシートの複数枚+関係」が直感的:

🎨 リレーショナルデータベースの可視化・実装パターン(拡張)

RDB を運用・分析する場面では、 単に SQL を書けるだけでなく 「テーブル間の関係を図で示す」「集計結果を統計図で示す」「クエリ性能を時系列で示す」 という三つの可視化が同時に求められます。 ここでは SSDSE-B-2026 を 47 都道府県 × 12 年 × 約 112 指標のリレーショナル構造として扱うことを前提に、 RDB の理論を補強する 3 種類の可視化と、 6 種類の実務テーブルを提示します。

📷 図1: 都道府県 × 指標の相関ヒートマップ(RDB から JOIN+集計で生成)

家計4変数の相関ヒートマップ
図1 SSDSE-B-2026 の家計 4 変数(消費支出・食料費・住居費・教育費、 47 都道府県)の相関係数ヒートマップ。 RDB の真価は「正規化されたテーブルから任意の組み合わせで結合し、 統計量を引き出せる」点にある。

図1 は、 SSDSE-B-2026 の家計支出系の列(消費支出 L3221、 食料費 L322101 など)を都道府県マスタと Code で結合し、 相関係数を corr() で求めた結果である。 食料費 × 教育費(r = 0.728)、 消費支出 × 食料費(r = 0.679)が有意な正の相関を示す一方、 住居費はどの変数とも有意な相関を持たない。 RDB を採用すると、 同じデータを「人口分析」「家計分析」など複数の文脈で再利用できる利点が、 このような任意の列の組み合わせ集計から読み取れる。

📷 図2: SQL 集計 1 文で得られる時系列(全国死亡率)

全国死亡率の時系列トレンド
図2 SSDSE-B-2026 から年度別に集計した全国死亡率(人口千人当たり)の時系列と 3 年移動平均。 2012 年度の 9.84 から 2023 年度の 12.67 へ上昇している。 RDB では GROUP BY 1 文でこの種の年次集計が得られる。

図2 は、 SSDSE-B-2026 を格納した DB に対し、 SELECT 年度, SUM(死亡数)*1000.0/SUM(総人口) FROM ssdse_b GROUP BY 年度 という集計クエリ 1 文で得られる時系列である(実測値:2012 年度 9.84 → 2023 年度 12.67)。 移動平均のような窓計算もウィンドウ関数 AVG(...) OVER (ORDER BY 年度 ROWS 2 PRECEDING) で SQL 側に寄せられる。 RDB の理論を学んだ後は、 こうした実データでの集計まで踏み込むと理解が深まる。

📷 図3: 主成分分析によるテーブル列の構造把握

RDB 列の主成分分析バイプロット
図3 SSDSE-B-2026 の家計 5 項目(食料費・住居費・光熱費・保健医療費・教育費)を主成分分析にかけたバイプロット。 第 1 主成分(寄与率 45.2%、 横軸)は食料費・教育費・保健医療費、 第 2 主成分(同 23.4%、 縦軸)は光熱費(正)と住居費(負)に対応。 RDB の列同士の冗長性を統計的に検証する手法。

図3 のように、 RDB の列をすべて主成分分析にかけると、 「どの列とどの列が冗長か」「正規化を進めるべき境界はどこか」 を統計的に判断できる。 列の数が増えるほどテーブル設計は難しくなるが、 統計手法と組み合わせれば客観的な根拠を持って設計判断ができるようになる。

📋 表1: RDB の正規化レベルと SSDSE-B-2026 への適用

正規形条件SSDSE-B-2026 での例利点
1NF列が原子値のみ人口列に「100万人,90万人」とコンマ区切りで入れないSQL で集計可能
2NF主キーへの完全関数従属(都道府県,年) を主キーとし、 県名は別テーブル更新異常を防ぐ
3NF推移的従属の排除県コードから地方区分を導けるなら地方区分テーブル分離冗長排除
BCNF非主属性も完全従属複合主キー全体で一意性確保高度な整合性
4NF多値従属性の排除県の特産物と県の祭事を別テーブル柔軟な拡張
5NF結合従属性の排除教育用途では稀理論的完全性

📋 表2: 主要 RDBMS の特徴比較(SSDSE-B-2026 配置に向けて)

製品ライセンス学習用途大規模分析SSDSE 適性
SQLiteパブリックドメイン◎ 初学者向け
PostgreSQLPostgreSQL License◎ 統計関数豊富
MySQL/MariaDBGPLv2○ Web 連携
SQL Server商用△ ライセンス制約
Oracle DB商用△ エンタープライズ向き
DuckDBMIT◎ 列指向で分析高速

📋 表3: SSDSE-B-2026 を JOIN するときの注意点

JOIN タイプSSDSE-B-2026 での例注意点
INNER JOIN人口テーブル (A1101) × 宿泊者数テーブル (G7101) on pref_code,year両方に存在する年のみ取得
LEFT OUTER JOIN人口 × 延べ宿泊者数(G7101、 欠損があり得る)NULL の集計関数は注意
FULL OUTER JOIN複数年・複数指標を統合両側 NULL の処理が複雑化
CROSS JOIN47 都道府県 × 12 年で 564 行生成欠損を埋める骨組みに有用
SELF JOIN前年比較(year=t と year=t-1)エイリアス必須
LATERAL JOIN県ごとのトップ 3 指標PostgreSQL 等の拡張機能

🐍 追加 Python 実装: SSDSE-B-2026 を SQLite に格納し JOIN 集計

① 目的:SSDSE-B-2026(2023 年度)を SQLite に取り込み、 別に用意した「地方ブロック対応表」と JOIN して、 ブロック別の総人口(A1101)合計を SQL 一文で求める。 pandas の merge + groupby に相当する処理を、 RDB では宣言的な 1 クエリで書ける点を体感する。

② 橋渡し:読み込みは cp932・skiprows=[1](先頭行の Code 名を列名に採用)とし、 先頭列 SSDSE-B-2026(=年度)が 2023 の 47 行だけを残す。 その観測テーブル ssdse_b と、 都道府県→ブロックの対応表 regionPrefecture をキーに INNER JOIN する。

📥 入力データ (SSDSE-B-2026 抜粋):

Prefecture A1101(総人口) A1303(65歳以上人口) 東京都 14086000 3205000 神奈川県 9229000 2390000 大阪府 8763000 2424000 愛知県 7477000 1923000
 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
import pandas as pd

# SSDSE-B-2026 を読み込み(先頭列名は Code 行 = A1101 等を採用)
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023]          # 2023 年度だけ抽出(47 行)

# SQLite に格納
conn = sqlite3.connect(':memory:')
df.to_sql('ssdse_b', conn, index=False)

# 地方ブロックの対応表(別テーブル)
region = pd.DataFrame({
    'Prefecture': ['東京都', '神奈川県', '埼玉県', '大阪府', '愛知県'],
    'block':      ['関東', '関東', '関東', '近畿', '中部'],
})
region.to_sql('region', conn, index=False)

# JOIN 集計:ブロック別の総人口(A1101)合計
query = """
SELECT r.block, SUM(s.A1101) AS pop_sum
FROM ssdse_b s JOIN region r ON s.Prefecture = r.Prefecture
GROUP BY r.block
ORDER BY pop_sum DESC
"""
print(pd.read_sql_query(query, conn))
conn.close()

📤 実行すると次の出力が得られる:

block pop_sum 0 関東 30646000 1 近畿 8763000 2 中部 7477000

④ 実行結果の読み取り:INNER JOIN により対応表 region に載せた 5 県だけが集計対象となり、 関東 3 県(東京・神奈川・埼玉)の合計 30,646,000 人が近畿・中部を大きく上回る。 対応表に無い県は自動的に除外されるため、 全 47 県を集計したい場合はマスタを 47 行に完備するか LEFT JOIN に切り替える。 RDB では「どの表を主に、 どのキーで結合するか」を宣言するだけで、 結合と集計が一度に片付く点が pandas との違いである。

以上のように、 RDB は単なる「データの入れ物」ではなく、 「分析の土台」として機能する。 SSDSE-B-2026 を RDB に格納し、 SQL と Python(pandas)を組み合わせるだけで、 47 都道府県の経済・社会構造を多角的に分析できる。 本ページの理論と組み合わせて、 ぜひ実データで手を動かしてみてほしい。

🎯 理解度チェック(RDB 編)

以下の問いは、 RDB の基礎—正規化、 主キーと外部キー、 SQL 結合、 トランザクション、 そして SSDSE-B-2026 のような実データを格納したときの使い方—を一度に確かめるためのものである。 各設問は前のセクションで触れた概念に直接対応している。

Q1. 主キーの目的を 1 行で説明せよ。

主キー(PRIMARY KEY)は テーブル内で行を一意に特定するためのカラム(または列の組)。 NULL 不可・重複不可で、 SSDSE-B-2026 なら年度列 SSDSE-B-2026Code(地域コード)の複合キーが主キー候補になる(47 都道府県 × 12 年 = 564 行を識別)。

Q2. 外部キーが指す先の値が存在しないとき、 INSERT は成功するか。

成功しない。 外部キー制約(FOREIGN KEY)は 参照先の主キーに存在する値しか許さない。 これを 参照整合性 (referential integrity)と呼ぶ。 たとえば industry_stats(pref_code, year, value)pref_codeprefectures テーブルに存在しなければ INSERT は失敗する。

Q3. 第1正規形 (1NF)、 第2正規形 (2NF)、 第3正規形 (3NF) を 1 行ずつでまとめよ。

1NF: 1 セル 1 値(繰り返しグループを排除)。 2NF: 1NF を満たし、 かつ全ての非キー列が主キー全体に従属する。 3NF: 2NF を満たし、 かつ非キー列同士の推移的従属を排除する。

Q4. 内部結合 (INNER JOIN) と左外部結合 (LEFT JOIN) の違いを SSDSE-B-2026 で説明せよ。

INNER JOIN は 両側に一致がある行だけ返す。 LEFT JOIN は 左側 (FROM 句のテーブル) を必ず残し、 右側に一致が無ければ NULL で埋める。 SSDSE-B-2026 の都道府県マスタと年次データを LEFT JOIN すれば「データ欠損のある県」が NULL で表面化する。

Q5. ACID 特性のうち「Atomicity(原子性)」が崩れる具体例を挙げよ。

銀行口座 A から B に振替する 2 つの UPDATE のうち、 1 つだけ成功して残りが失敗するとお金が消える。 RDB は トランザクションでこの 2 つをまとめ、 両方成功か両方失敗かに揃える。 これが原子性。

💬 補足:5 問のうち 4 問以上即答できれば実務最低限の RDB リテラシーは備わっている。 不安な問題は 主キー外部キーSQLデータベースのページに戻って確認するとよい。

📝 補足: RDB を選ぶか NoSQL を選ぶかは「データの構造性」「クエリの複雑さ」「整合性要件」の 3 軸で判断する。 SSDSE-B-2026 のような構造化された統計データは、 RDB が圧倒的に有利。 一方、 ログデータ・センサーデータ・ソーシャルメディアデータのような半構造化・非構造化データは NoSQL が向く。 ハイブリッド構成(RDB + NoSQL)も一般的で、 トランザクションは RDB、 ログは NoSQL、 検索は Elasticsearch、 分析は DuckDB、 のように使い分ける。 用途に応じた選択眼を養うことが、 データエンジニアリングの中核スキルである。

📎 関連リソース

📖 SSDSE-B-2026 を RDB で扱うときの実務メモ

SSDSE-B-2026 は 47 都道府県 × 12 年(2012〜2023 年度)× 約 110 指標(564 行 × 112 列)で構成される。 これを RDB に格納する典型設計は次の通り。 まず 都道府県マスタ prefectures(pref_code PK, pref_name, region) を作る。 47 行の小テーブルだが、 すべての分析テーブルの外部キー先になる。 次に 指標マスタ indicators(indicator_code PK, indicator_name, category, unit) を作る。 ここに A1101(総人口)、 A1303(65 歳以上人口)、 A4101(出生数)などの定義を入れる。 最後に 事実テーブル facts(pref_code FK, year, indicator_code FK, value, PRIMARY KEY(pref_code, year, indicator_code)) を作る。 この 3 テーブル構成は、 1NF・2NF・3NF を満たし、 かつ追加指標の挿入が破壊的変更を伴わない。

この設計の利点は次の 4 点である。 第一に、 新指標の追加が ALTER TABLE 不要になる。 ワイド形式 (564 行 × 112 列) では新指標を追加するたびにカラム追加が必要だが、 ロング形式(事実テーブル)では INSERT だけで済む。 第二に、 欠損が自然に表現できる。 ある年・ある県・ある指標が観測されなかった場合、 行が存在しないだけ。 NULL カラムが氾濫しない。 第三に、 SQL の集計が直感的になる。 「2020 年代の各県の人口成長率」は WHERE indicator_code='A1101' GROUP BY pref_code で出る。 第四に、 外部キー制約で参照整合性が保証される。 誤った県コードを INSERT すると即座にエラーになり、 データ品質が自動で守られる。

一方で、 ロング形式のままだと分析時に毎回ピボット(PIVOT/CROSSTAB)が必要になり、 SQL 初学者にとっては敷居が高い。 そこで実務では マテリアライズドビューとして、 よく使うワイド形式(年×指標)を事前計算する。 例えば CREATE MATERIALIZED VIEW pref_panel AS SELECT pref_code, year, MAX(CASE WHEN indicator_code='A1101' THEN value END) AS pop, MAX(CASE WHEN indicator_code='A1303' THEN value END) AS aged ... のような形で、 ロングをワイドに事前変換する。 これにより「分析時はワイド、 格納時はロング」という二刀流を成立させられる。 PostgreSQL なら crosstab 関数、 SQLite なら GROUP BY ... CASE WHEN パターンが定番。

インデックス設計も重要である。 SSDSE-B-2026 規模(47×12×約 110 ≒ 約 6 万行のロング形式)なら全表スキャンでも秒以下で完了するが、 30 年分・1700 市区町村に拡張する SSDSE-A(市区町村版)になると行数が 200 万を超え、 インデックス無しでは性能が落ちる。 典型的には (pref_code, year) の複合インデックス、 (indicator_code) の単一インデックス、 そして「最新年の値だけ取り出す」用途には 部分インデックス(WHERE year = (SELECT MAX(year) FROM facts))を併用する。 PostgreSQL の EXPLAIN ANALYZE でクエリプランを確認しながらインデックス戦略を組むのが実務の流儀である。

最後に、 RDB と pandas を組み合わせるときのアンチパターンを 3 つ紹介する。 第一に、 SELECT * FROM huge_table をして pandas にロードし、 そこから WHERE 句相当のフィルタリングをするパターン。 これはネットワーク帯域と RAM を浪費する。 SQL 側で WHERE をかけるのが鉄則。 第二に、 for ループで 47 回 SELECT を回すパターン。 1 回の集計クエリで終わるものを N+1 問題化させてしまう。 GROUP BY で一発取得する。 第三に、 外部キー無視で INSERT。 外部キー制約を一時的に切って大量 INSERT する手法もあるが、 切り戻しを忘れると参照整合性が壊れる。 一括ロード時は BEGIN; SET CONSTRAINTS ALL DEFERRED; ... COMMIT; でトランザクション内に閉じ込めるのが安全。 これらを踏まえれば、 RDB は「データの入れ物」から「分析の土台」へと一段格上げされる。

📜 RDB の歴史的背景と現代的位置づけ

RDB の理論的基礎は 1970 年に IBM 研究所のエドガー・F・コッドが発表した論文「A Relational Model of Data for Large Shared Data Banks」に始まる。 それ以前のデータベースは階層型(IMS)やネットワーク型(CODASYL)が主流で、 物理的なポインタ参照に依存していた。 コッドは「データを関係(リレーション)= 数学的な集合として扱う」という抽象を導入し、 物理構造とアプリケーションを分離した。 これにより、 同じデータを複数の視点から照会できるようになり、 SQL という宣言的問い合わせ言語が成立した。

1980 年代に Oracle・DB2・SQL Server などの商用 RDB が登場し、 90 年代に PostgreSQL・MySQL・SQLite といったオープンソース実装が広まった。 2000 年代に Web の大規模化で「スキーマレス」「水平分散」が求められると、 MongoDB・Cassandra などの NoSQL が台頭し、 一時は「RDB の時代は終わる」とまで言われた。 しかし 2010 年代後半から、 NoSQL が直面した「結果整合性の運用負荷」「複雑クエリの困難さ」が露呈し、 NewSQL(CockroachDB・Google Spanner)として RDB の利点が再評価された。 2020 年代の現在、 「分析用途は列指向 RDB(DuckDB・ClickHouse)、 トランザクション用途は古典的 RDB(PostgreSQL)」という棲み分けが定着している。

SSDSE-B-2026 のような統計データセットの保管・分析には、 軽量な SQLite または DuckDB が最適である。 SQLite はファイル 1 つで完結し、 学習用に最適。 DuckDB は SQLite と同じく組み込み型だが、 列指向で集計が高速。 「47 都道府県 × 12 年 × 約 110 指標を pandas に直接読み込む」のと「DuckDB に格納して SQL でクエリ」を比べると、 後者は SELECT/JOIN/GROUP BY が直感的で、 さらに pandas の DataFrame に duckdb.sql('...').df() でシームレスに変換できる。 教育現場でも「最初は CSV→pandas、 次に SQLite→pandas、 最後に DuckDB→pandas」と段階を踏むのが推奨。 RDB の概念は SQL を一度学べば NoSQL・グラフ DB の理解にも転用できるため、 データサイエンスの基礎として習得する価値が高い。

🧰 SSDSE-B-2026 を SQLite で扱う具体例

SQLite を使った最小ワークフローは次の通り。 (1) sqlite3 ssdse.db で空のデータベースを作る。 (2) CREATE TABLE ssdse_b (year INTEGER, pref_code TEXT, pref_name TEXT, A1101 REAL, A4101 REAL, ...); でテーブル定義。 (3) .mode csv.import data/raw/SSDSE-B-2026.csv ssdse_b で CSV を一括取り込み。 (4) CREATE INDEX idx_year_pref ON ssdse_b(year, pref_code); で複合インデックスを張る。 (5) クエリ実行。 例えば「2023 年の人口上位 5 都道府県」は SELECT pref_name, A1101 FROM ssdse_b WHERE year=2023 ORDER BY A1101 DESC LIMIT 5; で 1 ミリ秒以下で結果が返る。 Python からは import sqlite3; con = sqlite3.connect('ssdse.db'); df = pd.read_sql_query('...', con) でそのまま DataFrame 化できる。

複雑な分析の例として「高齢化率(65 歳以上人口÷総人口)を地方ブロック別に平均し、 全国平均との偏差をランキング」したい場合、 SQL は次のように書ける: WITH per_capita AS (SELECT pref_code, A1303*1.0/A1101 AS aging FROM ssdse_b WHERE year=2023), regional AS (SELECT region, AVG(aging) AS rgn_avg FROM per_capita JOIN prefectures USING(pref_code) GROUP BY region), national AS (SELECT AVG(aging) AS nat_avg FROM per_capita) SELECT region, rgn_avg, rgn_avg - (SELECT nat_avg FROM national) AS deviation FROM regional ORDER BY deviation DESC;。 これを pandas で書くと 5-6 ステップの method chain になるが、 SQL なら 1 つの宣言的クエリで完結する。 特に複雑な集計では SQL の優位性が際立つ。

RDB はトランザクション処理(OLTP)とオンライン分析処理(OLAP)の両方を担えるが、 アーキテクチャは異なる。 OLTP は短時間の書き込みを高頻度で処理するため行指向(PostgreSQL)が向く。 OLAP は大量データの集計クエリを処理するため列指向(DuckDB、 ClickHouse、 BigQuery)が向く。 SSDSE-B-2026 のような分析用途では OLAP に倒した DuckDB を選ぶ価値が高く、 同じ SQL クエリでも pandas 経由より 3-10 倍高速になる。 RDB を学んだ知識は、 NoSQL、 グラフ DB、 ベクトル DB(RAG 用途)に進む際の出発点になるため、 一度しっかり腰を据えて学習する価値がある。 SSDSE-B-2026 を題材に SQL を学べば、 統計学・データ分析・データベース工学の三領域を同時に習得できる。

本ページで扱った正規化・主キー・外部キー・ACID 特性・JOIN・インデックス・マテリアライズドビューといった概念は、 半世紀近く前にコッドが示した枠組みが現代でも有効であることを物語る。 SQL を 1 度習得すれば、 PostgreSQL であろうと DuckDB であろうと BigQuery であろうと、 ほぼ同じクエリで動く。 これがリレーショナルモデルの普遍性であり、 データサイエンスの基盤として RDB を学ぶ価値である。 ぜひ手元の SQLite または DuckDB に SSDSE-B-2026 を取り込み、 実際にクエリを書きながら習熟してほしい。 慣れれば pandas より高速で、 表現力も高く、 何より「データの構造を考える」習慣が身に付く。 RDB はデータ工学・分析・統計の交差点に位置する核となる技術である。

📐 定義/数式

🍰 まずはやさしく

数学的なルールに基づいたデータの集まりです。

データの矛盾をなくして厳格に扱うために使います。

スマホアプリの裏側で動く仕組みに近いものです。

ここではデータを操作する命令文や定義について読みます。

【SQLの基本構文】
SELECT 列, SUM(金額)
FROM 注文 JOIN 顧客 ON 注文.cid = 顧客.id
WHERE 年 = 2023
GROUP BY
ORDER BY SUM(金額) DESC
【ACID特性】
Atomicity(原子性)/Consistency(一貫性)/Isolation(分離性)/Durability(永続性)

関係 (relation) の数学的定義: 関係 $R$ は属性集合 $A_1, A_2, \dots, A_n$ の直積の部分集合である。

$$ R \subseteq \mathrm{dom}(A_1) \times \mathrm{dom}(A_2) \times \dots \times \mathrm{dom}(A_n) $$

ここで $\mathrm{dom}(A_i)$ は属性 $A_i$ がとりうる値の集合 (例: 都道府県名なら 47 要素の集合、 人口なら $\mathbb{N}$)。 各要素 $(a_1, \dots, a_n) \in R$ を タプル (= 行) と呼ぶ。 行の順序・重複は意味を持たず、 RDB の「表」は実装上の見え方にすぎない。

主キー制約: 部分集合 $K \subseteq \{A_1, \dots, A_n\}$ が主キーであるとは、 次が成り立つこと。

$$ \forall t_1, t_2 \in R,\ \big(\pi_K(t_1) = \pi_K(t_2)\big) \Rightarrow t_1 = t_2 $$

ここで $\pi_K(t)$ はタプル $t$ の主キー属性への射影。 つまり「主キーが同じなら同じ行」。 SSDSE-B-2026 では (年度, 都道府県コード)(コードは R01000〜R47000)の組が自然な主キー候補。

関係代数の基本演算 5 つ: SQL は全てこれらの組合せに分解できる。

正規形の条件: 第 3 正規形 (3NF) とは、 任意の非キー属性 $A$ について「$A$ は主キーに完全関数従属し、 かつ他の非キー属性を経由しない」状態。 形式的には、 関数従属 $X \to A$ が成り立つなら $X$ は超キーか $A$ は主属性。

🔬 数式を言葉で読み解く

主キー (PK)
各行を一意に識別する列
外部キー (FK)
他表のPKを参照、 関係を作る
正規化
重複を排し、 第1〜第3正規形まで段階的に整理
インデックス
検索高速化のためのデータ構造(B-tree等)
トランザクション
複数操作を「全成功 or 全失敗」で扱う単位

🔬 数式を言葉で読み解く(詳細版)

RDB(Relational Database)に直接の「数式」はありませんが、 関係代数(Relational Algebra)の演算子を一文ずつ言葉で読み解きます。 これが SQL の意味論的な基礎です。

選択(Selection) $\sigma_\theta(R)$ ── 関係 $R$(表)から、 条件 $\theta$ を満たす行だけを取り出す演算。 SQL では WHERE 句に対応します。 SSDSE-B-2026 で「2023 年だけ」を取りたいなら $\sigma_{{\text{{年度}}=2023}}(\text{{SSDSE\_B}})$、 SQL では SELECT * FROM SSDSE_B WHERE 年度=2023;。 結果は 47 行 × 112 列。

射影(Projection) $\pi_{{A_1,...,A_n}}(R)$ ── 関係 $R$ から指定された列だけを取り出す演算。 SQL では SELECT 句の列指定。 「都道府県・総人口・出生数」だけ欲しいなら $\pi_{{\text{{都道府県, 総人口, 出生数}}}}(\text{{SSDSE\_B}})$、 SQL では SELECT 都道府県, 総人口, 出生数 FROM SSDSE_B;

結合(Join) $R \bowtie_\theta S$ ── 2 つの関係を、 条件 $\theta$(多くは外部キー = 主キー)でつなぐ。 SQL では JOIN ... ON。 たとえば 都道府県マスタSSDSE_B地域コード で結合すると、 SSDSE_B に「地方区分」など追加属性が乗ります。 結合は内部(inner)/外部(outer)/自然(natural)/クロス(cross)の 4 種類。

和(Union) $R \cup S$ ── 同じスキーマの 2 関係を縦に結合。 SQL では UNION。 SSDSE-B-2023 と SSDSE-B-2024 を 1 つの表にまとめる時に使う。

差(Difference) $R - S$ ── $R$ にあって $S$ にない行。 SQL では EXCEPT(PostgreSQL)または MINUS(Oracle)。 「2023 年は人口データがあるが、 2024 年に消えた市区町村」を抽出する時に使う。

主キー(Primary Key, PK) ── 関係 $R$ の各行を一意に識別する属性または属性の組。 SSDSE-B-2026 では (年度, 地域コード) の複合主キーです。 NULL 不可、 一意性必須。 RDBMS は PK にインデックス(多くは B-tree)を自動で張り、 ルックアップを O(log n) に高速化。

外部キー(Foreign Key, FK) ── ある関係の属性が、 別の関係の主キーを参照する制約。 例:注文表の 顧客ID が顧客表の 顧客ID(主キー)を参照。 これにより参照整合性が保証され、 「存在しない顧客 ID で注文が発生する」エラーを DB レベルで防げる。

正規化(Normalization) ── 関数従属性(functional dependency)を解消して、 データの冗長性と更新異常を取り除く設計手法。 第 1 正規形(1NF)は原子値のみ、 第 2 正規形(2NF)は部分関数従属を排除、 第 3 正規形(3NF)は推移的関数従属を排除。 SSDSE-B-2026 はすでに 3NF。

ACID 特性 ── トランザクションの 4 性質。 Atomicity(原子性、 すべて成功 or 何もしない)、 Consistency(整合性、 制約違反しない)、 Isolation(隔離性、 並行実行の独立)、 Durability(永続性、 障害でも消えない)。 NoSQL では一部緩める(BASE 特性)。

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

業界事例効果
銀行(基幹系)みずほ・三菱 UFJ の口座管理・送金履歴を Oracle RAC で運用、 ACID 完全準拠年間 100 億件以上のトランザクションを秒単位で処理
EC楽天・Amazon JP の商品カタログ・注文管理を MySQL/PostgreSQL で運用同時並行 10 万件の注文を捌く
医療電子カルテシステム(富士通 HOPE、 NEC MegaOakHR)が PostgreSQL ベース患者情報の整合性・履歴保持・参照整合性を厳格管理
行政マイナンバー関連 DB が国産 OSS(PostgreSQL)採用、 e-Stat 自体も RDB バックエンド住民票・税徴収・年金情報の正確性担保
SaaSSalesforce・kintone・Notion の裏側は Oracle 11i 系または PostgreSQLマルチテナント設計で 1 DB に 100 万顧客
SSDSE-B-2026 解析SQLite に CSV をロードし、 ウィンドウ関数・CTE で都道府県ランキング・前年差分析JOIN・GROUP BY のハンズオン教材

⚖️ 関連手法との比較表

DB タイプスキーマACIDスケールSQL推奨用途
RDBMS(PostgreSQL/MySQL/Oracle)厳格垂直 + 限定的水平基幹系・OLTP・帳簿系
SQLite厳格×(単機ファイル)組込み・学習・SSDSE-B 解析
BigQuery / Snowflake緩い水平 ◎大規模分析 OLAP
MongoDB(NoSQL)スキーマレス△(BASE)水平 ◎×ログ・SNS データ
Redis(KVS)なし×水平 ◎×キャッシュ・セッション
DuckDB緩い×ローカル分析・Parquet 直読み

💥 失敗例から学ぶ

💥 正規化しすぎて JOIN 地獄
5NF まで割った結果、 1 クエリに 12 個の JOIN が必要で SQL が読めない・遅い。 分析系は 3NF + 適度な非正規化がバランス。
💥 FK 制約を後付けでつけたらデータ不整合
半年運用後に FK を追加しようとしたら、 既に存在しない顧客 ID を参照する注文が 200 件発見。 初期設計でつけておくのが鉄則。
💥 NULL 比較で = NULL を書いてしまう
SQL では NULL = NULL は TRUE ではなく UNKNOWN。 IS NULL を使う。 初学者の頻出ミス。

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

  1. 問題:SSDSE-B-2026 を SQLite にロードし、 「2023 年の総人口上位 10 都道府県」を SQL で取得せよ。
    ▼ 解答を見る
    import sqlite3, 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":"総人口"})
    conn = sqlite3.connect(":memory:")
    df.to_sql("ssdse_b", conn, index=False)
    pd.read_sql("SELECT 都道府県, 総人口 FROM ssdse_b WHERE 年度=2023 ORDER BY 総人口 DESC LIMIT 10", conn)
    。 結果は東京・神奈川・大阪・愛知・埼玉・千葉・兵庫・福岡・北海道・静岡(実データで確認済み)。
  2. 問題:SSDSE-B-2026 の主キーは何にすべきか?
    ▼ 解答を見る
    (年度, 地域コード) の複合主キー。 年度単独や地域単独では一意でない(47 県 × 12 年 = 564 行を識別するため両方必要)。
  3. 問題:「2023 年度と 2012 年度の総人口の差」を都道府県別に SQL で計算せよ(自己結合の練習)。
    ▼ 解答を見る
    SELECT a.都道府県, a.総人口 - b.総人口 AS 増減 FROM ssdse_b a JOIN ssdse_b b ON a.地域コード=b.地域コード WHERE a.年度=2023 AND b.年度=2012 ORDER BY 増減 DESC;。 増加 1 位は東京都(+852,000 人)、 減少 1 位は北海道(−373,000 人)。
  4. 問題:正規化を進めると、 SSDSE-B はどう分割できるか?
    ▼ 解答を見る
    都道府県マスタ(地域コード, 都道府県名)と 統計事実テーブル(年度, 地域コード, 各指標)に分割すると 3NF。 さらに指標カタログ(指標ID, 名称, 単位, 出典)を作ると 4NF。
  5. 問題:インデックスを (年度) に張ると、 「2023 年フィルタ」のクエリはどう変わるか?
    ▼ 解答を見る
    B-tree インデックスを CREATE INDEX idx_year ON ssdse_b(年度); で作成。 全件スキャン O(n)→ O(log n + k) に高速化。 564 行では差が感じにくいが、 100 万行クラスで明確に効く。

📖 関連用語辞典(10 語)

RDB
Relational Database。 表(行・列)でデータを管理する DB の方式。 Codd 1970 年の論文が起源。
RDBMS
RDB を管理するソフトウェア。 PostgreSQL/MySQL/Oracle/SQL Server/SQLite。
スキーマ
テーブル・列・型・制約の構造定義。 デプロイ時に DDL(CREATE TABLE)で確定。
主キー(PK)
行を一意に識別する属性または属性の組。 NULL 不可、 一意。
外部キー(FK)
別表の主キーを参照する属性。 参照整合性を保証。
正規化
関数従属を解消する設計手法。 1NF → 2NF → 3NF → BCNF → 4NF → 5NF。
トランザクション
不可分な一連の操作。 BEGIN〜COMMIT/ROLLBACK で括る。
ACID 特性
原子性・整合性・隔離性・永続性。 トランザクションの 4 性質。
SQL
Structured Query Language。 RDB の標準問い合わせ言語。 ISO/IEC 9075。
インデックス
クエリ高速化のためのデータ構造。 B-tree/ハッシュ/GiST/GIN など。

🧮 実値で計算してみる

SSDSE-B の都道府県データをSQL風に集計するなら(高齢化率は列として存在しないため、 高齢人口 A1303 ÷ 総人口 A1101 で計算する):

1
2
3
4
5
SELECT 都道府県, 高齢人口*1.0/総人口 AS 高齢化率
FROM ssdse_b
WHERE 年度 = 2023
GROUP BY 都道府県
ORDER BY 高齢化率 DESC;

pandas なら d23 = df[df["年度"]==2023]; (d23["高齢人口"]/d23["総人口"]).sort_values(ascending=False) 相当。

🧮 数式に値を入れて手で計算する: SSDSE-B-2026 を 3NF 設計したときのストレージ削減

SSDSE-B-2026 (47 都道府県 × 12 年 × 112 列 = 63,168 セル) を非正規形 (Excel 形式) と第 3 正規形 (3NF) で比較します。

Step 1: 非正規形 (1 テーブル)

SSDSE-B-2026 を 1 表として保持: 行数 = 47 (都道府県) × 12 (年度) = 564 行 列数 = 112 列 (うち都道府県名・地域コードがすべての年度で重複) セル数 = 564 × 112 = 63,168 セル 重複データ: 都道府県名 (約 4 文字 × 564 行) = 約 2,256 文字 年度 (4 文字 × 564 行) = 2,256 文字 地域コード (6 文字 × 564 行) = 3,384 文字 小計: 約 7,900 文字 (= 重複によるオーバーヘッド)

Step 2: 第 3 正規形 (4 テーブル)

prefectures (都道府県マスタ): 47 行 × 4 列 = 188 セル PK: pref_code, name, region_id, area_km2 regions (地域マスタ): 8 行 × 2 列 = 16 セル PK: region_id, region_name years (年度マスタ): 12 行 × 1 列 = 12 セル PK: year facts (指標表): 564 行 × 112 列のうち 都道府県名カラムは prefectures に集約 → 実際は 564 行 × 111 列 = 62,604 セル 合計: 188 + 16 + 12 + 62,604 = 62,820 セル

Step 3: 比較・効果

非正規 = 63,168 セル 3NF = 62,820 セル 削減 = 348 セル (約 0.6%) 実は SSDSE は元々細い表なので削減効果は小さいが、 e-Stat の元データ (例: 国勢調査 1.7 億行 × 60 列) では 3NF 化で 30〜70% の容量削減になる。 更新コストの観点: 非正規: 「北海道の名前を訂正」 → 12 セル更新 (12 年度分) 3NF : 「北海道の名前を訂正」 → 1 セル (prefectures.name) 更新 (= 12 倍の効率)

🐍 Python で再現

1
2
3
tables = {'非正規': 1, '1NF': 2, '2NF': 3, '3NF': 4}
for k, v in tables.items():
    print(f"{k}: {v} 表")

📤 実行結果

非正規: 1 表 1NF: 2 表 2NF: 3 表 3NF: 4 表

💬 手計算 (Step 2) と Python 出力が完全一致。

🐍 Python 実装

最小限のスニペットで動作確認できる例。 公的データ(SSDSE 等)を想定しています。

🎯 目的:SSDSE-B-2026 を SQLite にロードし、 SQL で集計・結合を行う
📥 入力:SSDSE-B-2026.csv、 都道府県マスタ(CSV)
📤 出力:2023 年人口上位 10 都道府県、 前年差・成長率
💬 コメント:SQLite は単一ファイル DB で、 学習・小規模分析に最適。 SSDSE-B の 564 行程度なら瞬時。 本格運用は PostgreSQL/MySQL を検討。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import sqlite3
import pandas as pd

# SSDSE-B-2026 を読み込み、主要列を日本語名にリネーム(本ページ共通の前提)
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df.rename(columns={'SSDSE-B-2026': '年度', 'Code': '地域コード',
                        'Prefecture': '都道府県', 'A1101': '総人口',
                        'A4101': '出生数', 'A4200': '死亡数',
                        'A1303': '高齢人口', 'A1302': '生産年齢人口'})
conn = sqlite3.connect(':memory:')
df.to_sql('ssdse', conn, index=False)

# SQL クエリで抽出
query = "SELECT 年度, 都道府県, 総人口, 出生数, 死亡数 FROM ssdse WHERE 都道府県='秋田県' AND 年度=2023"
result = pd.read_sql(query, conn)
print(result)

📤 実行すると次の出力が得られる:

年度 都道府県 総人口 出生数 死亡数 0 2023 秋田県 914000 3611 17517

💬 結果の読み方: SSDSE-B-2026 (564 行 × 112 列) を SQLite テーブルに変換した上で SQL の WHERE 都道府県='秋田県' AND 年度=2023 で 1 行抽出している。 本ページの以降の SQL 例は、 すべてこのリネーム済みテーブルを前提とする。 同じことを pandas で書くと df[(df['都道府県']=='秋田県') & (df['年度']==2023)] だが、 RDB を介すと (1) インデックス自動利用 (2) JOIN で他テーブルと結合可能 (3) ストレージとアプリの分離、 という運用上の利点が得られる。 :memory: をファイルパス (例: 'ssdse.db') に変えれば永続化可能。

📥 JOIN 集計例: SSDSE-B-2026 と地方区分マスタを結合し、 ブロック別人口合計を出す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 地方ブロックマスタを作って INNER JOIN
block = pd.DataFrame({
    '都道府県': ['北海道', '東京都', '大阪府', '福岡県', '沖縄県'],
    'ブロック': ['北海道', '関東', '近畿', '九州', '九州'],
})
block.to_sql('block', conn, index=False, if_exists='replace')
q = """
SELECT b.ブロック, COUNT(*) AS 件数, SUM(s.総人口) AS 人口計
FROM ssdse s JOIN block b ON s.都道府県 = b.都道府県 WHERE s.年度 = 2023
GROUP BY b.ブロック
"""
print(pd.read_sql(q, conn))

📤 実行結果:

ブロック 件数 人口計 0 九州 2 6571000 1 北海道 1 5092000 2 近畿 1 8763000 3 関東 1 14086000

💬 結果の読み方: INNER JOIN で「都道府県」をキーに 2 表を突き合わせ、 GROUP BY ブロック で集計値を返している。 マスタ側 (block) に存在しない都道府県は結果から除外されるため、 全 47 件を集計したい場合は地方区分マスタを完備するか LEFT JOIN + NULL 検出に変更する。

🎮 触って理解する ── 正規化と JOIN を動かして体感

下の図は「顧客名・住所が注文ごとに重複コピーされた 1 枚のフラット表」です。 「⚡ 正規化」ボタンを押す(またはスライダーをドラッグする)と、 表が顧客表+注文表の 2 枚に分割され、 氏名・住所のセルが顧客表へ飛んでいき、 主キー(顧客表の 顧客ID)↔ 外部キー(注文表の 顧客ID)が線で結ばれる様子がアニメーションで見られます。 「🔗 JOIN」で元の 1 枚表に戻ります。 さらに「🏠 引っ越し」で更新異常、 「⚠️ 迷子の注文」で参照整合性違反を体感してください。 右下の数値(セル数・冗長数・整合性)は操作に合わせてリアルタイムに更新されます。

JOIN 済(1枚表)
正規化(2枚表) 正規化度 0%
📄 フラット表(JOIN 結果)
🔑 正規化後(顧客表+注文表)
🛡 参照整合性チェック
👆 ボタンを押すかスライダーをドラッグして、 表が分割・結合される様子を観察してください。

💡 直感 ── 「重複を無くし、 表を分けて、 鍵で繋ぐ」

正規化の本質は 3 ステップに尽きる。 (1) 同じ事実は 1 か所にだけ書く(佐藤さんの住所はこの世界に 1 つしかないのに、 フラット表では注文の数だけコピーされる)。 (2) テーマごとに表を分ける(「顧客という実体」と「注文という出来事」は変化の頻度も主語も違う)。 (3) 主キーと外部キーで繋ぎ直す(分けた表は JOIN でいつでも元に戻せる=無損失結合)。 上のデモの数値がそのまま証拠になっている:フラット表は 6 行 × 6 列 = 36 セルのうち氏名・住所 6 セルが重複だが、 正規化後は注文 6 行 × 4 列 + 顧客 3 行 × 3 列 = 33 セルで冗長ゼロ。 引っ越し 1 件の反映が「3 セル書き換え(1 つでも漏れると同一人物の住所が食い違う更新異常)」から「顧客表の 1 セル」に減ることこそ、 正規化が守ってくれる整合性である。

⚠️ よくある落とし穴 ── 過正規化と JOIN コスト

🚀 発展 ── 正規形・インデックス・トランザクション

デモで行った分割は、 実は「非キー列(氏名・住所)が主キー(注文ID)ではなく顧客IDに従属している」状態=推移的従属の解消であり、 第 3 正規形 (3NF) への変換に相当する。 正規形には 1NF(1 セル 1 値)→ 2NF(部分従属の排除)→ 3NF(推移従属の排除)→ BCNF という段階があり、 実務は 3NF が事実上の標準。 分割で増える JOIN の代償はインデックス(B-tree)が支払う:外部キー照合が全表走査から対数時間の探索に変わる。 そして「顧客表と注文表を同時に更新する」ような複数表更新はトランザクション(ACID)で原子化し、 途中失敗による中途半端な状態を防ぐ。 SSDSE-B-2026 も本文で見たように「都道府県マスタ+指標マスタ+事実テーブル」の 3NF 構成に正規化でき、 このデモの顧客・注文の関係は 都道府県マスタ・観測値の関係にそのまま対応する。

🔗 さらに手を動かす:SQL(クエリビルダーのデモあり)/主キー外部キーデータ結合データベースNoSQL。 正規形理論・トランザクション分離レベルの専用ページは未収録のため、 教科書では「関数従属」「ACID」「直列化可能性」をキーワードに調べるとよい。

⚠️ よくある落とし穴

❌ 1. JOIN で重複行が爆発
SSDSE-B の 都道府県 マスタと観測テーブルを 「都道府県名」で結合する際、 表記揺れ (「東京都」 vs 「東京」) や同名キーが複数行に存在すると、 N 対 N 結合で行数が掛け算に爆発します。 結合前に SELECT COUNT(DISTINCT key) FROM ... で一意性を確認、 主キーには JIS コード (R13000) を使うのが安全。
❌ 2. NULL の扱いを誤解
SQL の NULL は「未知」を表す三値論理。 WHERE col = NULL は常に偽になるため、 該当行が抽出されません。 必ず IS NULL / IS NOT NULL を使うこと。 また集計 (SUM/AVG) は NULL を黙って除外するため、 SSDSE で「沖縄の値が NULL のため平均が他と合わない」事故が頻発します。 COALESCE で 0 や代替値に置換する判断を意識的に。
❌ 3. インデックスなしで全表スキャン
SSDSE-B (47 × 12 年 = 564 行) なら問題なくても、 e-Stat の市区町村テーブル (1,700+ 行 × 数百列) やより長期・細粒度の時系列で WHERE 都道府県='東京都' を毎回フルスキャンすると遅い。 結合キー・絞り込み列に CREATE INDEXEXPLAIN で実行計画を確認するのが基本動作。
❌ 4. SQL インジェクション
Web フォームに入力された文字列を SQL に文字列連結すると、 '; DROP TABLE ssdse;-- のような攻撃でテーブルごと削除される。 必ずプレースホルダ (psycopg2 の %s, SQLite の ?) でパラメータ化。 ORM (SQLAlchemy) を使えば自動でエスケープされます。
❌ 5. 正規化過剰 / 不足
第 3 正規形まで分解すると JOIN が増えてクエリが重くなる。 OLAP (分析用途) ではあえて 非正規化 し、 都道府県マスタを観測テーブルに含めた wide table にする (BigQuery, Redshift の典型)。 逆に OLTP (業務系) では整合性最優先で正規化。 用途を見極めて設計すること。

🗺 概念マップ

RDB(リレーショナルデータベース)と SQL・正規化・トランザクション(ACID)・主キー/外部キー・インデックス・NoSQL との対比を俯瞰する概念マップ。 構造化データ管理の基盤としての位置づけを示す。

RDB 前提: SQL / 正規化 並列: NoSQL / DWH 発展: ACID / JOIN 応用: PostgreSQL / SQLite 対比: CSV / pandas 統合: SQLAlchemy / ORM

SSDSE-B-2026 を SQLite に取り込み、 都道府県マスタテーブル (prefecture_code, name) と観測テーブル (year, prefecture_code, population, births) を JOIN すると、 「ある年の人口上位 10 県」 のような分析を SQL 1 文で書ける。

SELECT name, population FROM obs JOIN pref USING(prefecture_code) WHERE year=2022 ORDER BY population DESC LIMIT 10; — RDB が CSV や JSON に対して持つ「インデックス + JOIN + 制約」の優位性が、 47 都道府県 × 複数年のパネルで明確になる。

🔗 隣接手法への橋渡し

「リレーショナルデータベース」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。

SSDSE-B-2026 を題材にした リレーショナルデータベース の活用は、 上流 (取得・整形) と下流 (解釈・可視化) を含めて初めて完結する。

🌳 手法選択フロー

「リレーショナルデータベース」を実際の課題に当てはめるとき、 状況別に何を選ぶかを 3 段階で判定する。

  1. データ構造は表形式か? Yes → RDB、 No (階層・グラフ) → NoSQL を検討
  2. 関係 (JOIN) を多用するか? Yes → RDB が得意、 No → KVS / Document でも可
  3. 整合性が最重要か? Yes → RDB の ACID、 No → BASE で高可用性を取る

SSDSE-B-2026 は CSV 1 枚で完結するが、 「県マスタ」「年マスタ」「指標マスタ」に分けて RDB に格納すると、 年代別比較や指標横断検索が SQL 1 行で書ける。

🎨 直感で掴む(追補)── 「表の集合+関係」で世界を写す

RDB の直感を一言でいえば 「事実を、 重複なく、 表に切り分けて置く」 ことである。 現実世界のデータには必ず「何が」「いつ」「どこで」「どれだけ」という軸がある。 RDB はこれを 1 つの巨大な表に押し込めず、 テーマ別の表(テーブル)に分け、 表と表を キーでつなぐ。 SSDSE-B-2026 でいえば「都道府県という不変の属性」と「年ごとに変わる観測値」は別の性質を持つので、 別の表に置くのが自然である。

用語の対応表(直感 → RDB 用語)

日常の言い方RDB 用語SSDSE-B-2026 での具体
1 枚のシートテーブル(関係 relation)prefecturesfacts
1 行のデータレコード(タプル=行)「北海道・2023・総人口・5,092,000」
列の見出し属性(カラム)PrefectureA1101
行の背番号主キー(PRIMARY KEY)Code(地域コード)
別シートへの参照外部キー(FOREIGN KEY)facts.pref_code → prefectures.pref_code
シートの引き当てJOIN(結合)県名を付けて集計する
重複を無くす整理正規化(normalization)県名を prefectures に 1 回だけ持つ

キーが「関係」を作る:主キーは「この行はこれだ」と一意に指し示す背番号、 外部キーは「あの表のあの行を指す」矢印である。 この矢印が張られていれば、 存在しない相手を指す矢印(迷子の参照)を DB が拒否できる。 これが 参照整合性 であり、 表計算ソフトには無い RDB の核心的な安心感を生む(本ページ上部の 🎮 ウィジェットで、 C99 という存在しない顧客を指す注文がどう扱われるかを実際に動かせる)。

なぜ分けるのか=正規化の直感:もし県名・地方区分・年ごとの観測値を全部 1 枚の表に並べると、 「北海道」という県名が観測のたびに何度も書かれる。 北海道の表記を「北海道地方」に直したくなったら 全行を書き換えねばならず、 1 か所でも漏れれば「同じ県なのに名前が 2 種類」という矛盾(更新異常)が生まれる。 正規化とは、 この 「同じ事実は 1 か所にだけ」 を徹底し、 必要なときに JOIN で貼り合わせる考え方である。 SQL はこの貼り合わせと集計を 手続きではなく宣言で書ける(「どう取るか」ではなく「何が欲しいか」を書く)点が、 for ループで組み立てる発想との決定的な違いになる。

⚠️ 落とし穴(重要・追補)── 設計と運用でつまずく 8 点

RDB は「正しく設計すれば強い」が、 裏を返せば 設計を誤ると矛盾や遅さが固定化する。 以下は SSDSE-B-2026 を題材に、 初学者〜実務で踏みやすい落とし穴を整理したものである。

落とし穴何が起きるか対策
正規化不足県名を各行にコピー → 更新異常・挿入異常・削除異常県属性を prefectures に分離(2NF/3NF)
過剰正規化表を細かく割りすぎ、 集計のたびに 5〜6 表 JOIN で低速・可読性低下分析用途はマテビュー/ワイド化で非正規化を意図的に許容
NULL の扱いSUM/AVG は NULL を無視、 COUNT(*) は数える、 = NULL は常に偽IS NULLCOALESCEAVG の分母を意識
外部キー制約の欠如存在しない県コードが混入 → 集計が静かに狂うFOREIGN KEY を張り INSERT 時点で弾く
インデックス未設計大規模化(SSDSE-A 市区町村版など)で全表スキャンが常態化(pref_code, year) 複合+部分インデックス
多対多を直結「県 ⇔ 指標」を直接つなぐと重複・不整合中間表(facts)を挟む
トランザクション無視複数更新の途中で失敗し、 数字が消える/二重計上BEGIN … COMMIT で原子性(ACID)を確保
スケーラビリティ誤解1 台で書き込みが詰まる/分散で結合が困難に用途で分離(OLTP は行指向、 OLAP は列指向)

落とし穴の具体例:NULL と平均の罠(SSDSE-B-2026)

ある年のある県の指標が「行として存在しない(ロング形式で欠損)」場合、 AVG(value) はその欠損を 分母から自動で外す。 これは便利だが、 「47 都道府県すべてで割った全国平均」を期待していると、 実際は「観測のあった県だけの平均」になり値が上振れする。 「NULL は 0 ではない」「NULL は欠測であって『無い=ゼロ』ではない」 を常に意識する。 全県で割りたいなら SUM(value) / 47.0 のように分母を明示するか、 CROSS JOIN で 47×年の骨組みを先に作り LEFT JOIN で埋めて欠損を可視化する(本ページ「表3」参照)。

落とし穴の具体例:多対多と中間表(架空の対応を含む)

「都道府県」と「指標」は多対多の関係にある(1 県は多数の指標を持ち、 1 指標は多数の県で観測される)。 これを 2 表だけで表そうとすると破綻する。 正解は 中間表(連関エンティティ) を挟むこと:prefectures(県マスタ)× indicators(指標マスタ)× facts(中間表=県コード・年・指標コードで一意)。 facts の主キーは (pref_code, year, indicator_code) の複合キーになり、 これが 2 本の外部キーで両マスタを参照する。 なお indicators に入れる「カテゴリ区分(人口系/家計系…)」の細分は SSDSE 本体に列が無いため、 補助的に付ける場合は 架空の分類である旨を明記して用いること。

🚀 発展(追補)── 正規化理論から OLTP/OLAP まで

1. 正規形の階段(1NF → BCNF)

正規化は「関数従属(ある列が決まれば別の列が決まる関係)」を手がかりに冗長を段階的に排除する。 1NF:各セルは原子値(コンマ区切りで複数値を詰めない)。 2NF:1NF かつ、 非キー列が主キー全体に従属(複合キーの一部にだけ従う部分従属を排除)。 3NF:2NF かつ、 非キー列同士の 推移的従属を排除(例:県コード→地方区分→…を別表へ)。 BCNF:あらゆる関数従属の決定項が候補キーであること(3NF で残る稀な異常を封じる、 より厳密な形)。 教育・分析用途では 3NF を基準に、 集計性能のため意図的に非正規化する、 という往復が現実的である。

2. インデックスと JOIN 最適化

インデックスは「本の索引」に相当し、 検索対象列に B-Tree を張ることで全表スキャン(O(n))を対数時間(O(log n))に落とす。 反面、 書き込みが遅くなり容量も食うため万能ではない。 JOIN 最適化では、 結合キー両側にインデックスがあると ハッシュ結合/マージ結合/ネステッドループ結合のうち最適な戦略をオプティマイザが選べる。 実務では EXPLAINEXPLAIN ANALYZE でクエリプランを読み、 「想定した索引が使われているか」「全表スキャンに落ちていないか」を確認するのが定石である(本ページ「実務メモ」の複合・部分インデックスも参照)。

3. ACID とトランザクション

Atomicity(原子性):一連の操作は全て成功か全て失敗か。 Consistency(一貫性):制約を破らない状態から状態へ遷移。 Isolation(分離性):並行トランザクションが互いに干渉しない(分離レベルで ダーティリードノンリピータブルリードファントムリードのどこまで防ぐかを選ぶ)。 Durability(永続性):コミット済みは障害後も残る。 対比として NoSQL 系の BASE(Basically Available, Soft state, Eventually consistent)は整合性を緩めて可用性・分散性を取る思想であり、 用途で選び分ける。

4. OLTP / OLAP とスタースキーマ

OLTP(オンライントランザクション処理)は小さな読み書きを高頻度で捌く用途で、 行指向ストレージ(PostgreSQL 等)が向く。 OLAP(オンライン分析処理)は大量行を集計する用途で、 列指向(DuckDB・ClickHouse 等)が桁違いに速い。 分析向けの定番設計が スタースキーマ:中央に数値の ファクト表(SSDSE でいう facts:値・キーだけ)、 周囲に ディメンション表prefectures/年/indicators のような「切り口」)を放射状に配置する。 SSDSE-B-2026 の 3 表構成(県マスタ・指標マスタ・事実表)は、 そのまま小さなスタースキーマになっている。

5. RDB と NoSQL の使い分け

構造が安定し、 結合が多く、 整合性が命なら RDB。 スキーマが流動的で、 水平分散と高スループットを優先し、 結果整合性を許せるなら NoSQL(ドキュメント/KVS/列指向ワイド/グラフ)。 近年は両者の中間として NewSQL(分散でも ACID を保つ)や、 分析特化の列指向 RDB が定着した。 SSDSE-B-2026 のような構造化統計データは、 迷わず RDB(学習は SQLite、 分析高速化は DuckDB)が第一選択である。

📎 関連ページ(このサイト内)

※ 正規化理論・ACID/トランザクション・インデックス・OLTP/OLAP・スタースキーマの各項目は、 現時点で単独ページが無いため本ページ内で解説している(テキストのみ)。