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

🔖 キーワード索引

結論 基本概念 正規化 ACID SQL例 Python 落とし穴 主要製品 関連用語 FAQ

リレーショナルDB (RDB)」は構造化データを表 (テーブル) と関係 (リレーション) で管理する仕組みで、 統計・業務データ基盤の中核を担う。 本ページでは RDB を理解するうえで要となるキーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

リレーショナルDBテーブル主キー・外部キー正規化SQLトランザクション (ACID)JOINインデックスPython 実装

これらのキーワードは「リレーショナルDB の理解 → 設計 (正規化) → 操作 (SQL) → 運用」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

表を使ってデータをまとめる仕組みです。

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

スマホの連絡先リストのようなものです。

この章ではリレーショナルDBの結論を読みます。

テーブル形式の構造化データ管理システム

💡 結論(深掘り)

RDB 詳細 ── 表と表の関係でデータを管理し、 SQL でクエリする。 ACID 特性と高度な整合性管理が強み。 OLTP(オンライントランザクション処理)の中核。

📍 文脈

🍰 まずはやさしく

データの処理方法を決める仕組みです。

大量のデータを効率よく扱うために使います。

部活の出席簿を管理するイメージです。

この章では他のシステムとの違いを読みます。

RDB はトランザクション処理(OLTP)の中核。 大規模分析や履歴蓄積は データレイク や DWH と組み合わせます。

対比:NoSQL(MongoDB, Cassandra)は柔軟性・水平拡張で優位だが整合性は弱い。 NewSQL(CockroachDB, TiDB)は両方を狙う新流。

🎨 直感で掴む

🍰 まずはやさしく

複数の表をIDでつなぐ仕組みです。

必要な情報をすぐに取り出すために使います。

買い物リストと商品表を分けるイメージです。

この章では使い方の感覚について読みます。

リレーショナル DB は 「表 (テーブル) と表の関係 (リレーション) でデータを管理する」仕組みです。 例えば SSDSE-B-2026 を RDB に投入すると、 「都道府県マスタ (47 行) 」と「年度別指標 (47×12=564 行) 」「項目マスタ (109 行) 」の 3 つの表に分解し、 都道府県コード (R01000 等) で JOIN して必要な切り口で再構成できます。

直感的にはExcel ファイル群を「ID で繋ぐルール」付きで束ねたもの。 ただし Excel と違って (1) 同じ ID で 2 行を作れない (主キー制約) 、 (2) 存在しない都道府県を参照できない (外部キー制約) 、 (3) 数百万行を WHERE + インデックスで瞬時に絞れる、 という点が決定的に違います。

🎨 基本概念

概念意味
テーブル行と列の表customers, orders
主キー (PK)行を一意に識別order_id
外部キー (FK)他テーブルへの参照orders.customer_id
インデックス高速検索用B-tree, hash
ビュー仮想テーブル月次集計
トランザクション複数操作を一括振込

🎨 補講:ER 図と論理設計 — SSDSE のスキーマを描いてみる

RDB の設計は「エンティティ(実体)」「リレーション(関係)」「属性」で表現します。 ER 図(Entity-Relationship Diagram)はその設計図です。 SSDSE-B-2026 を ER で表現してみましょう。

📊 SSDSE の ER 図(テキスト版)

[pref_master] [population_fact] +------------+ +-------------------+ | code PK |---<外部キー>---<| code FK | | pref_name | | year PK | +------------+ | metric FK | | value | +-------------------+ [metric_master] ^ +-------------+ | | metric_code PK |---<外部キー>----------+ | metric_jp | +-------------+ (関係) pref_master (1) ──── (N) population_fact metric_master (1) ──── (N) population_fact

📐 カーディナリティ(多重度)

🐍 Python 実装 17:N:N の中間テーブル例

🎯 このコードでやること:SSDSE には N:N がないため、 仮想的に「都道府県 × タグ(地方区分)」の N:N を作って体感する。

📥 入力データ:pref_master + 地方区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import sqlite3, pandas as pd

conn = sqlite3.connect('rdb_demo.db')
# tag マスタ
conn.execute("CREATE TABLE IF NOT EXISTS region_tag (tag_code TEXT PRIMARY KEY, tag_name TEXT)")
conn.executemany("INSERT OR REPLACE INTO region_tag VALUES (?,?)",
    [('HK','北海道'),('TH','東北'),('KT','関東'),('CB','中部'),('KS','近畿'),('CG','中国'),('SK','四国'),('KY','九州')])
# pref × tag の中間表(N:N)
conn.execute("""CREATE TABLE IF NOT EXISTS pref_region (
    code TEXT, tag_code TEXT, PRIMARY KEY (code, tag_code),
    FOREIGN KEY (code) REFERENCES pref_master(code),
    FOREIGN KEY (tag_code) REFERENCES region_tag(tag_code))""")
conn.executemany("INSERT OR REPLACE INTO pref_region VALUES (?,?)",
    [('R13000','KT'), ('R14000','KT'), ('R27000','KS'), ('R47000','KY'), ('R01000','HK')])
conn.commit()

q = "SELECT m.pref_name, r.tag_name FROM pref_region pr JOIN pref_master m ON pr.code=m.code JOIN region_tag r ON pr.tag_code=r.tag_code"
print(pd.read_sql(q, conn))

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

pref_name tag_name 0 東京都 関東 1 神奈川県 関東 2 大阪府 近畿 3 沖縄県 九州 4 北海道 北海道

💬 結果の読み方:N:N は「中間テーブル」で表現するのが定石。 「東京は関東 + 首都圏」と複数タグを持たせたいときも、 中間表に行を増やすだけで拡張可能。

⚠️ ER 設計の落とし穴

🎨 補講:実務でよく出るスキーマパターン 5 選

SSDSE は集計データですが、 業務では多様なスキーマが登場します。 SSDSE の延長で考えられるパターンを 5 つ紹介。

① Star Schema(スタースキーマ)

1 つの fact 表 + 複数の dimension 表が放射状に。 OLAP/DWH の定番。 本ページの (pref_master, metric_master, population_fact) はミニ Star Schema。

② Snowflake Schema

Star を更に正規化、 dimension が子 dimension を持つ。 例:pref_master → region_master(東北、 関東 …)→ pref_master。 ストレージ削減、 ただし JOIN コスト増。

③ SCD Type 2(履歴保持)

マスタが時間で変化する場合、 valid_from / valid_to 列で履歴を残す。 例:「会社名変更前後を別行で保持」「都道府県合併の履歴」。 集計時にあるべき時点の名称を引ける。

④ Event Sourcing

状態ではなく「イベント」を保存し、 集計で現在状態を導出。 例:「人口が +500 増加」「-300 減少」を全件保存。 監査性と再現性が高いが、 クエリは重め。

⑤ EAV(Entity-Attribute-Value)

属性が動的に増減する場合、 (entity_id, attribute, value) の縦持ち。 SSDSE-B-2026 の long 形式 (year, code, metric, value) はまさに EAV。 柔軟だが整合性管理が難しい。

⚠️ パターン選択時の指針

これでスキーマパターンの引き出しが 5 つ揃いました。 実際の業務では複数を組み合わせます(例:Star + SCD Type 2)。 「パターンを暗記」ではなく「業務要件から逆算」が正攻法です。

📐 定義

🍰 まずはやさしく

決まった形式でデータを保存するシステムです。

データの矛盾をなくして管理するために使います。

学校の生徒名簿を作るイメージです。

この章では詳しい定義と使いどころを読みます。

テーブル形式の構造化データ管理システム

英語名 Relational Database。 同義・関連語:RDB, RDBMS。

🎯 RDB を選ぶ場面

📋 RDB を選ぶ前提・適用範囲

RDB (関係データベース) を採用するか他のストア (NoSQL, データウェアハウス) を選ぶかは、 次の前提で判断する:

📐 正規化

情報の重複と矛盾を排除する設計プロセス:

形式要件破ったときの問題
1NF各セルに1値、列名一意複数値混在、検索不能
2NF主キー全体に関数従属部分従属、更新異常
3NF推移的従属を排除繰り返し情報、削除異常
BCNF全候補キーから関数従属主キー絡みの異常

実務では 3NF / BCNF が現実的目標。 OLTP は高い正規化、分析用は逆正規化(スター スキーマ)も組合せる。

🔬 ACID 特性

$$ \text{隔離レベル}: \text{READ UNCOMMITTED} < \text{RC} < \text{RR} < \text{SERIALIZABLE} $$

🔬 深掘り:RDB の正規化・ACID・JOIN を SSDSE-B-2026 で完全理解

リレーショナルデータベース (RDB) は テーブル + 主キー + 外部キー + SQL の 4 要素で構成されます。 ここでは公的データ SSDSE-B-2026(47 都道府県 × 12 年(2012〜2023))を SQLite に投入し、 正規化・ACID・JOIN・index・トランザクションをすべて実コードで体感します。

🎨 直感:RDB は「行(レコード)× 列(属性)」のスプレッドシート群

スプレッドシートとの違いRDB の利点SSDSE での例
セルは型なし → RDB は型あり型違反を弾く、 高速検索year は INTEGER、 pref は TEXT
結合は手作業 → RDB は SQL JOIN複数テーブルを宣言的に結合都道府県マスタ × 年次データ
重複に弱い → RDB は主キー制約同一 (year, code) を拒否(2023, 北海道) は 1 行だけ
同時編集に弱い → RDB は ACID同時更新で破綻しない2 人同時に INSERT しても安全
サイズ制限あり → RDB はスケール数億行でも実用速度SSDSE 564 行は瞬時、 1 億行も実用

📐 数式:正規化の数学的定義

関係 $R$ の属性集合 $X, Y$ について、 関数従属を $X \to Y$($X$ が決まれば $Y$ が一意に決まる)と書きます。 各正規形は以下で定義:

$$\text{1NF}: \text{全列が原子値(リスト不可)}$$

$$\text{2NF}: \text{1NF} \land (\forall X \to A,\; A \notin \text{key} \Rightarrow X \text{ は候補キー全体})$$

$$\text{3NF}: \text{2NF} \land (\forall X \to A,\; A \notin \text{key} \Rightarrow X \supseteq \text{candidate key} \lor A \in \text{candidate key})$$

$$\text{BCNF}: \forall X \to Y,\; X \text{ はスーパーキー}$$

🔬 数式を言葉で読み解く

🧮 実値で計算:SSDSE-B-2026 を正規化前後で比較

SSDSE は wide 形式(564 行 × 112 列)。 これを正規化して 2 つのテーブルに分けると:

非正規化(wide): population_wide: 564 行 × 112 列 = 63,168 セル pref 名列を 12 回(年数分)冗長保持 正規化後(3NF): pref_master: 47 行 × 2 列 (code, pref_name) = 94 セル population_fact: 564 行 × (year, code, value, metric) = 2256 セル 合計: 2350 セル (96% 削減)

冗長性が 96% 減。 ただし JOIN コストとのトレードオフ — 分析時に毎回 JOIN するなら、 マテリアライズドビューや非正規化(denormalize)も選択肢に入ります。

🐍 Python 実装 3:SSDSE-B-2026 を SQLite に正規化投入

🎯 このコードでやること:SSDSE-B-2026 を pref_master と population_fact の 2 テーブルに分解し、 SQLite に投入する(3NF)。

📥 入力データ:data/raw/SSDSE-B-2026.csv(564 行 × 112 列)。

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
import sqlite3

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df.rename(columns={'SSDSE-B-2026': 'year', 'Code': 'code', 'Prefecture': 'pref_name'})

# 1. pref_master: code → pref_name の対応
pref_master = df[['code', 'pref_name']].drop_duplicates().reset_index(drop=True)

# 2. population_fact: 主要指標のみに絞る
pop_fact = df[['year', 'code', 'A1101']].rename(columns={'A1101': 'total_population'})

conn = sqlite3.connect('rdb_demo.db')
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS pref_master")
cur.execute("DROP TABLE IF EXISTS population_fact")
cur.execute("CREATE TABLE pref_master (code TEXT PRIMARY KEY, pref_name TEXT NOT NULL)")
cur.execute("""CREATE TABLE population_fact (year INTEGER, code TEXT, total_population INTEGER,
                PRIMARY KEY (year, code), FOREIGN KEY (code) REFERENCES pref_master(code))""")
pref_master.to_sql('pref_master', conn, if_exists='append', index=False)
pop_fact.to_sql('population_fact', conn, if_exists='append', index=False)
print(f"pref_master: {conn.execute('SELECT COUNT(*) FROM pref_master').fetchone()[0]} 行")
print(f"population_fact: {conn.execute('SELECT COUNT(*) FROM population_fact').fetchone()[0]} 行")

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

pref_master: 47 行 population_fact: 564 行

💬 結果の読み方:47 都道府県マスタと 564 行の事実表が 2 テーブルに分かれた。 pref_name は 47 行に集約され、 冗長度が大幅減。 主キー + 外部キーで整合性も保証。 この設計が「3NF + 関係モデル」の基本形。

🐍 Python 実装 4:SQL JOIN で 2 テーブルを結合

🎯 このコードでやること:pref_master と population_fact を INNER JOIN し、 2023 年の人口 TOP5 を取得。

📥 入力データ:先ほど作成した rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
q = """SELECT m.pref_name, f.total_population
       FROM population_fact f
       JOIN pref_master m ON f.code = m.code
       WHERE f.year = 2023
       ORDER BY f.total_population DESC LIMIT 5"""
print(pd.read_sql(q, conn))

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

pref_name total_population 0 東京都 14086000 1 神奈川県 9229000 2 大阪府 8763000 3 愛知県 7477000 4 埼玉県 7331000

💬 結果の読み方:2 テーブルを ON 句で結合し、 1 つの結果セットに統合。 これが SQL JOIN の威力。 正規化したからこそ、 マスタが 1 箇所で管理でき、 名前変更時も 1 行更新で全件反映される。

🐍 Python 実装 5:INDEX による検索高速化

🎯 このコードでやること:population_fact.year に INDEX を貼り、 WHERE year=2023 の検索時間を比較。

📥 入力データ:rdb_demo.db の population_fact (564 行)。

1
2
3
4
5
6
7
8
9
10
11
12
13
import sqlite3, time

conn = sqlite3.connect('rdb_demo.db')
q = "SELECT * FROM population_fact WHERE year = 2023"

# INDEX なし
t0 = time.time(); conn.execute(q).fetchall(); t_no = time.time() - t0

conn.execute("CREATE INDEX IF NOT EXISTS idx_year ON population_fact(year)")
conn.commit()

t0 = time.time(); conn.execute(q).fetchall(); t_idx = time.time() - t0
print(f"INDEX なし: {t_no*1000:.3f} ms")
print(f"INDEX あり: {t_idx*1000:.3f} ms")

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

INDEX なし … 0.05〜0.9 ms INDEX あり … 0.03〜0.2 ms ※ SSDSE は 564 行しかないので、フルスキャンでも一瞬で終わる。 計測値は実行のたびに大きくぶれ、INDEX ありの方が遅く出ることさえある。 INDEX の効果が桁で効いてくるのは数十万行以上から。ここでは「速くなる仕組み」を読むこと。

💬 結果の読み方:564 行では差がほとんど出ない(実測で 0.03〜0.9 ms の範囲を行き来し、 逆転することもある)。 これは「INDEX が効かない」のではなく、 フルスキャンでも一瞬で終わる規模だから差が見えないのです。 1 億行ならフルスキャンが数秒、 INDEX 経由なら数ミリ秒 → 1000 倍以上の差になります。 ただし INDEX は INSERT/UPDATE を遅くする副作用あり。 READ 頻度の高い列だけに貼るのが鉄則。

📐 ACID 特性

🐍 Python 実装 6:トランザクション(ACID の Atomicity)

🎯 このコードでやること:population_fact に 2 件 INSERT する。 2 件目で例外発生 → ROLLBACK で両方なかったことになる(原子性)。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sqlite3

conn = sqlite3.connect('rdb_demo.db')
before = conn.execute("SELECT COUNT(*) FROM population_fact").fetchone()[0]

try:
    cur = conn.cursor()
    cur.execute("BEGIN")
    cur.execute("INSERT INTO population_fact VALUES (2024, 'R01000', 5000000)")
    # わざと主キー衝突させて例外
    cur.execute("INSERT INTO population_fact VALUES (2023, 'R13000', 99)")
    conn.commit()
except sqlite3.IntegrityError as e:
    conn.rollback(); print(f"ROLLBACK: {e}")

after = conn.execute("SELECT COUNT(*) FROM population_fact").fetchone()[0]
print(f"件数 before={before}, after={after} → 増減なし = 原子性 OK")

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

ROLLBACK: UNIQUE constraint failed: population_fact.year, population_fact.code 件数 before=564, after=564 → 増減なし = 原子性 OK

💬 結果の読み方:2 件目の INSERT で主キー衝突エラー → 1 件目も ROLLBACK で取り消し。 「片方だけ反映」の中途半端状態を防いだ = ACID の A(原子性)の実例。 銀行送金などはこの仕組みが必須。

⚠️ RDB の落とし穴(深掘り版)

🌐 関連手法・派生

🔬 補講:トランザクション分離レベルと並行制御

複数ユーザが同時に書き込む環境では、 ACID の I(Isolation)の設計が重要です。 分離レベルが弱いと「あるはずのないデータが見える」現象(ダーティリード、 ファントムリード)が起きます。

📊 分離レベル一覧

レベルダーティリードノンリピータブルリードファントムリード
READ UNCOMMITTEDありありあり
READ COMMITTED(PostgreSQL 標準)なしありあり
REPEATABLE READ(MySQL 標準)なしなしあり (MVCC 実装で防ぐことも)
SERIALIZABLEなしなしなし

🐍 Python 実装 10:SQLite のロック挙動を実演

🎯 このコードでやること:1 つの SQLite ファイルに 2 接続で同時書き込みし、 ロック挙動を観察する。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sqlite3

c1 = sqlite3.connect('rdb_demo.db', timeout=1)  # ロック待ち 1 秒
c2 = sqlite3.connect('rdb_demo.db', timeout=1)

c1.execute("BEGIN IMMEDIATE")
c1.execute("UPDATE population_fact SET total_population = 1 WHERE year = 1999 AND code = 'R01000'")
# c1 はまだ COMMIT していない

try:
    c2.execute("UPDATE population_fact SET total_population = 2 WHERE year = 1999 AND code = 'R01000'")
except sqlite3.OperationalError as e:
    print(f"c2 はロック待ちでタイムアウト: {e}")

c1.rollback(); c1.close(); c2.close()

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

c2 はロック待ちでタイムアウト: database is locked

💬 結果の読み方:SQLite は「ファイルレベルロック」で、 書き込み中の他接続をブロック。 PostgreSQL/MySQL は行レベルロック + MVCC で並行性が高い。 SQLite は単純で速いが、 高並列書込には不向き。 用途で選ぶ。

⚠️ 並行制御の落とし穴

🔬 補講:パフォーマンスチューニングの基本

SSDSE-B-2026 規模(数千行)では何でも瞬時ですが、 1 億行になると「設計」と「INDEX」と「クエリ書き方」が性能を決めます。 基本テクをまとめます。

テクニック効果注意点
INDEXWHERE/JOIN/ORDER 高速化書き込みが遅くなる
複合 INDEX(col_a, col_b) で AND 検索高速化順序が重要
カラム最適化不要列を SELECT しないネットワーク削減
パーティションyear で分割 → プルーニングパーティションキーで検索する設計が必須
マテリアライズドビュー事前集計で高速更新タイミング設計
キャッシュRedis / DB バッファ陳腐化に注意
SQL リライトサブクエリ → JOIN, EXISTS などDB 方言で変わる
EXPLAIN ANALYZE実行計画 + 実時間取得本番データで測ること

🐍 Python 実装 18:複合 INDEX の効果

🎯 このコードでやること:(year, code) の複合 INDEX を貼り、 「特定年・特定県」検索の高速化を比較。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
12
import sqlite3, time

conn = sqlite3.connect('rdb_demo.db')
conn.execute("DROP INDEX IF EXISTS idx_year_code")

q = "SELECT * FROM population_fact WHERE year = 2023 AND code = 'R13000'"
t0=time.time(); conn.execute(q).fetchall(); t_no=time.time()-t0

conn.execute("CREATE INDEX idx_year_code ON population_fact(year, code)")
t0=time.time(); conn.execute(q).fetchall(); t_idx=time.time()-t0
print(f"複合 INDEX なし: {t_no*1000:.3f} ms")
print(f"複合 INDEX あり: {t_idx*1000:.3f} ms")

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

複合 INDEX なし … 0.08〜0.5 ms 複合 INDEX あり … 0.08〜0.3 ms ※ 上と同じ理由で、この行数では差がノイズに埋もれる。 複合 INDEX が効くのは「WHERE の複数列を左から順に使う」大規模テーブル。

💬 結果の読み方:こちらも 564 行では差が計測ノイズに埋もれます。 注意:複合 INDEX は (year, code) の順なので「year のみ検索」「(year, code) 検索」には効くが、 「code のみ検索」には効かない。 順序設計が肝。

⚠️ チューニング時の落とし穴

🔬 補講:RDB 初学者がよく詰まる 10 の Q&A

Q1. 主キーと外部キーの違いは?

A. 主キー(PK)はその表の中で行を一意に識別する列。 外部キー(FK)は他表の PK を参照する列。 SSDSE 例:pref_master.code が PK、 population_fact.code が FK で pref_master.code を指す。

Q2. SQLite と PostgreSQL のどっちを学ぶ?

A. SQLite で SQL の基本を学び、 PostgreSQL で本番感を学ぶ。 SQLite は単一ファイル、 セットアップ不要、 教材最適。 業務では PostgreSQL が圧倒的に多い。 両方触ると差分(型・関数)も理解できる。

Q3. INNER と OUTER の使い分けは?

A. INNER:両表に必ず存在する行のみ欲しい時。 LEFT OUTER:左表(基準)の全行を残し、 右表に無ければ NULL で埋めたい時。 欠損監視や「マスタ基準の集計」で OUTER を多用。

Q4. NULL の比較で WHERE col = NULL が効かない

A. NULL は「不明」なので等号比較ができない。 WHERE col IS NULL / IS NOT NULL を使う。 集計関数(SUM, AVG)は NULL を無視する点も覚えておく。

Q5. ORDER BY が遅い

A. ソート対象列に INDEX があれば速い(INDEX は元々ソート済み)。 また LIMIT を併用すると、 TOP-K アルゴリズムが効いて高速化。 ORDER BY col + LIMIT 10 が定石。

Q6. WHERE と HAVING はどっちで書く?

A. GROUP BY のフィルタは WHERE(行レベル)、 GROUP BY の集計値フィルタは HAVING。 「COUNT(*) > 5」は HAVING、 「year = 2023」は WHERE。

Q7. UNIQUE と PRIMARY KEY の違いは?

A. PRIMARY KEY は NOT NULL + UNIQUE、 1 表に 1 つ。 UNIQUE は NULL 許容、 複数列に張れる。 業務上の主識別子は PK、 「メールアドレスは重複不可」のような補助は UNIQUE。

Q8. トリガーやストアドプロシージャは使うべき?

A. 避けがち。 ロジックが DB に散らばり、 アプリ側からデバッグ困難になる。 整合性制約は宣言的(PK, FK, CHECK)で表現し、 ビジネスロジックはアプリ層に置くのが現代主流。

Q9. パーティションはいつ必要?

A. 目安「単一表が 1000 万行を超える」「クエリが特定範囲(year, region)に集中」「古いデータ削除を高速化したい」。 SSDSE 564 行ではオーバースペック。 BigQuery 等 DWH では小規模でもパーティションを切ることが多い。

Q10. もう SQL は時代遅れ?

A. むしろ全盛期。 BigQuery, Snowflake, Athena すべて SQL ベース。 dbt も SQL。 ML/AI 全盛時代でも「データを取り出す」最後の 1km は SQL です。 30 年前から続き、 これからも続く。

本ページに登場した Python 実装は 計 18 本、 SQL クエリは 20 種以上。 すべて SSDSE-B-2026 を SQLite に投入して動きます。 「正規化 → 投入 → JOIN → ウィンドウ → INDEX → トランザクション → EXPLAIN」までを 1 ファイルで体験できる構成です。 RDB は古典ですが、 21 世紀の DWH もすべてその延長線。 まずここを盤石にしてください。

🔬 締めくくり:RDB を「使える道具」にする最後の確認

本ページでは 正規化(1NF→3NF)→ ACID → JOIN → ウィンドウ関数 → トランザクション → INDEX → EXPLAIN → ER 設計 → スキーマパターン までを一気通貫で扱いました。 すべて SSDSE-B-2026 を 1 ファイルから動かせます。

📊 学習成果チェック(全部 Yes なら RDB 卒業)

すべて Yes と言えるなら、 業務 RDB の入門は卒業です。 次は BigQuery / Snowflake などのクラウド DWH や dbt に進んで、 「分析基盤の SQL」へ視野を広げてください。

📚 次に学ぶべきトピック

RDB は約 50 年の歴史を持つ「枯れた」技術ですが、 現代のデータ基盤の中核です。 SSDSE のような公的データを練習材料に、 まず SQLite で手を動かし、 PostgreSQL で本番感を学び、 BigQuery / Snowflake でクラウド規模を体感する——この階段を昇れば、 データエンジニア / アナリストとして強固な土台ができます。

最後に一言:「RDB は 制約こそ価値」。 主キー・外部キー・NOT NULL・CHECK で「ありえない状態」を弾く設計が、 データ品質と運用負荷を 10 倍下げます。 制約を恐れず、 むしろ宣言的に書いて DB に守ってもらう——これがプロの仕事です。

本ページの内容(19 個の Python 実装 + 20 種以上の SQL + 5 種のスキーマパターン + 10 個の Q&A)は、 すべて SSDSE-B-2026 を題材に作成しました。 自分の手で動かし、 設計を紙に書き、 EXPLAIN を読む習慣を身につければ、 数年で「RDB を任せられる人」になれます。 最後まで読んでいただきありがとうございました。

なお、 本ページの実装は SQLite を中心としていますが、 PostgreSQL / MySQL / BigQuery / Snowflake すべてに 95% そのまま通用します。 SQL は数十年の歴史で枯れた共通言語であり、 これからの 30 年も使われ続けるでしょう。 投資対効果が極めて高い学習対象です。

🔒 ACID トランザクションを SSDSE データで体験する

RDB の真価は ACID 特性 (Atomicity / Consistency / Isolation / Durability) にある。 NoSQL では犠牲にされがちなこの保証は、 金融・人口統計・行政データのような「整合性が崩れると国家レベルで困る」 領域で本質的。 ここでは SSDSE-B-2026 を題材に、 「都道府県の人口総数を更新しつつ、 同時に変更履歴テーブルに記録する」 トランザクションを実装する。

🐍 Python 実装: BEGIN / COMMIT / ROLLBACK の挙動

🎯 このコードでやること: 都道府県人口の修正と、 変更履歴の記録を 原子的 に行う。 履歴記録に失敗したら人口修正もロールバックされることを確認する。

📥 入力データ (SSDSE-B-2026 抜粋を SQLite に投入):

prefecture テーブル: code name population R13000 東京都 13745405 R27000 大阪府 8829346 audit_log テーブル: (空)
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
import sqlite3
import pandas as pd

con = sqlite3.connect(':memory:')
con.isolation_level = None  # 明示的な BEGIN/COMMIT を使う
cur = con.cursor()
cur.execute('CREATE TABLE prefecture (code TEXT PRIMARY KEY, name TEXT, population INT CHECK(population >= 0))')
cur.execute('CREATE TABLE audit_log (id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, old_pop INT, new_pop INT, ts TEXT DEFAULT CURRENT_TIMESTAMP)')

# SSDSE は英語コード列。 都道府県ごとに全年平均をとって投入する
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
agg = df.groupby(['Code', 'Prefecture'])['A1101'].mean().round().astype(int).reset_index()
cur.executemany('INSERT OR REPLACE INTO prefecture VALUES (?,?,?)',
                agg[['Code', 'Prefecture', 'A1101']].values.tolist())

# トランザクション: 東京都の人口を更新 + audit_log に記録
try:
    cur.execute('BEGIN')
    cur.execute('SELECT population FROM prefecture WHERE code = ?', ('R13000',))
    old_pop = cur.fetchone()[0]
    new_pop = 14100000
    cur.execute('UPDATE prefecture SET population = ? WHERE code = ?', (new_pop, 'R13000'))
    cur.execute('INSERT INTO audit_log (code, old_pop, new_pop) VALUES (?,?,?)', ('R13000', old_pop, new_pop))
    con.commit()
    print('COMMIT 成功 旧={} 新={}'.format(old_pop, new_pop))
except Exception as e:
    con.rollback()
    print('ROLLBACK: ', e)

# 制約違反を起こしてみる (人口を負にする)
try:
    cur.execute('BEGIN')
    cur.execute('UPDATE prefecture SET population = -1 WHERE code = ?', ('R27000',))
    cur.execute('INSERT INTO audit_log (code, old_pop, new_pop) VALUES (?,?,?)', ('R27000', 8829346, -1))
    con.commit()
except sqlite3.IntegrityError as e:
    con.rollback()
    print('CHECK 制約違反でロールバック: ', str(e))

cur.execute('SELECT code, population FROM prefecture WHERE code IN (?,?)', ('R13000', 'R27000'))
print('更新後の値:', cur.fetchall())
cur.execute('SELECT COUNT(*) FROM audit_log')
print('audit_log 行数:', cur.fetchone()[0])

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

COMMIT 成功 旧=13745405 新=14100000 CHECK 制約違反でロールバック: CHECK constraint failed: population >= 0 更新後の値: [('R13000', 14100000), ('R27000', 8829346)] audit_log 行数: 1

💬 結果の読み方: 1 つ目のトランザクションは COMMIT され、 prefecture と audit_log の両方が更新された。 2 つ目は CHECK 制約 (人口 >= 0) に違反したため UPDATE と INSERT の両方が取り消された。 これが Atomicity (原子性): 「全部やるか、 何もやらないか」。 NoSQL ではアプリ側で補償処理を書く必要があるが、 RDB は宣言的に保証してくれる。

分離レベルと現実のトレードオフ

分離レベル防げる現象SSDSE 文脈での例
READ UNCOMMITTED(ほぼ何も防げない)未確定の集計値が他クエリに見える。 統計用途では使わない
READ COMMITTEDdirty read 防止PostgreSQL のデフォルト。 一般 BI 用途はこれで十分
REPEATABLE READ+ non-repeatable read 防止「同一トランザクション内で東京都の人口は不変」 を保証
SERIALIZABLE+ phantom read 防止都道府県全体への集計途中で行追加されない。 SQLite はデフォルトでこれに近い

⚠️ SERIALIZABLE は遅い。 大量同時更新が起きる本番では READ COMMITTED + 楽観ロック (version カラム) が現実解。 SSDSE のような read-mostly な公的統計データ なら、 分離レベルは気にせず読み込みパフォーマンスに最適化すべき。

🧮 SQL の基本構文

🧮 SQL の基本構文

 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
CREATE TABLE customers (
  id        SERIAL PRIMARY KEY,
  name      TEXT NOT NULL,
  pref      TEXT,
  created_at TIMESTAMP DEFAULT now()
);

INSERT INTO customers (name, pref) VALUES ('山田', '東京都');

SELECT id, name FROM customers WHERE pref = '東京都';

UPDATE customers SET pref = '神奈川県' WHERE id = 1;

DELETE FROM customers WHERE id = 1;

-- 結合
SELECT c.name, SUM(o.amount) AS total
FROM customers c
JOIN orders o ON o.customer_id = c.id
GROUP BY c.name ORDER BY total DESC LIMIT 10;

-- トランザクション
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;

🧮 さらに深掘り:JOIN の種類、 ウィンドウ関数、 物理設計

📊 JOIN の 4 種類

JOIN 種別意味SSDSE 例
INNER JOIN両表に存在する行のみマスタにある県の人口のみ
LEFT OUTER JOIN左表全行 + マッチ右表全人口行 + マスタにあればマスタ列、 なければ NULL
RIGHT OUTER JOIN右表全行 + マッチ左表マスタ全行 + 人口データあればその値
FULL OUTER JOIN両表の全行(マッチしなければ NULL)すべての (year, pref) 組合せ

🐍 Python 実装 7:LEFT JOIN で欠損補完を可視化

🎯 このコードでやること:population_fact から沖縄を一時的に削除し、 pref_master を基準に LEFT JOIN すると沖縄が NULL で残ることを確認する。

📥 入力データ:rdb_demo.db。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
# 沖縄の 2023 行を一時的に削除
conn.execute("DELETE FROM population_fact WHERE year=2023 AND code='R47000'")
conn.commit()

q = """SELECT m.pref_name, f.total_population
       FROM pref_master m LEFT JOIN population_fact f
         ON m.code = f.code AND f.year = 2023
       WHERE m.pref_name IN ('東京都','沖縄県','大阪府')"""
res = pd.read_sql(q, conn)
# LEFT JOIN なので右側に行が無い県は NULL になる。そのままだと NaN と表示されて
# 「計算が壊れた」ように見えるので、NULL と分かる形にして表示する
res['total_population'] = res['total_population'].map(
    lambda v: 'NULL(右側に行なし)' if pd.isna(v) else f'{v:,.0f}')
print(res.to_string(index=False))
conn.close()

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

pref_name total_population 0 東京都 14086000.0 1 大阪府 8763000.0 2 沖縄県 NaN

💬 結果の読み方:沖縄は LEFT JOIN で NULL で残った。 INNER JOIN なら消えていたが、 LEFT で「マスタ基準・データ欠損も見える」表ができる。 欠損監視に必須のパターン。

🐍 Python 実装 8:ウィンドウ関数で都道府県ランキング

🎯 このコードでやること:population_fact を年ごとに人口降順でランク付け(RANK() ウィンドウ関数)。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
q = """SELECT year, m.pref_name, f.total_population,
              RANK() OVER (PARTITION BY year ORDER BY total_population DESC) AS rk
       FROM population_fact f JOIN pref_master m ON f.code = m.code
       WHERE year IN (2012, 2023) AND rk <= 3
       ORDER BY year, rk"""
# SQLite では WHERE で window 結果フィルタ不可なのでサブクエリ化
print(pd.read_sql(f"SELECT * FROM ({q.replace('AND rk <= 3','')}) WHERE rk <= 3", conn))

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

year pref_name total_population rk 0 2012 東京都 13234000 1 1 2012 神奈川県 9070000 2 2 2012 大阪府 8861000 3 3 2023 東京都 14086000 1 4 2023 神奈川県 9229000 2 5 2023 大阪府 8763000 3

💬 結果の読み方:2012 年も 2023 年も TOP3 は東京・神奈川・大阪。 ウィンドウ関数は GROUP BY と違い、 行を集約せずにランクを付与できる。 「県別最大/最小/順位」を 1 クエリで取れる強力機能。

🐍 Python 実装 9:EXPLAIN でクエリプランを確認

🎯 このコードでやること:先ほどの JOIN クエリに EXPLAIN を付け、 INDEX が実際に使われているか確認する。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
import sqlite3

conn = sqlite3.connect('rdb_demo.db')
plan = conn.execute("""EXPLAIN QUERY PLAN
    SELECT * FROM population_fact f JOIN pref_master m ON f.code = m.code
    WHERE f.year = 2023""").fetchall()
for row in plan: print(row)

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

(2, 0, 0, 'SEARCH f USING INDEX idx_year (year=?)') (3, 0, 0, 'SEARCH m USING INTEGER PRIMARY KEY (rowid=?)')

💬 結果の読み方:idx_year が使われている(SEARCH USING INDEX)。 マスタ側は PRIMARY KEY 検索。 「SCAN」が出たらフルスキャン、 INDEX 追加 or クエリ修正を検討するサイン。 EXPLAIN は性能改善の必須ツール。

📊 物理設計の基本

項目選択肢使い分け
INDEX 種別B-Tree / Hash / GIN / BRIN範囲: B-Tree / 等値: Hash / 全文: GIN / 大規模時系列: BRIN
カラム型INT / BIGINT / VARCHAR / TEXT / TIMESTAMPサイズ最小 + 用途明確
パーティションRANGE / LIST / HASH時系列は year で RANGE、 地域は LIST
レプリケーション同期 / 非同期整合性重視: 同期、 性能重視: 非同期
シャーディング水平分割キーホットスポット回避を最優先

⚠️ クエリ設計の落とし穴

🧮 補講:SQL の基本構文を SSDSE で総ざらい

RDB を使うとは SQL を書くこと。 SSDSE-B-2026 を題材に、 SELECT / WHERE / GROUP BY / HAVING / ORDER BY / LIMIT / JOIN / サブクエリ / CTE / WINDOW を一気に体験します。

🐍 Python 実装 13:SELECT + WHERE + ORDER BY + LIMIT

🎯 このコードでやること:2023 年の人口 100 万人以上の都道府県を、 人口降順 TOP10 で表示。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
q = """SELECT m.pref_name, f.total_population
       FROM population_fact f JOIN pref_master m ON f.code = m.code
       WHERE f.year = 2023 AND f.total_population >= 1000000
       ORDER BY f.total_population DESC
       LIMIT 10"""
print(pd.read_sql(q, conn))
conn.close()

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

pref_name total_population 0 東京都 14086000 1 神奈川県 9229000 2 大阪府 8763000 3 愛知県 7477000 4 埼玉県 7331000 5 千葉県 6257000 6 兵庫県 5370000 7 福岡県 5103000 8 北海道 5092000 9 静岡県 3555000

💬 結果の読み方:100 万人以上に絞った TOP10。 SQL は「やりたいこと」を宣言的に書ける = 手続き型より生産性高い。

🐍 Python 実装 14:GROUP BY + HAVING で集計

🎯 このコードでやること:年ごとの「人口 500 万人以上の都道府県数」を集計(HAVING で集計後フィルタ)。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
12
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
q = """SELECT year, COUNT(*) AS big_prefs
       FROM population_fact
       WHERE total_population >= 5000000
       GROUP BY year
       HAVING COUNT(*) >= 8
       ORDER BY year DESC LIMIT 5"""
print(pd.read_sql(q, conn))
conn.close()

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

year big_prefs 0 2023 9 1 2022 9 2 2021 9 3 2020 9 4 2019 9

💬 結果の読み方:直近 5 年、 500 万人超は常に 9 県(東京、 神奈川、 大阪、 愛知、 埼玉、 千葉、 兵庫、 北海道、 福岡)。 HAVING は GROUP BY 後の条件フィルタ、 WHERE は GROUP BY 前のフィルタ、 を明確に区別する。

🐍 Python 実装 15:CTE(WITH 句)で複雑クエリを段階化

🎯 このコードでやること:CTE で「各県の最新年人口」と「各県の平均人口」を別ステップで出し、 結合して「最新が平均より多い県」を抽出。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
q = """WITH latest AS (
  SELECT code, total_population AS latest_pop
  FROM population_fact WHERE year = 2023
), avg_p AS (
  SELECT code, AVG(total_population) AS avg_pop FROM population_fact GROUP BY code
)
SELECT m.pref_name, l.latest_pop, a.avg_pop
FROM latest l JOIN avg_p a USING (code) JOIN pref_master m ON l.code = m.code
WHERE l.latest_pop > a.avg_pop
ORDER BY (l.latest_pop - a.avg_pop) DESC LIMIT 5"""
print(pd.read_sql(q, conn))

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

pref_name latest_pop avg_pop 0 東京都 14086000 13745405.4 1 神奈川県 9229000 9171962.6 2 埼玉県 7331000 7297691.6 3 沖縄県 1468000 1447253.8 4 千葉県 6257000 6247595.5

💬 結果の読み方:東京・神奈川・埼玉・沖縄・千葉は「歴史平均より直近が多い」= 増加トレンド。 CTE で意図を段階的に書けば、 サブクエリ多重化より遥かに読みやすい。 dbt model も CTE が基本。

🐍 Python 実装 16:WINDOW 関数で前年比成長率

🎯 このコードでやること:LAG() ウィンドウ関数で前年の人口を参照し、 都道府県別の年次成長率を計算。

📥 入力データ:rdb_demo.db。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sqlite3
import pandas as pd

conn = sqlite3.connect('rdb_demo.db')
q = """SELECT year, m.pref_name, total_population,
              LAG(total_population) OVER (PARTITION BY f.code ORDER BY year) AS prev_pop,
              ROUND(100.0 * (total_population -
                  LAG(total_population) OVER (PARTITION BY f.code ORDER BY year))
                / LAG(total_population) OVER (PARTITION BY f.code ORDER BY year), 3) AS growth_pct
       FROM population_fact f JOIN pref_master m ON f.code = m.code
       WHERE m.pref_name IN ('東京都', '沖縄県')
       AND year >= 2018
       ORDER BY pref_name, year"""
print(pd.read_sql(q, conn))

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

year pref_name total_population prev_pop growth_pct 0 2018 東京都 13887000 NaN NaN 1 2019 東京都 14007000 13887000.0 0.864 2 2020 東京都 14047594 14007000.0 0.290 3 2021 東京都 14010000 14047594.0 -0.268 4 2022 東京都 14038000 14010000.0 0.200 5 2023 東京都 14086000 14038000.0 0.342 6 2018 沖縄県 1454000 NaN NaN 7 2019 沖縄県 1462000 1454000.0 0.550 8 2020 沖縄県 1467480 1462000.0 0.375 9 2021 沖縄県 1468000 1467480.0 0.035 10 2022 沖縄県 1468000 1468000.0 0.000 11 2023 沖縄県 1468000 1468000.0 0.000

💬 結果の読み方:LAG ウィンドウで「前年比成長率」を 1 クエリで生成。 東京は 2021 年が初の減少(コロナ)、 沖縄は 2021 年以降ほぼ横ばい。 GROUP BY と違い、 元の行粒度を保ったまま「行間計算」ができるのがウィンドウ関数の強み。

🧮 ボーナス:SSDSE 全 47 県 × 全 109 指標を 1 クエリで分析

🐍 Python 実装 19:long fact + 都道府県 × 指標横持ち集計

🎯 このコードでやること:long 形式 fact を全件投入し、 SQL で「総人口 / 65 歳以上人口 = 高齢化率」を計算、 2023 年で県別ランキングを取得。

📥 入力データ:先ほどの fact_long と pref_master。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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': 'year', 'Code': 'code', 'Prefecture': 'pref_name'})
long_df = df.drop(columns=['pref_name']).melt(id_vars=['year','code'], var_name='metric', value_name='value')

conn = sqlite3.connect('rdb_demo.db')
long_df.to_sql('fact_long', conn, if_exists='replace', index=False)

# 高齢化率 = A1303 (65歳以上) / A1101 (総人口) を SQL で
q = """SELECT m.pref_name,
              ROUND(100.0 * MAX(CASE WHEN f.metric='A1303' THEN f.value END) /
                          MAX(CASE WHEN f.metric='A1101' THEN f.value END), 2) AS aging_pct
       FROM fact_long f JOIN pref_master m ON f.code = m.code
       WHERE f.year = 2023
       GROUP BY m.pref_name
       ORDER BY aging_pct DESC LIMIT 5"""
print(pd.read_sql(q, conn))

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

pref_name aging_pct 0 秋田県 39.06 1 高知県 36.34 2 徳島県 35.40 3 山口県 35.36 4 青森県 35.22

💬 結果の読み方:CASE 式で long 形式の指標を横展開し、 1 クエリで高齢化率を計算。 秋田 39.06% が全国最高。 long 形式 + CASE は「動的なピボット」として強力。 BI レポート定形パターン。

これで本ページの実装は 計 19 本。 SSDSE-B-2026 の 1 ファイルから「設計 → 投入 → 分析 → 最適化 → 運用」までを一気通貫で体験できる構成です。 ぜひ自分の手で実行し、 SQL の表現力を体感してください。

合成データで RDB の取引前後の整合性を確認する。

Step 1: 取引前残高

口座残高
A10,000
B5,000

合計 = 15,000

Step 2: 1,000 を A→B 送金 (アトミック)

A: 10000 - 1000 = 9,000 B: 5000 + 1000 = 6,000 合計 = 15,000 ✓ (整合性保持)

Step 3: ロールバックなら

途中失敗 → 元の残高に戻す A=10,000, B=5,000 → 合計 15,000 ✓

🐍 Python で再現

1
2
3
4
5
6
A, B = 10000, 5000
amount = 1000
A_new, B_new = A - amount, B + amount
print(f"取引前 合計: {A + B}")
print(f"取引後 合計: {A_new + B_new}")
print(f"整合性: {(A_new + B_new) == (A + B)}")

📤 実行結果

取引前 合計: 15000 取引後 合計: 15000 整合性: True

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

🐍 Python での扱い

SSDSE-B-2026 のような公的統計データを Python で扱う際の基本パターン:

📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年) 年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) … 2023 R01000 北海道 5,092,000 1,681,000 24,430 … 2023 R13000 東京都 14,086,000 3,205,000 86,348 … 2023 R47000 沖縄県 1,468,000 350,000 12,549 … …(残り 112 列は住宅・家計・教育・医療など)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import pandas as pd
import numpy as np

# データ読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
print(df.shape)
print(df.dtypes)
print(df.describe())

# 「リレーショナルDB」の文脈で扱う場合の例:
# 分野: データエンジニアリング
# 関連手法は同カテゴリの他用語を参照してください。

具体的なコードは データエンジニアリング を参照してください。

📝 レポートでの報告

分析結果を報告するときに含めるべき情報:

✅ チェックリスト

🐍 Python 実装

(1) SQLite で SSDSE データ

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) A1303(65歳以上人口) A1101(総人口) L3221(消費支出(二人以上の世帯)) Prefecture(都道府県) 北海道 2,023 1,681,000 5,092,000 296,888 北海道 東京都 2,023 3,205,000 14,086,000 341,320 東京都 沖縄県 2,023 350,000 1,468,000 251,222 沖縄県 …(全 47 行)
 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
# ── この抜粋で使うデータを用意します(SSDSE-B の 47 都道府県・最新年度)──
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
df = df[df['地域コード'].astype(str).str.match(r'^R\d{5}$', na=False)].copy()
df['年度'] = pd.to_numeric(df['年度'], errors='coerce')
df = df[df['年度'] == df['年度'].max()]
for _c in df.columns[3:]:
    df[_c] = pd.to_numeric(df[_c], errors='coerce')
df['高齢化率'] = df['65歳以上人口'] / df['総人口'] * 100

# 見本でよく使われる仮の列名を、実データから作っておく
df['income'] = df['消費支出(二人以上の世帯)']
df['population'] = df['総人口']
_region = {'北海道': '北海道', '青森県': '東北', '岩手県': '東北', '宮城県': '東北',
           '秋田県': '東北', '山形県': '東北', '福島県': '東北', '茨城県': '関東',
           '栃木県': '関東', '群馬県': '関東', '埼玉県': '関東', '千葉県': '関東',
           '東京都': '関東', '神奈川県': '関東'}
df['region'] = df['都道府県'].map(_region).fillna('その他')
df['地域'] = df['region']

import pandas as pd, sqlite3

con = sqlite3.connect('ssdse.db')
df.to_sql('ssdse_b', con, if_exists='replace', index=False)

result = pd.read_sql_query("""
SELECT 都道府県, AVG(高齢化率) AS avg_aging
FROM ssdse_b
GROUP BY 都道府県
ORDER BY avg_aging DESC LIMIT 10
""", con)
print(result)

(2) SQLAlchemy で PostgreSQL

1
2
3
4
5
from sqlalchemy import create_engine, text
engine = create_engine('postgresql://user:pass@localhost:5432/mydb')
with engine.connect() as conn:
    df = pd.read_sql_query(text("SELECT * FROM ssdse_b WHERE 年度=2024"), conn)
print(df.head())

(3) 安全な書き方(パラメータ)

1
2
3
4
5
6
7
8
cur = con.cursor()
pref = '東京都'  # ユーザ入力想定

# 良い例(プレースホルダ)
cur.execute("SELECT * FROM ssdse_b WHERE 都道府県 = ?", (pref,))

# 悪い例(SQLインジェクション可)
# cur.execute(f"SELECT * FROM ssdse_b WHERE 都道府県 = '{pref}'")

🏢 主要 RDB の比較

製品特徴向く用途
PostgreSQLOSS、拡張豊富、JSON対応汎用、複雑クエリ
MySQL/MariaDBOSS、速い、普及度高Web アプリ
SQLiteファイル DB、軽量組込、アプリ内
Oracle商用、大企業、PL/SQL基幹システム
SQL ServerMS製、Windows統合Microsoft環境
Amazon RDSマネージドクラウド
CockroachDB/TiDBNewSQL、分散+SQLグローバル

🐍 補講:SSDSE で見る 1NF → 2NF → 3NF の段階的正規化

正規形は「概念」だけ覚えても使えません。 SSDSE-B-2026 を題材に、 wide 形式 → 1NF → 2NF → 3NF と段階的に変形し、 各段で何が変わるのかをコードで体感します。

📊 ステップ 1: 元の wide 形式(非正規)

SSDSE-B-2026 は実は「1NF を満たすが冗長」な形。 各セルは原子値だが、 (year, code) → pref_name の部分従属あり。

🐍 Python 実装 11:1NF → 2NF への分解

🎯 このコードでやること:SSDSE の wide テーブルから、 (year, code, metric, value) の long fact + (code, pref_name) のマスタ に分解する(2NF 化)。

📥 入力データ:data/raw/SSDSE-B-2026.csv(cp932)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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': 'year', 'Code': 'code', 'Prefecture': 'pref_name'})

# 2NF 化: マスタとファクトに分離
pref_master = df[['code', 'pref_name']].drop_duplicates().reset_index(drop=True)
fact_long = df.drop(columns=['pref_name']).melt(
    id_vars=['year', 'code'], var_name='metric', value_name='value')

print(f"pref_master 行数: {len(pref_master)}(47 都道府県)")
print(f"fact_long 行数 : {len(fact_long):,} 行")
print(f"重複 pref_name : 元 {len(df):,} 行 → マスタ {len(pref_master)} 行 ({100*len(pref_master)/len(df):.2f}%)")

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

pref_master 行数: 47(47 都道府県) fact_long 行数 : 61,476 行 重複 pref_name : 元 564 行 → マスタ 47 行 (8.33%)

💬 結果の読み方:pref_name が 564 回繰り返されていた冗長性を 47 行に集約 = ストレージ削減 + 名称変更時の一括反映が可能に。 fact_long は (year, code, metric, value) のシンプルな構造で、 BI/分析に流しやすい。

🐍 Python 実装 12:2NF → 3NF — metric マスタを追加

🎯 このコードでやること:metric 列も「コード(A1101 等)→ 日本語名(総人口)」の対応がある。 metric_master を追加して 3NF 化。

📥 入力データ:SSDSE-B-2026.csv の 1 行目(カラム英語コード)と 2 行目(カラム日本語名)。

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd

# 1 行目: 英語コード、 2 行目: 日本語名
header = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=1).columns.tolist()
jp_label = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=2).iloc[0].tolist()

# 最初の 3 列は year/code/pref なのでスキップ
metric_master = pd.DataFrame({'metric_code': header[3:], 'metric_jp': jp_label[3:]})
print(f"metric_master: {len(metric_master)} 行")
print(metric_master.head(8))
print()
print(f"3NF 後の合計セル: {47*2 + len(metric_master)*2 + 61476*4:,} = pref + metric + fact")
print(f"元 wide 形式   : {564 * 112:,} セル")

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

metric_master: 109 行 metric_code metric_jp 0 A1101 総人口 1 A110101 総人口(男) 2 A110102 総人口(女) 3 A1102 日本人人口 4 A110201 日本人人口(男) 5 A110202 日本人人口(女) 6 A1301 15歳未満人口 7 A130101 15歳未満人口(男) 3NF 後の合計セル: 246,216 = pref + metric + fact 元 wide 形式 : 63,168 セル

💬 結果の読み方:metric_master 109 行が分離され、 metric_jp が一元管理可能に。 セル数は long 形式化で増えるが、 列指向圧縮(Parquet)と組み合わせれば実ストレージは縮む。 BI/ML での「指標切替」が SQL の WHERE metric_code = 'A1101' で済む利便性も得られる。

📐 セル数だけ見ると long は不利だが…

long 形式は「同じ値が縦に並ぶ → 圧縮率高い」「列追加に強い(新 metric_code 追加で済む)」という利点があります。 wide vs long は単純な比較ではなく、 ストレージ・クエリ・スキーマ進化のトレードオフです。

⚠️ 過剰正規化の警告

⚠️ よくある落とし穴

❌ JOIN の N 対 N で行が爆発
主キー以外で結合すると、 一方に重複行があるだけで結果が掛け算に。 SSDSE-B-2026 を年度マスタと結合する際、 同一都道府県が複数行ある誤データを見落とすと、 集計値が 2 倍に水増しされます。 EXPLAIN ANALYZE + 結合前 COUNT(DISTINCT key) 確認が安全策。
❌ トランザクション境界の誤設定
複数 INSERT/UPDATE を 1 トランザクションでまとめ忘れると、 途中で例外が出た場合に中途半端な状態でデータが残る。 一方、 長すぎるトランザクションは他クライアントを待たせ、 デッドロックの原因に。 「論理的に分かちがたい一連の変更」だけを境界に。
❌ 文字コード / 照合順序の不一致
日本語 CSV を utf-8 で取り込んだのに、 DB のカラムが latin1SHIFT_JIS 設定だと文字化け。 PostgreSQL では SHOW SERVER_ENCODING; で UTF-8 を確認、 MySQL は utf8mb4 (4 バイト UTF-8) を選び collation = utf8mb4_ja_0900_as_cs_ks で照合順序を日本語に最適化。

⚠️ 落とし穴

❌ 1. インデックス過多
書き込み遅延・容量肥大。 EXPLAIN で実用性確認。
❌ 2. N+1 クエリ
親取得後に子ごとに別クエリ。 JOIN や IN で1回にまとめる。
❌ 3. 過度な正規化
JOIN だらけで遅い。 性能要件があれば適度な逆正規化。
❌ 4. SQL インジェクション
文字列連結で SQL を組まない。 プレースホルダ必須。
❌ 5. デッドロック
互いのロック待ち。 取得順序統一、短いトランザクション。
❌ 6. NULL の扱い
= NULL は常に偽。 IS NULL を使う。 COUNT(col) は NULL を除外。
❌ 7. バックアップ未検証
取っても復元テストしないと意味ない。 定期演習を。

🗺 あなたが今見ているもの — RDB の全体地図 (冒頭文脈の再掲)

RDB 詳細 前提: 関係モデル / SQL 並列: NoSQL / DWH 発展: ACID / トランザクション 応用: PostgreSQL / MySQL 対比: CSV / pandas 統合: 正規化 / JOIN

この補講は、 ここまでの「正規化・ACID・JOIN・SQL」の各論を 1 枚絵で俯瞰し直す ためのまとめです。 あなたが今見ているものは「RDB という統計データ・業務データを格納する標準箱の中身」であり、 ジャストインタイム型データサイエンス教育では、 SSDSE-B-2026 (47 都道府県 × 109 指標) を例題に「読みやすい表=統計の前提となる正規化された表」をまず手に入れることを最優先で扱います。 冒頭文脈をもう一度押さえます: RDB は「分析の信頼性 (正しさ・再現性)」を保証する装置であり、 ノートブックで pandas を叩く前段にある「データの基盤」です。

📊 図 1: RDB 設計フロー (概念→論理→物理)

下図は SSDSE-B-2026.csv を RDB に格納する際の 3 段階設計を模式化したもの (実際の散布図で代用)。 概念設計 (エンティティ抽出) → 論理設計 (正規化と関係定義) → 物理設計 (インデックス・パーティション) の順に進める。

RDB 設計フローのイメージ (散布図で概念可視化)

→ 散布図の各点を「テーブル候補」と読み替えると、 まとまりごとに正規化単位 (prefecture / population_year / industry_year など) が見えてくる。 RDB 設計はまさにこの「点を線でくくる」作業に似ている。

📊 図 2: 都道府県人口のヒストグラム — テーブル化前の確認

RDB に格納する前に必ず「分布を見て CHECK 制約と型を決める」。 下のヒストグラムは SSDSE-B-2026 (2023 年・47 都道府県) の総人口分布。 右に大きく外れる東京を見て、 population BIGINT CHECK (population >= 0) 程度の制約に落ち着くことが分かる。

都道府県人口分布のヒストグラム

→ 最大値は 14,047,594 (東京) で、 INT (2,147,483,647 上限) で十分だが、 将来的な世界都市データ統合を見越して BIGINT を採用しておくと安全。 「分布を見て型を決める」は RDB 設計の鉄則。

📊 図 3: 多地域比較ボックスプロット — GROUP BY の前提分布

SQL の GROUP BY region で集計する前に、 ボックスプロットで 地域内ばらつき を確認する。 内的ばらつきが大きいほど平均値の意味は薄れ、 中央値や四分位を出すべきと判断できる。

地域別人口のボックスプロット

→ ボックスが縦に長い地域 (関東) は中の都県差が大きい。 RDB では PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY population) で中央値を取れる (PostgreSQL/Oracle)。 MySQL では ROW_NUMBER() で擬似的に実装する。

🔬 補講: RDB と pandas — どちらでやるべきか

SSDSE のような 約 560 行・約 110 列クラスのデータでは、 多くの操作は pandas でも RDB でも書ける。 ではどちらを選ぶべきか。 結論を先に書くと「データの正本 (Source of Truth) は RDB、 分析の作業場は pandas」がほぼ全ての現場で採用されているパターンである。 理由は以下の通り。

観点RDBpandas (CSV / Parquet)
同時編集◎ (トランザクション)× (ファイル上書きで競合)
制約検証◎ (CHECK / FK / UNIQUE)△ (assert を都度書く)
履歴管理◎ (audit_log + INSERT トリガ)△ (git でファイル全体差分)
探索・可視化△ (SQL は集計向き、 可視化は別ツール)◎ (matplotlib / seaborn 即時)
機械学習前処理△ (ピボット・型変換が冗長)◎ (scikit-learn と直結)
大規模 (10 万行〜)◎ (インデックスで O(log n))○ (Polars / DuckDB なら高速)

→ 実務では RDB → SELECT で部分抽出 → pandas で分析 → 結果テーブルとして RDB に書き戻し のパイプラインが標準。 SSDSE のような公開データを扱う研究室なら、 SQLite を 1 つ立てて全員で参照するだけでも、 「ファイル名が違って分析がズレた」 系の事故を激減させられる。

🐍 Python 実装: SSDSE-B-2026 を SQLite に投入して都道府県別集計

このコードでやること: SSDSE-B-2026.csv を読み込み、 SQLite に prefecture + population_year の 2 テーブルとして正規化投入、 SQL で「2023 年人口上位 5 県」を集計する。 RDB を「データ分析の正本」として扱う標準パターン。

📥 入力データ (SSDSE-B-2026 抜粋 — 投入前の df.head()):

SSDSE-B-2026 Code Prefecture A1101 ... 2023 R01000 北海道 5092000 ... 2022 R01000 北海道 5140000 ... 2021 R01000 北海道 5183000 ... (全 564 行 × 112 列)
 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
import sqlite3
import pandas as pd

# 1. CSV を読み込み (1 行目=英字コード, 2 行目=日本語名なので skiprows=[1])
d = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])

# 2. 2023 年のみ抽出 (年度列の名前は 'SSDSE-B-2026')
d23 = d[d['SSDSE-B-2026'] == 2023]

# 3. SQLite 接続(同じページの他の例がファイル DB を掴んでいることがあるのでメモリ上に作る)
conn = sqlite3.connect(':memory:')

# 4. 正規化: prefecture (県マスタ) と population_year (年別人口) に分割投入
pref = d23[['Code', 'Prefecture']].drop_duplicates()
pref.columns = ['pref_code', 'name']
pref.to_sql('prefecture', conn, if_exists='replace', index=False)

pop = d23[['Code', 'A1101']].copy()
pop.columns = ['pref_code', 'population']
pop['year'] = 2023
pop.to_sql('population_year', conn, if_exists='replace', index=False)

# 5. SQL で集計: 2023 年人口上位 5 県
sql = """
SELECT p.name, py.population
FROM   prefecture p
JOIN   population_year py ON p.pref_code = py.pref_code
WHERE  py.year = 2023
ORDER  BY py.population DESC
LIMIT  5
"""
print(pd.read_sql(sql, conn))

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

name population 0 東京都 14086000 1 神奈川県 9229000 2 大阪府 8763000 3 愛知県 7477000 4 埼玉県 7331000

💬 結果の読み方: 「県マスタ」と「年別人口」に分けたおかげで、 将来 2024 年のデータが入っても population_year に 47 行追加するだけで済む。 もし 1 つのテーブルに pop_2023, pop_2024, pop_2025 と横に並べていたら、 毎年 ALTER TABLE ADD COLUMN が必要になり、 BI ツールのクエリも全部書き直しになる。 これが「縦持ち (long format) で正規化する」 ことの実利。

📚 RDB 設計でよくある 5 つの失敗パターンと修正

#失敗パターン症状修正
11 セルに複数値 (CSV in cell)tags = "東京,神奈川,千葉" のような格納多対多テーブルに分割 (1NF 違反)
2主キーに業務的意味を持たせる都道府県コードを主キーにしたが市町村合併で変動サロゲートキー (BIGINT id) を別途用意
3NULL の乱用欠損か未入力か未確認かが区別できない状態カラム (status) を分離、 値の意味を定義
4JOIN を恐れて 1 テーブル肥大化50 列を超え、 SELECT * のコストが膨大3NF まで正規化、 SELECT は必要列だけ列挙
5外部キー無し子テーブルに親に無い ID が混入FOREIGN KEY 宣言 + ON DELETE RESTRICT

→ SSDSE データを RDB 化する研究室で最も多いのは パターン 1 (1 セルに複数値) と パターン 4 (1 テーブル肥大化)。 ジャストインタイム教育では、 「最初は CSV 1 枚で OK、 ただし 200 行を超えたら必ず正規化」 を経験則として教えると衝突が少ない。

🌐 関連用語: RDB を取り巻く周辺技術

領域用語RDB との関係
クエリ言語SQL / DDLRDB を操作する標準言語
結合テーブルの結合正規化されたテーブルを統合して読む
ID 一貫性主キー / 外部キーRDB の信頼性の核
派生NoSQL / DWH用途別の補完技術
処理基盤分散処理RDB を超える規模での選択肢

📝 理解度チェック (10 問)

この補講で扱った内容について、 自分で答えてみよう。 即答できない問題があれば該当セクションに戻って読み直す。

  1. 第 1 正規形 (1NF) 違反の典型例を SSDSE データで 1 つ挙げよ。 → ヒント: 1 セル複数値
  2. 第 3 正規形 (3NF) で削除される依存関係は何か。 → ヒント: 推移的関数従属
  3. 外部キー (FK) を宣言する 3 つのメリットは何か。 → ヒント: 整合性 / カスケード / 文書性
  4. ACID の D (Durability) が破られる典型シナリオは何か。 → ヒント: COMMIT 直後の停電
  5. READ COMMITTED と REPEATABLE READ の違いを SSDSE 文脈で説明せよ。 → ヒント: 同一トランザクション内の再読み込み
  6. INNER JOIN と LEFT JOIN の結果行数差はどのような場合に発生するか。 → ヒント: 子側のマッチ無し
  7. SSDSE-B-2026 を SQLite に投入するとき、 テーブルを 1 つにせず分けるべき理由を 2 つ挙げよ。 → ヒント: 重複削減 / 拡張性
  8. インデックスを張りすぎると遅くなる操作は何か。 → ヒント: INSERT / UPDATE
  9. NULL の意味を 3 種類区別せよ。 → ヒント: 未入力 / 欠損 / 該当無し
  10. SSDSE で 500 行のデータを扱うとき、 RDB と pandas のどちらを「正本」とすべきか、 理由とともに答えよ。 → ヒント: 同時編集 / 制約 / 履歴

→ 解答例は明示しないが、 すべて本文・補講・上の表のどこかに根拠がある。 自分で答えを書き出してから本文に戻ると、 RDB の設計判断が「定型作業」 ではなく「分析の信頼性を担保する明示的設計」 として腑に落ちる。

🧪 補講: SSDSE-B-2026 を 3NF で正規化する具体手順

SSDSE-B-2026 を素直に CSV のまま投入すると 1 行 = 1 県、 1 列 = 1 指標の横持ち (109 指標列) になる。 これは BI の一枚絵には便利だが、 RDB の世界では 「指標が増える度に ALTER TABLE が必要」という致命的な保守コストを生む。 ここでは縦持ち (long format) の 3NF テーブル設計を、 SSDSE 4 指標を例に段階的に示す。

段階テーブル名主な列解消した依存
0NF (CSV のまま)ssdse_rawpref_code, name, A1101, A1301, A1303, A4101, ...(なし)
1NFssdse_longpref_code, name, indicator_code, year, value列の繰り返しを行に展開
2NFprefecture / indicator_valueprefecture(pref_code,name) + indicator_value(pref_code, indicator_code, year, value)部分関数従属 (name は pref_code だけに依存)
3NF+ indicator_masterindicator_master(indicator_code, label, unit, source)推移的関数従属 (label/unit は indicator_code に依存)

→ 3NF に到達すると、 「指標を 1 つ追加」 は indicator_master に 1 行追加 + indicator_value に 47 行追加だけで済む。 0NF (CSV) では新しい列を追加してから全 BI クエリを書き直す必要があった。 SSDSE が毎年更新される現実を考えれば、 縦持ち 3NF が圧倒的に低コスト。

🐍 0NF → 3NF を Python で機械的に変換

このコードでやること: SSDSE-B-2026 (横持ち 109 指標列) を pandas で melt し、 3 つの正規化テーブルに分けて SQLite に投入する。 横持ち → 縦持ち変換は pd.melt 一発。

📥 入力データ (0NF, 47 行 × 109 指標列の先頭抜粋):

Code Prefecture A1101 A1301 A4101 ... R01000 北海道 5092000 514000 24430 ... R13000 東京都 14086000 1513000 86348 ... (2023 年抽出後 全 47 行)
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
import pandas as pd, sqlite3

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])

# 2023 年のみ抽出 → 1 県 1 行の 0NF 横持ちにする
d23 = df[df['SSDSE-B-2026'] == 2023].drop(columns=['SSDSE-B-2026'])

# 1NF: 横持ち → 縦持ち (Code/Prefecture 以外の指標列を melt)
long = d23.melt(id_vars=['Code', 'Prefecture'],
                var_name='indicator_code',
                value_name='value')
long['year'] = 2023
long = long.rename(columns={'Code': 'pref_code', 'Prefecture': 'name'})

# 2NF: prefecture (name の部分従属を分離)
prefecture = long[['pref_code', 'name']].drop_duplicates()
indicator_value = long[['pref_code', 'indicator_code', 'year', 'value']]

# 3NF: indicator_master (label/unit の推移従属を分離 — ここでは仮値で生成)
indicator_master = pd.DataFrame({
    'indicator_code': long['indicator_code'].unique(),
    'label': '(SSDSE 指標)',
    'unit': '(単位)',
})

conn = sqlite3.connect('ssdse_3nf.db')
prefecture.to_sql('prefecture', conn, if_exists='replace', index=False)
indicator_value.to_sql('indicator_value', conn, if_exists='replace', index=False)
indicator_master.to_sql('indicator_master', conn, if_exists='replace', index=False)

print('prefecture rows:', len(prefecture))
print('indicator_value rows:', len(indicator_value))
print('indicator_master rows:', len(indicator_master))

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

prefecture rows: 47 indicator_value rows: 5123 indicator_master rows: 109

💬 結果の読み方: 47 県 × 109 指標 = 5,123 行が縦持ちで生まれる。 一見「行数が爆発した」 ようだが、 これが 正規化の正常な姿。 indicator_value にインデックス (indicator_code, year) を張れば検索は O(log n) で済むので、 BI 用途でもパフォーマンス劣化は起きない。 むしろ「指標追加が ALTER TABLE 不要」「複数年データの統合が UNION 不要」 という保守利得が圧倒的。

🔚 まとめ: RDB を「使える道具」にするための最後の 3 行

🔗 隣接手法への橋渡し

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

SSDSE-B-2026 を SQLite に正規化投入し、 マスタ + 観測の JOIN + 集計 + ビュー定義まで通すと、 CSV 直読みでは見えなかった「整合性 + 速度 + 共有」が得られる。

🌳 手法選択フロー

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

  1. スキーマは固定か可変か? 固定 → RDB (PostgreSQL/MySQL)、 可変 → NoSQL (MongoDB)
  2. トランザクションは必要か? Yes (金融・在庫) → RDB の ACID、 No (ログ集計) → DWH/ストリーム
  3. スケールは縦/横どちら? 縦 (大きな単一サーバ) → RDB、 横 (分散) → NewSQL / NoSQL

SSDSE-B-2026 を 47 行 × 112 列で扱う限りは SQLite で十分。 県別・年別の集計テーブルを SQL で書けば、 pandas より高速かつ再現性が高い。