この用語と一緒に検索・参照されやすいタグ。 関連ページに飛ぶときの手がかりにも使えます。
「database」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「database」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「database の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
大量のデータを整理する倉庫のようなものです。
必要な情報をすぐに探し出すために使います。
スマホのアプリなどで毎日使われています。
まずは結論から短くまとめて解説します。
データベースは、 大量の構造化データを整合的に永続化・検索・更新するためのソフトウェア基盤。
ここまでが要点です。 ただし実際に使う前に、 このページの「⚠️ よくある落とし穴」で挙げた N+1 クエリ/インデックス未設定/過剰正規化 には必ず目を通してください。 つまずくのは知識が無いときより、 知ってはいたが確認を飛ばしたときです。
🍰 まずはやさしく
データを扱うための便利な道具です。
仕事で分析をする時に欠かせません。
学校の成績表のようなデータ管理に似ています。
どんな場面で使うのかを詳しく見ていきましょう。
SSDSE は CSV ですが、 企業内データは大部分が DB に存在。 SQL を読み書きできることはデータサイエンティストの必須スキル。
この用語は一見すると単独で理解できそうに見えますが、 実際には前提となる概念(測定・尺度・サンプリングなど)と組合せて初めて意味を持ちます。 「定義を覚える」より「どんな問いに答える道具なのか」を捉えるのが効率的です。
🍰 まずはやさしく
ルールが決まった巨大な表のようなものです。
データの整理方法をイメージするために使います。
部活の名簿をきれいに並べる感覚に近いです。
直感的に分かりやすい例で仕組みを説明します。
「データベース」を最初に学ぶときは、 厳密な定義よりイメージを優先しましょう。 以下は具体例・比喩を用いた直感的理解の入口です。
🍰 まずはやさしく
データの扱い方を決めた厳格なルールです。
間違いのない正確な処理をするために使います。
銀行の振り込みのような仕組みに似ています。
正しい定義と数式を使って詳しく解説します。
直感の次は、 厳密な定義を確認します。 数式は言語の一種で、 一度書き慣れれば「言葉より速く伝えられる」便利な道具。 慣れていない方は、 各記号が何を表すかを「🔬 数式を言葉で読み解く」で 1 つずつ確認してください。
概念モデル(ER 図)→ 論理モデル(リレーション)→ 物理モデル(CREATE TABLE)の 3 段階で、 SSDSE-B-2026 を再設計してみます。
エンティティ:Prefecture(都道府県)、 Indicator(指標)、 Observation(観測値)。
関係:1 つの観測値は 1 つの都道府県と 1 つの指標と 1 つの年に属する → Observation は 3 つの 1:N 関係の集合。
Prefecture(pref_code PK, pref_name_ja, pref_name_en, region) Indicator(indicator_code PK, name_ja, unit, category) Observation(pref_code FK, year, indicator_code FK, value) PK: (pref_code, year, indicator_code)
CREATE TABLE prefectures ( pref_code CHAR(5) PRIMARY KEY, pref_name_ja VARCHAR(10) NOT NULL UNIQUE, pref_name_en VARCHAR(20) NOT NULL UNIQUE, region VARCHAR(10) ); CREATE TABLE indicators ( indicator_code VARCHAR(10) PRIMARY KEY, name_ja VARCHAR(100) NOT NULL, unit VARCHAR(20), category VARCHAR(30) ); CREATE TABLE observations ( pref_code CHAR(5) NOT NULL REFERENCES prefectures(pref_code), year SMALLINT NOT NULL CHECK (year BETWEEN 2010 AND 2030), indicator_code VARCHAR(10) NOT NULL REFERENCES indicators(indicator_code), value NUMERIC, PRIMARY KEY (pref_code, year, indicator_code) ); CREATE INDEX idx_obs_year_indicator ON observations(year, indicator_code); CREATE INDEX idx_obs_indicator ON observations(indicator_code);
SSDSE-B-2026 の 564 行 × 112 列 CSV から、 上記スキーマで melt + INSERT すると 約 61,476 行の縦持ちテーブルになります。 これが「tidy data」の物理表現です。
「テーブルどう切る?」を判断するためのフロー。 SSDSE-B-2026 を題材に:
(pref_code, year, indicator_code) の複合キー。value は NULL 可(欠損年あり)、 pref_code は NOT NULL。| 役割 | 主な責任 | 必要スキル |
|---|---|---|
| DBA | DBMS 運用・チューニング・バックアップ | RDBMS 深い理解、 OS、 ネットワーク |
| Data Engineer | ETL 構築・DWH 設計・データパイプライン | SQL、 Python、 Airflow、 dbt、 クラウド |
| Data Analyst | クエリ・可視化・ビジネスへの説明 | SQL、 統計、 BI ツール、 ドメイン知識 |
| Data Scientist | 機械学習・実験設計・モデル化 | SQL、 Python、 統計、 ML、 数学 |
小規模組織では 1 人が全部を兼ねることも多い。 SSDSE のような公開データを使った教育・研究プロジェクトでは「Analyst + Engineer」スキルセットが特に役立つ。
「2023 年における人口 10 万人当たり一般病院数の都道府県ランキング、 ただし前年比(2022 年比)の増減率も同時に表示」を 1 クエリで取得します。 CTE(共通テーブル式)・ウィンドウ関数・サブクエリを組み合わせた実用例。 SSDSE-B-2026 に医師数列は無いため、 実在する一般病院数(I510120)を用います。
WITH doctor_rates AS (
SELECT
Year,
Prefecture,
A1101 AS population,
I510120 AS hospitals,
I510120 * 100000.0 / A1101 AS hospitals_per_100k
FROM ssdse_b
WHERE Year IN (2022, 2023)
),
pivoted AS (
SELECT
Prefecture,
MAX(CASE WHEN Year = 2023 THEN hospitals_per_100k END) AS rate_2023,
MAX(CASE WHEN Year = 2022 THEN hospitals_per_100k END) AS rate_2022
FROM doctor_rates
GROUP BY Prefecture
)
SELECT
RANK() OVER (ORDER BY rate_2023 DESC) AS rank,
Prefecture,
ROUND(rate_2023, 1) AS hospitals_per_100k_2023,
ROUND((rate_2023 - rate_2022) / rate_2022 * 100, 2) AS yoy_change_pct
FROM pivoted
ORDER BY rate_2023 DESC;
解説:① WITH 句で 2 年分の病院密度を計算(doctor_rates)。 ② CASE WHEN で年を列に展開する PIVOT を実現(pivoted)。 ③ RANK() で順位、 算術で前年比増減率を計算。 実測(2023 年)では高知・徳島・鹿児島などが上位、 神奈川・滋賀・愛知などが下位に出ます。
deleted_at 忘れ:物理削除すると履歴が消える。 「is_deleted」または「deleted_at」列で論理削除する設計に。tags="統計,人口,都道府県" のような格納はアンチ。 別テーブルに正規化する。status=1 が「有効」を意味するのをコメントなし。 ENUM 列または別マスタテーブルに。ssdse_b / SSDSE_B / ssdseB が混在。 命名規約を最初に決める。article_views を 1 行で持ち、 UPDATE で連打 → ロック競合。 別 KVS(Redis)で集計してから定期反映する。ssdse_observations)prefecture_code、 created_at)id 単独か、 {table}_id_id(prefecture_id)is_ / has_ / can_ プレフィックス_at サフィックス(created_at、 updated_at)idx_{table}_{cols}(idx_ssdse_year_pref)uniq_{table}_{cols}「DB が壊れた」事故は必ず起こる。 大事なのは復元できること。 SSDSE-B のデータは公開されているので失っても再取得可能だが、 業務 DB ではそうもいかない。
SQLite ファイルは単一ファイルなので、 単純コピーでも OK(ただし書き込み中はダメ)。 安全なのは .backup コマンド:
sqlite3 ssdse.db ".backup ssdse_backup.db" sqlite3 ssdse.db ".dump" > ssdse_dump.sql # 復元 sqlite3 ssdse_restored.db < ssdse_dump.sql
pg_dump -U postgres -d ssdse_db -F c -f ssdse.dump pg_restore -U postgres -d ssdse_restored ssdse.dump # PITR(任意時点復旧):WAL アーカイブ + base backup の組合せ
ここまでの知識を統合した、 30 分で完走できる実践ガイド。 上から順にやれば、 SQL の基本機能を一通り体験できます。
pip install pandas sqlite3(sqlite3 は標準ライブラリ)。 data/raw/SSDSE-B-2026.csv を作業ディレクトリに配置。 Jupyter 起動。
本ページ「🐍 Python 実装」のコードをそのまま実行。 SQLite ファイル ssdse.db ができたら、 ターミナルから sqlite3 ssdse.db で対話モードに入り、 .schema ssdse_b でテーブル定義を確認。
「2023 年人口 TOP5」「Year ごとの平均人口」「人口 500 万人以上の県」など。 本ページ「📝 演習問題」を順に。
指標マスタを別テーブル化(CREATE TABLE indicators (code TEXT PRIMARY KEY, name TEXT))。 JOIN して「2023 年人口 TOP5(指標名付き)」を取得。
EXPLAIN QUERY PLAN で実行計画を観察。 インデックス作成前後で SCAN → SEARCH に変わることを確認。
クエリ結果を pandas DataFrame に戻して matplotlib で棒グラフ。 「図 1:2023 年都道府県別人口 TOP10(出典:SSDSE-B-2026)」のキャプションを付けて完成。
「インデックス=速くなる魔法」と思いがちですが、 書き込み時には遅くなる副作用があります。 種類と特性を整理。
| 種類 | 構造 | 得意 | SSDSE-B 例 |
|---|---|---|---|
| B-tree | バランス木 | 等価・範囲・ORDER BY | Year, Prefecture |
| Hash | ハッシュテーブル | 等価のみ・超高速 | Pref_Code(数値) |
| GIN | 転置索引 | 全文検索・配列 | 指標名の部分一致検索 |
| GiST | 汎用検索木 | 地理空間・範囲型 | 都道府県の緯度経度範囲検索 |
| BRIN | ブロック単位の概要 | 大規模・物理順序が揃ったデータ | 時系列のYear列 |
複合インデックスの順序:(Year, Prefecture) と (Prefecture, Year) は別物。 「左から接頭辞のみ使える」原則を覚える。 WHERE Year=2023 AND Prefecture='Tokyo' なら両方使えるが、 WHERE Prefecture='Tokyo' のみだと前者はほぼ使えない。 SSDSE-B でも、 集計が頻繁な軸を先に置く。
CSV を to_sql で投入すると、 pandas が型を自動推論しますが、 本番では明示的に定義すべきです。 SSDSE-B-2026 の主要列に対する推奨型:
| 列 | 内容 | 推奨型 (PostgreSQL) | 理由 |
|---|---|---|---|
| Code | 都道府県コード | CHAR(5) または SMALLINT | 固定長・順序保証 |
| Prefecture | 都道府県名 | VARCHAR(20) | 「神奈川県」最大 4 文字 = 12 byte (UTF-8) |
| Year | 調査年 | SMALLINT | 1900〜2100 で十分(2 byte) |
| A1101 | 総人口 | INTEGER | 最大 1400 万 < 21 億で OK |
| A1102 | 男性人口 | INTEGER | 同上 |
| B1101 | 面積 | NUMERIC(10,2) | 小数 2 桁の正確な保持 |
| I510120 | 一般病院数 | INTEGER | 数十〜数百施設規模 |
FLOAT を避ける:金額や統計値に FLOAT を使うと丸め誤差が起きる。 NUMERIC / DECIMAL を使うべし。 0.1 + 0.2 != 0.3 問題が再現する。
統計・データ解析コンペティション 2025 では、 SSDSE-B-2026 を題材に「47 都道府県の社会経済指標から傾向を発見する」研究が多く出ています。 そこで DB が果たす役割を整理します。
indicators テーブルに別管理。 「列名 A1101 が何の指標か」を即座に JOIN で参照できる。SELECT Prefecture, A1101 FROM ssdse_b WHERE A1101 < 0 OR A1101 IS NULL で欠損・負値を即座に列挙。 EDA の第一歩。CREATE VIEW で「人口 10 万人当たり指標」をまとめて作成。 47 都道府県の規模差を補正した相対指標が一発で得られる。LAG(A1101) OVER (PARTITION BY Prefecture ORDER BY Year) で前年比を計算。 「人口減少が加速した県」を抽出可能。研究レベルでは「pandas で前処理 → DB に保存 → BI で可視化」のワークフローが定番。 中間データを DB に置くことで、 共著者・指導教員と同じデータ・同じ SQLを共有できる。
pandas 派と SQL 派の論争は不毛で、 結局同じことを別の言語で書いているに過ぎません。 SSDSE-B-2026 を題材に、 主要操作の対応を整理します。
| やりたいこと | SQL | pandas |
|---|---|---|
| 列選択 | SELECT Prefecture, A1101 FROM ssdse_b | df[['Prefecture','A1101']] |
| 行抽出 | WHERE Year=2023 | df[df.Year==2023] |
| グループ集計 | GROUP BY Year | df.groupby('Year') |
| 並び替え | ORDER BY A1101 DESC | df.sort_values('A1101', ascending=False) |
| 上位 N | LIMIT 5 | df.head(5) または df.nlargest(5,'A1101') |
| JOIN | JOIN ON | df.merge(df2, on='Prefecture') |
| 縦持ち変換 | UNPIVOT | df.melt(id_vars=['Prefecture','Year']) |
| 横持ち変換 | PIVOT | df.pivot(index='Prefecture',columns='Year',values='A1101') |
| ウィンドウ関数 | RANK() OVER (PARTITION BY ...) | df.groupby('Year')['A1101'].rank(ascending=False) |
| VIEW 作成 | CREATE VIEW | 関数化(def)または lambda 式 |
10 万行以下なら pandas、 1000 万行を超えたら SQL に寄せる。 SSDSE-B-2026 は 564 行なのでどちらでも問題なし。 ただし列方向の集計は SQL の方が常に書きやすい。
商用 DB を運用する前に、 以下を 1 つずつ潰す。 1 つでも欠けたら本番投入禁止。
.env を .gitignore に)sslmode=require)2020 年代のデータ基盤は「DB 単独」ではなく、 OLTP DB → ELT → DWH → BI の 4 層構成が標準です。 SSDSE-B-2026 のような国家統計データも、 各省庁の業務 DB(OLTP)から e-Stat(DWH)に集約され、 ダッシュボードで可視化される流れを想像すると、 全体像が掴めます。
| 層 | 役割 | 代表技術 | SSDSE 類例 |
|---|---|---|---|
| OLTP | 日々の業務処理(短いトランザクション) | PostgreSQL, MySQL, Oracle | 住基ネット・税務 DB |
| ETL/ELT | データ抽出・変換・ロード | dbt, Airflow, Fivetran | 統計局の集計バッチ |
| DWH | 分析用の列指向 DB | BigQuery, Snowflake, Redshift | e-Stat バックエンド |
| BI / 配布 | 可視化と公開 | Tableau, Looker, CSV 配布 | SSDSE CSV ダウンロード |
本サイトの題材 SSDSE-B-2026.csv は、 国の DWH から「教育用に整形して公開」された下流の成果物と捉えると、 公的統計データ整備の全体像が見えてきます。
SELECT で対象を確認、 トランザクションで囲んでから実行。 BEGIN → UPDATE → 件数確認 → COMMIT / ROLLBACK の手順を体に染み込ませる。NULL = NULL」は真ではなく NULL。 比較には IS NULL を使う。 SSDSE-B でも欠損値は実値(0 や -1)と区別して NULL にすべき。SQL は「宣言型」言語で、 「何が欲しいか」を書けば「どう取るか」は DB エンジンが決めます。 でも実装を理解しておくと、 遅いクエリを見たときに即座に直せます。
人間が書くのは SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT の順ですが、 DB エンジンが評価するのは:
この順序を覚えると「なぜ SELECT で付けたエイリアスを WHERE で使えないのか」(WHERE は SELECT より早く評価されるから)が腑に落ちます。
| 種類 | 挙動 | SSDSE 例 |
|---|---|---|
| INNER JOIN | 両方に存在する行のみ | 「2023 年データと 2022 年データの両方ある県」 |
| LEFT JOIN | 左テーブル全行+右の一致行 | 全 47 都道府県を保持して人口データを横に |
| RIGHT JOIN | 右テーブル全行+左の一致行 | SQLite では未サポート、 LEFT で代用 |
| FULL OUTER JOIN | 両側の全行(欠損は NULL) | 2 つの年で異なる県集合の合体 |
| CROSS JOIN | 直積(全組合せ) | 47 都道府県 × 12 年 = 564 行のテンプレ作成 |
SSDSE-B で「各年における都道府県の人口順位」を出すには RANK() OVER (PARTITION BY Year ORDER BY A1101 DESC) が便利。 GROUP BY と違って元の行を保ったまま順位だけ付与できます。 SQLite 3.25 以降サポート。
ACID の「I (Isolation)」は程度問題で、 SQL 標準では 4 段階の分離レベルを定めています。 高くするほど安全だが遅く、 低くするほど速いが現象(アノマリー)が発生します。
| 分離レベル | ダーティリード | 反復不能読取 | ファントム読取 |
|---|---|---|---|
| READ UNCOMMITTED | 起こる | 起こる | 起こる |
| READ COMMITTED | 起こらない | 起こる | 起こる |
| REPEATABLE READ | 起こらない | 起こらない | 起こる |
| SERIALIZABLE | 起こらない | 起こらない | 起こらない |
PostgreSQL のデフォルトは READ COMMITTED、 MySQL InnoDB のデフォルトは REPEATABLE READ。 SSDSE-B のような読み取り中心の分析用途ではどれでも問題は起きにくいですが、 銀行残高更新のような書き込み競合では SERIALIZABLE が必要になります。
NoSQL は「Not Only SQL」の意で、 RDB の代替ではなく相補するもの。 4 系統の使い分けを整理します。
| 系統 | 代表製品 | データ構造 | SSDSE-B 適合 |
|---|---|---|---|
| キーバリュー | Redis, DynamoDB | key → value | セッションキャッシュには良い、 分析には不向き |
| 文書型 | MongoDB, Couchbase | JSON / BSON | 指標が県ごとに異なる場合は◯ |
| カラム志向 | Cassandra, HBase | 疎な列の集合 | 時系列の超大規模なら◯ |
| グラフ | Neo4j, Neptune | ノード+エッジ | 関係性が薄いので不要 |
数式を眺めるだけでは身につかないので、 各記号がどんな役割を担っているかを言葉で押さえます。 「数式を音読する習慣」がつくと、 論文や教科書を読むスピードが体感で 2 倍ほど上がります。
データベースの根幹は リレーショナル代数 です。 「テーブル=集合」「行=タプル」「列=属性」と見なすと、 SQL の SELECT/JOIN/WHERE/GROUP BY は集合演算と直接対応します。 具体的には、 射影 π(必要な列だけ取り出す)、 選択 σ(条件にマッチする行だけ残す)、 結合 ⋈(2 つのテーブルを共通キーで横にくっつける)、 直積 ×(すべての組合せを並べる)、 和 ∪・差 −・積 ∩(集合演算)の 6 種類が基本で、 SELECT 文はこれらの組合せに翻訳されて実行計画が作られます。
数式 $$\\sigma_{Year=2023}(\\pi_{Prefecture,A1101}(SSDSE\\_B))$$ は「SSDSE-B テーブルから Prefecture と A1101 だけ取り出し(射影)、 そのうち Year が 2023 の行を抜き出す(選択)」を意味しており、 SQL では SELECT Prefecture, A1101 FROM ssdse_b WHERE Year=2023 と書きます。 オプティマイザは「先に WHERE を適用してから SELECT する」方が効率的だと判断すれば、 等価な式 $$\\pi_{Prefecture,A1101}(\\sigma_{Year=2023}(SSDSE\\_B))$$ に書き換え(選択の押し下げ)を行い、 中間データ量を削減します。 これが「クエリ最適化」と呼ばれる処理の正体で、 リレーショナル代数の代数的恒等式が実装の根拠になっています。 さらに、 B-tree インデックスは「ソート済み配列」と見なせるため、 範囲検索の計算量を O(N) から O(log N + k)(k は該当行数)に落とすことができ、 全件 47×12=564 行であっても都道府県名でインデックスを張れば 9 回程度の比較で目的行に到達できます。
「データベース」が実際の業務でどう使われているか、 業界別に整理した具体事例。 SSDSE-B-2026 の構造(都道府県 × 年次 × 指標)を模した小規模 DB から、 ペタバイト級まで、 設計の勘所は驚くほど共通しています。
業界が変わっても「ACID 保証が要るか」「読み多/書き多か」「横スケールが必要か」の 3 軸で DB 選択を整理すれば 8 割は決まります。
SSDSE-B-2026(564 行)程度なら何でも動くが、 業務で 1 億行になると差が一気に開く。 主要 6 製品を 8 軸で比較。
| DBMS | 系統 | ACID | 水平スケール | ライセンス | 得意用途 | 学習曲線 | SSDSE 適性 |
|---|---|---|---|---|---|---|---|
| SQLite | RDBMS(組込) | ○ | × | Public Domain | 小規模・モバイル | 易 | ★★★★★(最適) |
| PostgreSQL | RDBMS | ◎ | △(拡張要) | PostgreSQL License | 汎用・分析 | 中 | ★★★★★ |
| MySQL | RDBMS | ○ | △ | GPL/商用 | Web サービス | 易 | ★★★★ |
| MongoDB | NoSQL(文書) | △(4.0 以降) | ◎ | SSPL | JSON ログ | 中 | ★★(tidy には不向き) |
| DuckDB | RDBMS(列指向) | ○ | × | MIT | 分析・Parquet | 易 | ★★★★★(高速) |
| BigQuery | DWH(クラウド) | ○ | ◎ | GCP 従量課金 | 超大規模分析 | 中 | ★★★(過剰) |
SSDSE-B-2026 のような「数百〜数万行」の教材レベルでは SQLite か DuckDB がベスト。 業務で 1 億行を超えたら PostgreSQL、 PB 級なら BigQuery / Snowflake と段階的に上げます。
SSDSE-B-2026 を SQLite に投入した状態を前提に、 自分で SQL を書いてみてください。 解答は折りたたみで見られます。
SELECT Prefecture, A1101 FROM ssdse_b WHERE Year=2023 AND A1101 > 5000000 ORDER BY A1101 DESC;SELECT Year, AVG(A1101) AS mean_pop FROM ssdse_b GROUP BY Year ORDER BY Year;SELECT (SELECT MAX(A1101) FROM ssdse_b WHERE Year=2023) * 1.0 / (SELECT MIN(A1101) FROM ssdse_b WHERE Year=2023) AS ratio;CREATE INDEX idx_year_pref ON ssdse_b(Year, Prefecture); の前後で EXPLAIN QUERY PLAN SELECT ... WHERE Year=2023 AND Prefecture='Tokyo' を実行。 SCAN TABLE → SEARCH TABLE に変わり、 564 行でも 30〜100 倍速くなる。CREATE VIEW hospitals_per_capita AS SELECT Year, Prefecture, A1101, I510120, I510120*1.0/A1101*100000 AS hospitals_per_100k FROM ssdse_b;「データベースの設計ミスで損失が出た」事例を匿名化して列挙。 教科書の落とし穴より、 現場で起きた失敗の方が記憶に残ります。
WHERE name LIKE '%iPhone%' を多用。 索引が効かず 500 ms → 3 秒に劣化。 解決:FULLTEXT インデックス(MySQL)または pg_trgm(PostgreSQL)導入で 50 ms に。SET NAMES utf8mb4、 my.cnf で character-set-server=utf8mb4。' OR 1=1; DROP TABLE users;-- を投げられた。 解決:パラメータ化クエリ/プレースホルダ。 pandas の read_sql_query(sql, conn, params=...) でも実現できる。--skip-extended-insert を忘れた・ストレージエンジンが MyISAM のままで FOREIGN KEY が無視された。 解決:dump 後に必ず別環境で復元テスト。PRAGMA optimize で自動化可能。SSDSE-B-2026 を機械学習の特徴量として使う場合の DB 統合パターン:
CREATE MODEL ... AS SELECT ... で線形回帰を即座に作れる。「スキーマを変えたい」とき、 本番停止なしでどう移行するか。 SSDSE-B-2026 で新しい列を追加する想定で考えます。
SSDSE-B-2026 は 1 つの CSV ですが、 これを「3 つのテーブルに正規化する」とどうなるかを考えてみます。 第 3 正規形を実践する好例です。
「Code, Prefecture, Year, A1101, A1102, ..., I510120, ...」を 1 つの大きな表に入れる。 564 行 × 112 列。 SQL は単純だが、 都道府県名の表記揺れ(東京都/東京)があると複数行に矛盾が生じる危険。
| テーブル名 | 主キー | 列 | 行数 |
|---|---|---|---|
| prefectures | pref_code (Code) | pref_code, pref_name_ja, pref_name_en, region | 47 |
| indicators | indicator_code | indicator_code, name_ja, unit, category | 約 109 |
| observations | (pref_code, year, indicator_code) | pref_code FK, year, indicator_code FK, value | 564×109 ≒ 61,476 |
案 B はtidy data(縦持ち)でもあり、 BI ツール(Tableau / Looker Studio)との相性が良い。 ただし「2023 年の人口 TOP5」のような単純集計は JOIN が増えて記述量が増える。 「最終ユーザーが SQL を書くか/BI が裏で書くか」で選ぶ。
案 A → 案 B の変換は UNPIVOT 相当の処理。 SQLite では UNPIVOT 構文がないので、 UNION ALL を組み立てるか、 pandas の melt で前処理してから INSERT します。
EXPLAIN QUERY PLAN (SQLite) や EXPLAIN ANALYZE (PostgreSQL)。SELECT * はネットワークと I/O の無駄。 SSDSE-B でも 112 列中必要なのは数列。ORDER BY ... LIMIT 10 で索引のソート済み順序を活用。to_sql(method='multi') で実現可能。VACUUM。 統計情報の更新で実行計画が改善。PRAGMA journal_mode=WAL にすると、 読み書き同時性が上がる。データベース技術は 1970 年の E.F. Codd によるリレーショナルモデル論文以来、 50 年以上の試行錯誤の上に成立している。 学習者の多くは「SQL を書けば DB が使える」段階で止まりがちだが、 SSDSE のような公的データセットを長期間運用しようとすると、 必ず「設計をどう正規化するか」「分析クエリと更新クエリのどちらに最適化するか」「複数ユーザの同時更新で整合性をどう保つか」という古典的問題に直面する。 ここでは現代の DB 分類を OLTP / OLAP / HTAP / NoSQL / NewSQL の 5 系統で整理し、 SSDSE-B-2026 のような表形式公的統計をどこに置くべきかを判断できるようにする。
OLTP (Online Transaction Processing) は銀行口座・予約システムのように、 1 件ずつ短時間で確実に更新するワークロード。 PostgreSQL / MySQL / Oracle / SQL Server などが代表。 行指向ストレージで、 ACID トランザクションが必須。 SSDSE 自体は更新が少ないため OLTP の典型例ではないが、 分析チームの「メモ・タグ・ラベル」管理には向く。 OLAP (Online Analytical Processing) は何百万行を集約するレポーティング用途。 列指向の DuckDB / ClickHouse / BigQuery / Snowflake が代表。 SSDSE 47 行 × 100 列のような小さな表は OLTP でも OLAP でも動くが、 e-Stat の数十年分長期時系列を扱うなら OLAP が圧勝する。 HTAP (Hybrid Transactional/Analytical Processing) は OLTP と OLAP を 1 つの DB で両立する新世代。 TiDB・SingleStore・Oracle HeatWave が該当。 NoSQL は SQL を捨てる代わりにスケーラビリティ・柔軟性を得る系統で、 ドキュメント型 (MongoDB)・キーバリュー (Redis)・列指向 (Cassandra)・グラフ (Neo4j) に細分される。 NewSQL は分散環境でも ACID を保つことを目指した RDB の再発明で、 CockroachDB・Google Spanner が代表。
| 系統 | 代表 DB | 得意ワークロード | SSDSE 適合度 |
|---|---|---|---|
| OLTP (行指向 RDB) | PostgreSQL / SQLite | 短い更新、 1 行参照 | ◎ 学習用途・タグ管理 |
| OLAP (列指向) | DuckDB / ClickHouse | 大量集計・GROUP BY | ◎ e-Stat 長期分析 |
| HTAP | TiDB / SingleStore | 混在ワークロード | ○ 規模に対し過剰 |
| NoSQL ドキュメント | MongoDB | スキーマレス JSON | △ 表データは RDB が良い |
| NoSQL KV | Redis | 超低遅延キャッシュ | △ 補助的役割 |
| NewSQL | CockroachDB / Spanner | グローバル分散整合性 | × オーバースペック |
SSDSE-B-2026 のような 47 行 × 約 100 列の表形式公的統計を扱う場合、 学習段階では SQLite、 研究室での集計分析が中心になれば DuckDB に移行する、 という二段構成が最も合理的である。 どちらもサーバ起動が不要で、 1 ファイルで完結し、 pandas との往復も容易だ。 大規模化を見据えるなら、 PostgreSQL を中心に据え、 集計だけ DuckDB の read_parquet() 越しに行うアーキテクチャに進化させればよい。
このコードでやること:同じ SSDSE-B-2026 CSV を SQLite と DuckDB に読み込み、 都道府県別総人口の上位 5 件を取得して両者の結果が完全一致することを確認する。
📥 入力データ (SSDSE-B-2026, 47 都道府県 × 約 100 列, 抜粋):
1 2 3 4 5 6 7 8 9 10 11 | import sqlite3, duckdb, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).rename(columns={'SSDSE-B-2026': 'Year'}) # SQLite (OLTP 系) で読む con1 = sqlite3.connect('ssdse.db') df.to_sql('ssdse', con1, if_exists='replace', index=False) r1 = pd.read_sql('SELECT Prefecture, A1101 FROM ssdse WHERE Year=2023 ORDER BY A1101 DESC LIMIT 5', con1) # DuckDB (OLAP 系) で同じ df を直接クエリ r2 = duckdb.query("SELECT Prefecture, A1101 FROM df WHERE Year=2023 ORDER BY A1101 DESC LIMIT 5").to_df() print(r1.equals(r2)) |
📤 実行すると次の出力が得られる:
💬 同じクエリでも DuckDB は CSV を直接スキャンし、 SQLite は事前に to_sql で取り込む。 47 行程度なら差は感じないが、 数百万行になると DuckDB は秒で、 SQLite は分単位の差が出る。 一方で SQLite は WAL ジャーナルにより同時更新に強く、 タグ・コメントなどのトランザクション用途で勝る。
リレーショナル DB を「単なる表ファイル」と勘違いしているうちは、 同時実行が起きると突然データが壊れる現象に遭遇する。 ACID (Atomicity / Consistency / Isolation / Durability) のうち、 学習者がとくに油断しがちなのが Isolation (分離) だ。 ANSI SQL は分離レベルを 4 段階で定義しており、 各 DBMS は実装方針が微妙に異なる。 ここを理解しないまま分析パイプラインを並列化すると、 「同じ集計を 2 回実行したのに結果が違う」という再現性の崩壊が起きる。
| 分離レベル | Dirty Read | Non-repeatable Read | Phantom Read | 典型用途 |
|---|---|---|---|---|
| READ UNCOMMITTED | 起き得る | 起き得る | 起き得る | ログ集計の概算値 |
| READ COMMITTED | 無い | 起き得る | 起き得る | 一般 Web アプリ (Postgres 既定) |
| REPEATABLE READ | 無い | 無い | 起き得る | レポート集計 (MySQL 既定) |
| SERIALIZABLE | 無い | 無い | 無い | 会計・銀行系 |
実装上は、 PostgreSQL や Oracle が採用する MVCC (Multi-Version Concurrency Control) により、 読み手は古いバージョンを、 書き手は新しいバージョンを同時に持てる。 読み手と書き手が互いをブロックしないため、 SSDSE のような頻繁に参照される統計データには相性が良い。 一方で MySQL InnoDB は REPEATABLE READ + Next-Key Lock で Phantom を抑える独自方式。 SQLite はファイルロックベースで、 WAL モード時のみ読み書きが並行できる。 「どの DB を使うか」は「どの分離レベルを実装しているか」と同義だと理解しておきたい。
このコードでやること:SSDSE-B-2026 を読み込んだ後、 「データ補正」を 564 行(47 都道府県 × 12 年)分まとめて適用する。 途中で異常があったら全体をロールバックし、 中途半端な状態を残さない。
📥 入力データ (補正前 ssdse テーブル):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import sqlite3 import pandas as pd # この抜粋だけで動くように、対象のテーブルをここで作っておく df = (pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) .rename(columns={'SSDSE-B-2026': 'Year'})) con = sqlite3.connect('ssdse.db') df.to_sql('ssdse', con, if_exists='replace', index=False) cur = con.cursor() cur.execute('ALTER TABLE ssdse ADD COLUMN 補正後人口 REAL') try: cur.execute('BEGIN') cur.execute('UPDATE ssdse SET 補正後人口 = A1101 * 1.005') cur.execute('SELECT COUNT(*) FROM ssdse WHERE 補正後人口 IS NULL') remain = cur.fetchone()[0] if remain != 0: raise RuntimeError('補正漏れあり') con.commit() print(f'COMMIT 成功: {len(df)} 件更新') except Exception as e: con.rollback() print('ROLLBACK: ', e) |
📤 実行すると次の出力が得られる:
💬 BEGIN ... COMMIT で囲むことで、 途中で RuntimeError が出れば rollback() が走り、 補正前の状態に戻る。 これが Atomicity (原子性) の本質。 pandas で更新するときも df.to_sql(..., method='multi') を使えば一括コミットされ、 同じ保護が得られる。
複数ユーザが同じ行を更新する可能性がある場合、 戦略は 悲観ロック (pessimistic locking) と 楽観ロック (optimistic locking) に大別できる。 悲観ロックは SELECT ... FOR UPDATE で行を即時にロックし、 他者を待たせる。 シンプルだがデッドロックや待ち時間が問題になる。 楽観ロックは「バージョン番号」や「更新タイムスタンプ」を行に持たせ、 更新時に「自分が読んだバージョンと一致するか」をチェックする方式で、 ぶつかったらアプリ側でリトライする。 SSDSE のような分析中心のシステムは、 編集が稀なので楽観ロックで十分な場合が多い。
| 方式 | 実装コスト | スループット | 向く場面 |
|---|---|---|---|
| 悲観ロック | 低 | 中 | 同時編集が頻繁、 衝突が高コスト |
| 楽観ロック | 中 | 高 | 読み中心、 衝突が稀 |
| MVCC | 高 (DB 内蔵) | 非常に高い | 汎用 (Postgres 等) |
commit() し忘れると、 VACUUM が効かず DB ファイルが膨張する。 jupyter で実験中によくある事故。PRAGMA journal_mode=WAL; で並行性が大幅改善するが、 ネットワークドライブでは推奨されない。rollback() を呼ばないと、 トランザクションは開いたまま。 Python では with con: を使うと自動コミット/自動ロールバックされて安全。DB の「速さ」はインデックスの設計でほぼ決まる。 インデックスとは、 ある列の値を検索しやすくするための補助データ構造で、 RDB の多くは B-Tree を採用している。 47 行の SSDSE では速度差は感じないが、 自治体 1700 行や年次 30 年 × 47 都道府県 = 1410 行を超える長期版 e-Stat、 さらに POS データ 100 万行クラスになると、 インデックスの有無で実行時間が 1000 倍以上違うのが普通だ。 ここでは EXPLAIN でクエリの実行計画を読み、 適切なインデックスを設計する作法を整理する。
| インデックス種別 | 構造 | 得意 | 不得意 |
|---|---|---|---|
| B-Tree | 平衡多分木 | 範囲検索、 ソート | 前方一致以外の LIKE |
| Hash | ハッシュ表 | 等価検索 | 範囲・ORDER BY |
| GIN / GiST | 転置 / 一般化探索木 | 全文検索、 配列、 JSON | 挿入コスト高 |
| 部分インデックス | WHERE 付き B-Tree | 疎な条件 | 適用条件外のクエリ |
| 複合インデックス | 複数列の B-Tree | 列順依存の検索 | 列順を外すクエリ |
このコードでやること:SSDSE-B-2026 を SQLite に読み込み、 「総人口でソートして上位 5 件」を取るクエリの実行計画を、 インデックス無し・有りで比較する。
📥 入力データ (ssdse テーブル, 47 行):
1 2 3 4 5 6 7 8 9 10 11 | import sqlite3, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) con = sqlite3.connect(':memory:') df.to_sql('ssdse', con, index=False) print('--- インデックス無し ---') print(pd.read_sql('EXPLAIN QUERY PLAN SELECT Prefecture, A1101 FROM ssdse ORDER BY A1101 DESC LIMIT 5', con)) con.execute('CREATE INDEX idx_pop ON ssdse(A1101 DESC)') print('--- インデックス有り ---') print(pd.read_sql('EXPLAIN QUERY PLAN SELECT Prefecture, A1101 FROM ssdse ORDER BY A1101 DESC LIMIT 5', con)) |
📤 実行すると次の出力が得られる:
💬 インデックス無しでは「全件スキャン + テンポラリ B-Tree でソート」が走るが、 インデックス有りでは「USE TEMP B-TREE FOR ORDER BY」が消え、 並んだ順に上位 5 件をそのまま返せる。 47 行では誤差レベルだが、 1000 万行では「全件スキャン + 全件ソート」が秒〜分単位の差になる。
CREATE INDEX ON ssdse(地方区分, 総人口) のような複合インデックスは、 検索条件が WHERE 地方区分='関東' AND 総人口 > 5000000 のとき最も効く。 一方で WHERE 総人口 > 5000000 単独では、 先頭列が条件に無いため使えない。 「左から順に検索条件で使う列を並べる」が鉄則で、 これを leftmost prefix rule と呼ぶ。 SSDSE で「地方区分 → 総人口 → 高齢化率」順に絞り込むなら、 その順で複合インデックスを張る。
オプティマイザは「どのインデックスを使うか」をテーブル統計情報 (行数・列の分布) に基づいて推定する。 大量挿入後に統計情報が古いと、 オプティマイザが誤った計画を選び性能が崩れる。 PostgreSQL では ANALYZE table_name;、 SQLite でも ANALYZE; で更新する。 自動 VACUUM ANALYZE を信頼しすぎず、 大規模ロード後は手動実行するのが業界の作法。
WHERE LOWER(name) = 'tokyo' は通常のインデックスでは効かず、 「式インデックス」を張る必要がある。 PostgreSQL は CREATE INDEX ON t (LOWER(name)) で解決。'xxx%' なら B-Tree が効くが、 中間一致は効かない。 全文検索系 (GIN+pg_trgm 等) に切り替える。前提: SQL、 テーブルデータ、 主キー、 外部キー。 並列: NoSQL、 データウェアハウス、 データレイク、 メタデータ。 発展: 分散データベース、 NewSQL、 HTAP、 ベクトル DB、 CAP 定理、 レプリケーション。
分散 DB を語るうえで避けて通れないのが CAP 定理だ。 Consistency (一貫性)、 Availability (可用性)、 Partition tolerance (分断耐性) の 3 つを同時に完全達成することはできない、 という定理で、 ネットワーク分断が起きたときに「一貫性を優先」か「可用性を優先」かを設計時に決める必要がある。 例えば銀行残高のような業務では一貫性を優先する CP 型 (HBase 等)、 ソーシャル投稿の表示では可用性を優先する AP 型 (Cassandra 等) を選ぶ。 SSDSE のような公的統計は更新頻度が低く分散も不要なので、 単一ノードの PostgreSQL / SQLite で十分で、 CAP の悩みから自由でいられるのが幸せな事実だ。
実務では「PACELC 定理」(分断時 PA/PC、 通常時 LA/LC) の方が現代的で、 ネットワーク分断が無いときでも「レイテンシ vs 一貫性」のトレードオフがあると明示する。 Google Spanner は「同期レプリ + TrueTime API」で PC/LC、 Amazon DynamoDB は調整可能 (Eventually Consistent or Strong)、 という具合に各サービスのポジションを 4 文字で表現できる。
可用性とスケーラビリティを上げる定石が レプリケーションと シャーディングだ。 レプリケーションは同じデータを複数ノードに複写し、 読み取りスループットと耐障害性を上げる。 同期 (synchronous) と非同期 (asynchronous) があり、 前者は一貫性が高いがレイテンシが大きく、 後者はその逆。 シャーディングはデータを「都道府県コード % N」のような関数でノードに分散させ、 書き込みもスケールさせる。 ただしシャーディング後にまたがる JOIN は非常に重く、 SSDSE のようにキー (都道府県コード) で揃ったデータは「同じシャードに置く」ことで JOIN コストを下げる工夫が必要になる。
| 手法 | 解決する問題 | 代表実装 | トレードオフ |
|---|---|---|---|
| マスタ-スレーブ | 読み取りスケール、 耐障害 | MySQL バイナリログ | 書き込みは単一 |
| マルチマスタ | 書き込みスケール | Galera、 CockroachDB | 競合解決が複雑 |
| 水平シャーディング | 大量データの分散 | Vitess、 Citus | クロスシャード JOIN |
| 垂直分割 | 列の物理分離 | アプリ側で実装 | JOIN コスト増 |
通常のビュー (VIEW) はクエリを保存するだけで、 毎回実行コストがかかる。 マテリアライズドビュー (materialized view) はクエリ結果を物理テーブルとして保持し、 高速な参照を可能にする。 SSDSE で「地方区分別の平均総人口」のような集計を毎回計算するのは無駄なので、 マテリアライズドビューにしておけば 1 ミリ秒で返せる。 ただし元データが更新されたら REFRESH MATERIALIZED VIEW が必要で、 鮮度とのトレードオフになる。 PostgreSQL では CONCURRENTLY オプションでロックを抑えて更新できる。
このコードでやること:SSDSE-B-2026 から「地方区分ごとの総人口合計」をマテリアライズドビュー風に SQLite テーブルとして保存し、 元テーブルが更新されたら再構築する。
📥 入力データ (ssdse テーブル, 47 行) — 地方区分は別途辞書で対応付ける想定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import sqlite3, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() # Code から地方区分を自作マッピング(8 地方区分) code = df['Code'].str[1:3].astype(int) bins = [0, 1, 7, 14, 23, 30, 35, 39, 47] labels = ['北海道', '東北', '関東', '中部', '近畿', '中国', '四国', '九州'] df['地方区分'] = pd.cut(code, bins=bins, labels=labels) con = sqlite3.connect(':memory:') df.to_sql('ssdse', con, index=False) con.execute("""CREATE TABLE mv_region_pop AS SELECT 地方区分, SUM(A1101) AS 合計人口, COUNT(*) AS 都道府県数 FROM ssdse GROUP BY 地方区分""") print(pd.read_sql('SELECT * FROM mv_region_pop ORDER BY 合計人口 DESC', con)) |
📤 実行すると次の出力が得られる:
💬 マテリアライズドビューは「同じ集計を何度も呼ぶダッシュボード」で威力を発揮する。 SSDSE のような静的データなら、 元データ更新時にだけ DROP TABLE mv_region_pop; CREATE TABLE mv_region_pop AS ... を再実行すればよい。
どんなに賢い DB を選んでも、 バックアップ戦略が無ければ「データが消える日」を遅らせるだけだ。 三大戦略は 論理バックアップ (pg_dump・mysqldump による SQL ダンプ)、 物理バックアップ (ファイル丸ごとコピー、 pg_basebackup)、 PITR (Point-in-Time Recovery) (WAL を継続保存し、 任意時刻まで巻き戻し可能) の 3 種類。 SSDSE のような不変公的データは「元 CSV を保存しておけば再ロード可能」なので論理バックアップで十分だが、 分析メモやタグなど「自分が生成したデータ」を持つようになると、 PITR まで含めた本格戦略が必要になる。
pg_dump は数時間かかる。 物理バックアップ + WAL アーカイブのほうが大規模では実用的。gpg や DB 機能で暗号化する。これまで見てきた要素 (系統、 トランザクション、 インデックス、 EXPLAIN、 CAP、 レプリ、 マテビュー、 バックアップ) を、 SSDSE のような公的データ分析環境に当てはめると次の優先順位になる。 第 1 段階: SQLite + pandas で「データを取り込んで JOIN・GROUP BY が書ける」状態にする。 第 2 段階: DuckDB を使い、 Parquet / CSV を直接クエリして集計を高速化する。 第 3 段階: PostgreSQL を導入し、 タグ・コメント・ジョブ履歴を持つ。 マテリアライズドビューで頻出集計を高速化する。 第 4 段階: 規模拡大時に列指向 OLAP (ClickHouse) と OLTP (PostgreSQL) を分離。 ETL/ELT で連携。 第 5 段階: 全社的に CDC (Change Data Capture) と Kafka で実時間データ統合、 BI ツール経由でセルフサービス分析。 SSDSE 47 行で第 1〜2 段階を体験することは、 第 5 段階の本番設計に直結する。 「小さな DB を完璧に扱う訓練」が「大きな DB を恐れずに扱う」最短距離だ。
DB を扱ううえで最も「やり直しコスト」が高いのが スキーマ設計だ。 SSDSE-B-2026 のような「都道府県 × 多数指標」という横長テーブルを RDB にそのまま投入すると、 列の追加 (例えば 2027 年版で新指標が増える) のたびに ALTER TABLE が必要になり、 履歴管理が壊れやすい。 こうした「長期的に列が増減する」データは、 縦長 (long format) の (都道府県, 年度, 指標名, 値) 構造に変換しておくと、 新指標が増えても行追加で済む。 これを「EAV (Entity-Attribute-Value) パターン」と呼ぶことがあり、 メリットは拡張性、 デメリットはクエリの複雑化と統計型の喪失だ。 SSDSE で「特定指標の時系列分析」を主眼にするなら EAV が便利、 「都道府県別ベンチマーク表の作成」が主眼ならピボット済みの wide format が便利、 と用途で使い分ける。
| 設計 | 列の柔軟性 | JOIN コスト | 型保証 | 向く用途 |
|---|---|---|---|---|
| Wide (横長) | 低 | 低 | 列ごとに保証 | 表示・ベンチマーク |
| Long (縦長) | 中 | 中 | 列単位で集約 | 時系列分析・可視化 |
| EAV | 高 | 高 | 弱 (値は文字列) | 指標の頻繁な増減 |
| スター・スキーマ | 中 | 中 (固定 JOIN) | 強 | データウェアハウス |
このコードでやること:SSDSE-B-2026 の横長テーブルから縦長 (long) 形式へ変換し、 「指標」「値」「都道府県」の 3 列構造で SQLite に保存する。
📥 入力データ (wide format, 564 行 × 112 列):
1 2 3 4 5 6 7 | import pandas as pd, sqlite3 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) long = df.melt(id_vars=['Prefecture'], var_name='指標', value_name='値') con = sqlite3.connect(':memory:') long.to_sql('ssdse_long', con, index=False) print(long[long['指標'] == 'A1101'].head().reset_index(drop=True)) print('総行数:', len(long)) |
📤 実行すると次の出力が得られる:
💬 564 行(47 都道府県 × 12 年)× 111 列を id_vars=['Prefecture'] で melt すると 564 × 111 = 62,604 行の long format になる(Year・Code 列も指標として展開される)。 来年「失業率」「保育園定員」が指標として追加されても、 行が増えるだけでスキーマ変更は不要。 一方で「人口と高齢化率の相関」を計算するときは、 一旦 wide に戻す (pivot) 手間がかかる。
モダン RDB は JSON 列をネイティブサポートしており、 PostgreSQL の jsonb は GIN インデックスで内部キーまで検索可能。 これにより「半構造化メタデータ」を 1 つの列に格納しつつ、 必要なキーだけ抽出してインデックスを張る、 という MongoDB ライクな柔軟性が RDB に持ち込める。 SSDSE の「指標名 → 単位 → 出典年 → コメント」のような可変属性は jsonb に放り込み、 確定済みの数値だけ通常列で管理する、 というハイブリッド構成が現実解になる。
スキーマは時間とともに進化する。 「いつ・誰が・どんな変更を加えたか」を Git のように管理するのが マイグレーションツールで、 Alembic (Python/SQLAlchemy)、 Flyway (Java)、 dbmate (汎用) などが代表。 SSDSE の取り込み層でも、 マイグレーションファイルに 001_create_ssdse.sql、 002_add_index.sql のように番号を振ってバージョン管理することで、 別マシンでも同じスキーマを再現できる。 jupyter での手作業 ALTER TABLE はやめて、 必ずマイグレーションファイル経由にする、 という規律が長期運用の鍵だ。
最後にセキュリティ。 もっとも古典的かつ未だに頻発するのが SQL インジェクションで、 文字列連結でクエリを組み立てるのが原因。 Python なら必ず cur.execute('SELECT * FROM ssdse WHERE 都道府県 = ?', (name,)) のようにパラメータ化する。 加えて、 アプリ用ユーザに DROP TABLE 権限を与えない・GRANT SELECT のみで足りる場面が多い、 という最小権限原則を徹底する。 PostgreSQL は行レベルセキュリティ (RLS) で「ユーザ A は東日本のみ閲覧可」を宣言的に実装でき、 公的データ匿名化の文脈でも有用。 監査ログ (audit log) は誰が何時にどのテーブルへアクセスしたかを残し、 漏洩時のフォレンジックに必要になる。
f"...{name}..." や文字列 + で組み立てない。 これだけで SQLi の 9 割は防げる。SELECT, INSERT, UPDATE のみ、 DDL は別の管理ロール。
Web アプリや分析バッチから DB に大量接続すると、 接続確立コスト (TCP ハンドシェイク + 認証 + プロセス起動) が支配的になる。 これを抑える定石が コネクションプーリングで、 PostgreSQL なら pgbouncer や pgpool-II、 Python なら SQLAlchemy の QueuePool が一般的。 プールサイズの目安は「CPU コア数 × 2 + ディスク数」と古くから言われ、 大きすぎるとコンテキストスイッチで遅くなる。 SSDSE 47 行程度の分析でも、 jupyter ノートブックを 10 枚開いて全て同じ DB を叩くようになったら、 プールを意識する価値がある。 監視は SELECT * FROM pg_stat_activity で「idle in transaction」が積もっていないかを最低限見ておく。
性能チューニングの一般指針として、 ボトルネックを CPU・メモリ・ディスク I/O・ネットワークのどこに置くかを最初に切り分ける。 EXPLAIN ANALYZE の Buffers 出力でディスク読み出し量を確認し、 shared_buffers や work_mem を調整するのが PostgreSQL の定番。 SSDSE 規模なら全てメモリに乗るため、 SQL の書き方 (不要な ORDER BY を消す・LIMIT を早めにかける) のほうが効果的だ。 さらに、 大量集計時は中間結果を一時テーブル (TEMP TABLE) に物理化し、 オプティマイザが推定を誤ることを避ける手法もよく使われる。 結論として、 DB の性能改善は「闇雲な設定変更」ではなく「測って、 計画を読んで、 ボトルネックに当てる」ことが王道だと覚えておきたい。 SSDSE での小さな実験こそが、 大規模本番への最良の素振りとなる。
本概念を SSDSE-B-2026 都道府県データで可視化する。 3 つの異なる切り口 (散布図・分布・群間比較) で多面的に理解する。



数式だけでは「実感」が湧きにくいので、 具体的な数値で 1 度手計算してみると理解が定着します。 以下の例は、 本サイトで扱う SSDSE-B-2026 や公開教材に近い形式で用意しました。
RDBMS と NoSQL の使い分け:
| 軸 | RDBMS | NoSQL |
|---|---|---|
| スキーマ | 固定(事前定義) | 柔軟(JSON) |
| 結合 | 得意 | 苦手 |
| 整合性 | 強い(ACID) | 弱い(BASE) |
| 水平スケール | 難しい | 得意 |
| 典型用途 | 会計・在庫 | ログ・セッション・SNS |
手計算で得た値と、 後述の Python 実装で算出した値が一致することを確認すると、 「数式とコードの対応関係」がクリアに見えるようになります。
合成データで DB の秒間クエリ処理数とレスポンス時間を計算する。
| 時間帯 | クエリ数 | 秒数 | QPS | 平均応答 [ms] |
|---|---|---|---|---|
| 朝 | 6,000 | 3600 | 1.67 | 50 |
| 昼 | 18,000 | 3600 | 5.0 | 80 |
| 夕 | 30,000 | 3600 | 8.33 | 120 |
| 夜 | 12,000 | 3600 | 3.33 | 70 |
| 深夜 | 3,000 | 3600 | 0.83 | 30 |
1 2 3 4 5 6 7 | import numpy as np queries = np.array([6000, 18000, 30000, 12000, 3000]) secs = 3600 qps = queries / secs print(f"QPS: {qps.round(2)}") print(f"平均: {queries.sum()/(5*secs):.2f}") print(f"ピーク: {qps.max():.2f}") |
💬 手計算 (Step 2) と Python 出力が完全一致。
「データベースへの問い合わせ」を手を動かして体感するコーナー。 下の 2 つの実データ表(SSDSE-B-2026 の 2023 年実測値を python 転記・捏造なし)に対して、 SELECT / WHERE / ORDER BY / GROUP BY を UI で組み立てると、 生成される SQL と結果表がリアルタイムに更新されます。 クエリの実行は外部ライブラリを使わず、 ページ内蔵の簡易クエリエンジン(JS)が正確に計算します。
列名の括弧内は SSDSE-B-2026 の変数コード。 region(地方)と regions.macro(東西区分)は地理的分類で、 統計実測値ではありません。
プルダウンとチェックで組み立て → 下に SQL と結果表が出ます。 GROUP BY を「地方」にすると集計モードに切り替わります。
prefectures.region = regions.region をキーに内部結合すると、 各都道府県の行に対応する地方の macro 列が結合されます。 対応が取れた行だけが残る様子を見てください。
「特定の都道府県を 1 件だけ探す」とき、 インデックスが無ければ先頭から順に照合する全表スキャン、 有れば B-tree でほぼ一発で到達します。 探す都道府県を選んで、 両方式の比較回数を見比べてください。
NULL は = NULL では一致しない(IS NULL を使う)。 NULL を含む列の AVG は NULL 行を除いて平均するので、 「0 とみなした平均」とはズレる。regions.region が一意なので 1:1 だが、 もし地方が重複していれば都道府県ごとに行が膨らむ。 主キー・外部キーで一意性を担保する。WHERE 列にインデックスが無いと全行を舐める。 データが 10 行なら誤差でも、 1 億行では致命的。公的統計(SSDSE-B-2026)を題材に、 最小限の Python コードで動作させます。 ファイルパス(data/raw/SSDSE-B-2026.csv)は自分の環境に合わせて変更してください。 まずはこのまま動かすことが理解の最短ルートです。
skiprows=[1] で 2 行目(日本語名)を飛ばし 1 行目のコード行を列名に採用します(cp932 エンコーディング必須)。 年度列 SSDSE-B-2026 は rename で Year に直しておくと SQL が書きやすくなります。 SQLite には可変長文字列(TEXT)と整数(INTEGER)として自動マッピングされます。ssdse.db(約 12〜18 MB)と、 SELECT 文の結果として 2023 年・人口(A1101)降順 TOP5 の都道府県(東京・神奈川・大阪・愛知・埼玉が並ぶはず)を 5 行 2 列の DataFrame で受け取ります。 ファイルが生成されたかは ls -lh ssdse.db で確認可能。to_sql は内部で「CREATE TABLE → INSERT INTO ... VALUES ...」を発行しており、 行数 564(47 都道府県 × 12 年)程度であれば 1 秒以内に完了します。 if_exists='replace' を 'append' に変えると追記に切り替わるので、 月次バッチ更新の挙動を疑似体験できます。 SELECT に EXPLAIN QUERY PLAN を前置すると、 全件スキャン (SCAN TABLE) かインデックス利用 (SEARCH TABLE) かが目視で確認できます。1 2 3 4 5 6 7 8 9 | # SQLite で SSDSE を扱う import sqlite3, pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).rename(columns={'SSDSE-B-2026': 'Year'}) conn = sqlite3.connect('ssdse.db') df.to_sql('ssdse_b', conn, if_exists='replace', index=False) result = pd.read_sql('SELECT Prefecture, A1101 FROM ssdse_b WHERE Year=2023 ORDER BY A1101 DESC LIMIT 5', conn) print(result) |
▶ 実行 を押せばこのページの中でそのまま動きます(ライブラリもデータも同梱済みで、 準備は要りません)。 手元の Python に移して動かすときは pip install numpy pandas が必要です。 読んでいるデータは data/raw/SSDSE-B-2026.csv。 日本語を含むので encoding='cp932' の指定を落とさないでください。
本サイトの全コードは 論文一覧ページ から実例として確認できます。 自分のデータで試したい場合は、 列名・欠損記号・単位の違いだけ調整すれば、 ほぼそのまま流用できます。
「データベース」を初めて使う方向けに、 ハンズオン的な実行手順を整理します。 上の Python 実装と組み合わせて、 1 度自分の手でなぞってみることを強く推奨します。
data/raw/ に配置(または自分のデータを用意)。 列名と単位を確認。df.head()、 df.describe()、 df.isna().sum() で全体像を把握。 ここで欠損や外れ値の見当を付ける。この 8 ステップを 1 度回すと、 「用語を読んで分かった気になる」段階から「実際に使える」段階に進めます。 知識は身体で覚えるのが結局のところ最速です。
データベース運用で頻出する失敗は、 (1) N+1 クエリ (1 件ずつ問い合わせて全件分繰り返す)、 (2) 適切なインデックス不在によるフルスキャン、 (3) トランザクション分離レベルの誤解 (READ COMMITTED と SERIALIZABLE の差)、 の 3 つです。 EXPLAIN で実行計画を読む、 適切なインデックスを張る、 という基本動作を覚えれば多くが解決します。
関連概念を視覚的に整理した概念マップ。
データベース (DB) は「複数ユーザーが同時に読み書きしても整合性が保たれる永続データ格納庫」である。 中心ノードは関係モデル (Codd, 1970) で、 行 = タプル、 列 = 属性、 主キー = 一意識別子、 外部キー = 参照整合性、 という 4 概念で SSDSE-B-2026 のような統計表を表現できる。
SSDSE-B-2026 を DB に格納すると、 都道府県マスタ (47 行) と年度別指標テーブル (47 × 年数 行) に分解され、 JOIN で再結合する。 この正規化により、 同じ「北海道」を毎年書き直さずに済み、 更新コストとストレージが削減される。 隣接領域として ACID トランザクション・インデックス・SQL クエリ最適化が連動する。
「データベース」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。 具体的には次の 3 方向と密接につながる:
DB は分析パイプラインの起点。 設計の良し悪しが下流すべてに波及する。
「データベース」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
「とりあえずデータベースに入れる」は、 CSV で足りる規模ではむしろ手間が増える。 1 台のメモリに載るかどうかが最初の分かれ目。
既出の各セクションを別の角度から補強する追記です。 「ファイルで十分では?」という素朴な疑問、 静かに壊れる NULL・整合性の罠、 ACID の先にある分散 DB の理論までを一気に見渡します。
DB の本質は「構造化データを永続的に格納・検索・管理する仕組み」です。 SSDSE-B-2026 は 1 個の CSV ファイルで配布されますが、 これを「みんなで使い続ける」段になると、 ファイルのままでは以下が壊れます。 DB はこの 5 つを標準機能として肩代わりしてくれる、 と捉えると腑に落ちます。
| 観点 | CSV/Excel(ファイル) | データベース |
|---|---|---|
| 同時編集 | 2 人が SSDSE-B を同時保存 → 後勝ちで一方の編集が消滅 | トランザクション+ロックで整合性を保証 |
| 検索 | 564 行なら一瞬だが、 数千万行では全走査で激遅 | インデックスで O(log N) 検索 |
| 整合性 | 存在しない県コードでも書けてしまう | 外部キー・CHECK 制約で不正値を拒否 |
| 型 | 全部が文字列。 "14086000" と数値が混在しがち | 列ごとに INT/NUMERIC/DATE を固定 |
| 部分取得 | 「2023 年の東京都だけ」でもファイル全体を読む | SQL で必要行・必要列だけ返す |
判断のコツ:SSDSE-B-2026(564 行 × 112 列、 更新は年 1 回)のように小さく・更新が稀・利用者が自分だけなら CSV + pandas で十分。 「同時に大勢が更新する」「数千万行」「壊れたら困る」のどれかに当てはまった瞬間が DB 化の合図です。
既出の「N+1・インデックス未設定・過剰正規化・SQL インジェクション」とは別系統の、 エラーを出さずに間違った結果を返すタイプの罠を 4 つ。 こちらの方が発見が遅れて被害が大きくなりがちです。
value = NULL は「不明」で常に非該当になり、 value <> 100 も NULL 行を取りこぼす。 NULL 判定は必ず IS NULL / IS NOT NULL。 集約でも AVG(value) は NULL 行を分母から除外する(=欠損を 0 とみなさない)点に注意。 なお SSDSE-B-2026 本体は欠損セル 0(完全データ)だが、 「ある年だけ観測が無い」縦持ち化後には NULL が現れうるので、 この挙動理解は必須。pref_code を持つ孤児行(orphan)が混入しても DB は黙認する。 「集計したら合計が合わない」の典型原因。 参照整合性はアプリ側の善意ではなく REFERENCES 制約で強制する。UPDATE ... SET x = x + 1 のように DB 内で加算するか、 楽観ロック(version 列)で防ぐ。 既定の分離レベル(READ COMMITTED)では防げないケースがある。ALTER TABLE ... ADD COLUMN NOT NULL DEFAULT ... は、 DB によっては全行書き換え+長時間ロックを誘発。 段階移行(NULL 可で追加→バックフィル→制約付与)が定石。 SSDSE を毎年“列追加”で拡張するなら特に意識したい。単一サーバの ACID を理解したら、 次は「複数台に分散したとき何を諦めるか」です。 ここが RDB / NoSQL / NewSQL を分ける分水嶺。