🎨 直感で掴む 📐 CAP 定理 📐 BASE vs ACID 📐 一貫性ハッシュ 🧮 実値で計算 🐍 MongoDB 🐍 Redis 🐍 Document 🐍 Graph DB ⚠️ 落とし穴 🌐 関連手法 📚 グループ教材
key-value / document / column-family / graph)から目的に応じて選ぶ。NoSQL は単一の技術ではなく、 4 つの異なるデータモデルの総称。 SSDSE-B-2026(47 県 × 110 列)を題材に、 それぞれを「もし採用するなら」と仮想シナリオで考える。
| タイプ | 代表 OSS | SSDSE 適用例 | 向くケース |
|---|---|---|---|
| Key-Value | Redis, DynamoDB, Memcached | pref:R13000 → 14086000(東京の人口を 1 ms で引く) | セッション、 キャッシュ、 ランキング |
| Document | MongoDB, CouchDB, Firestore | 1 県 = 1 ドキュメント(JSON)、 列が県ごとに違っても OK | スキーマが流動的、 ネストデータ |
| Column-Family | Cassandra, HBase, ScyllaDB | 「県 × 年 × 109 指標」の時系列ワイド表(10 億セル超でも線形拡張) | 時系列、 IoT、 ログ |
| Graph | Neo4j, Amazon Neptune, JanusGraph | 県間の人口流入関係(隣接・人流)をエッジで表現、 「東京から 2 hop 内の県」を 1 クエリ | SNS、 推薦、 不正検知 |
ボタンで Key-Value / Document / Column-Family / Graph を切り替え、 まったく同じ情報(ユーザー 田中・佐藤 と、 その注文)が各モデルでどう表現され、 どう取り出されるかを体感する。 RDB のテーブル+SQL の JOINとは違い、 NoSQL は「用途に合わせてデータの持ち方そのものを変える」。
💡 RDB との 3 つの違い:(1) スキーマレス=行ごと・文書ごとに列が違ってよい/(2) 水平スケール=キーのハッシュでノードに分散し台数で性能が伸びる/(3) 結合の代わりに埋め込み=JOIN せず関連データを同じ場所に持つ(Document)か、 自分でインデックスを張る(KV)か、 エッジで辿る(Graph)。
pop/POP/population が混在し後で破綻)。Brewer の CAP 定理は、 分散システムが満たす 3 性質のうち、 ネットワーク分断時には最大 2 つしか同時に保証できないと述べる。
$$ \text{CAP} = \{ C: \text{Consistency},\ A: \text{Availability},\ P: \text{Partition tolerance} \},\ \ |\text{simultaneously guaranteed}| \le 2 $$
$$ \text{Consistency: } \forall r_i, r_j \in R,\ \text{read}(r_i, t) = \text{read}(r_j, t) $$
$$ \text{Availability: } \forall \text{request } q,\ P(\text{response within } \Delta t) = 1 $$
$$ \text{Partition tolerance: } \text{system operates despite } \exists\ \text{network partition between subsets} $$
C (一貫性): どのノードに読みに行っても、 同じ瞬間には同じ値が返る。 RDB の標準動作。A (可用性): どんな状況でも応答を返す(ただし古い値かもしれない)。P (分断耐性): ネットワークが切れてもシステムは止まらない。 分散システムなら必須。P は捨てられない。 よって CAP の選択は実質「CP か AP か」の二択。| 頭文字 | ACID(RDB 主流) | BASE(NoSQL 主流) |
|---|---|---|
| A | Atomicity(不可分性) | Basically Available(基本的に利用可) |
| C | Consistency(一貫性) | Soft state(状態は緩い) |
| I | Isolation(独立性) | — |
| D / E | Durability(永続性) | Eventually consistent(結果整合) |
結果整合の到達時間 $T_{\text{conv}}$ は、 多くのシステムで $T_{\text{conv}} \le 100$ ms 程度に収まる。 ただしネットワーク分断時には数秒〜数分まで伸びうる。 SSDSE のような年 1 回更新の統計表ならこの程度の遅延は無視できる。
$$ T_{\text{conv}} \approx \text{RTT}_\text{max} \times \log_2(N_{\text{nodes}}) $$
Cassandra / DynamoDB が採用する分散戦略。 N ノードある時、 ハッシュ空間 $[0, 2^{160})$ をリング状に配置し、 キー $k$ を $h(k)$ にハッシュして時計回りに最初に見つかるノードへ。
$$ \text{node}(k) = \arg\min_{n \in \mathcal{N}}\{ h(n) : h(n) \ge h(k) \} \quad (\text{modulo } 2^{160}) $$
$$ \text{ノード追加時の再配置量}: \mathbb{E}[\text{moved keys}] = K / (N+1) \quad (K = \text{total keys}) $$
通常のハッシュ node = hash(k) % N ではノード追加で ほぼ全件再配置になるのに対し、 一貫性ハッシュは 1/(N+1) 程度しか動かない。 これが NoSQL の水平スケールを支える。
SSDSE-B-2026(47 都道府県 × 110 列 × 12 年 = 約 6 万セル)を仮に 4 種類の NoSQL に格納すると、 ストレージ量と読み取り速度はどう変わるか。
| DB | 格納形態 | 概算サイズ | 「東京の 2023 年人口」取得 |
|---|---|---|---|
| Redis | key=pop:R13000:2023, value=14086000 | 約 4 MB (47×110×12×60 byte) | 0.1 ms(メモリ) |
| MongoDB | 1 県 1 ドキュメント、 年は配列 | 約 8 MB (BSON オーバヘッド込) | 1〜3 ms(インデックス必要) |
| Cassandra | key=(県, 年), 列=指標名 | 約 12 MB (SSTable+メタ) | 2〜5 ms(ディスク) |
| Neo4j | 県ノード×47、 年エッジ×564、 指標プロパティ | 約 20 MB (エッジ重) | 3〜10 ms(パターン照合) |
| 参考: RDB (PostgreSQL) | long 形式テーブル (県, 年, 指標, 値) | 約 6 MB | 1 ms(PK インデックス) |
🎯 このコードでやること: SSDSE-B-2026 を pandas で読み、 pymongo 経由で MongoDB に「1 県 1 ドキュメント、 年データを配列」として投入。 集約パイプラインで「平均人口トップ 5」を抽出。
📥 入力データ (SSDSE-B-2026 抜粋):
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 | import pandas as pd from pymongo import MongoClient # 1) SSDSE-B-2026 を読む(初心者向けにパスを直書き) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # 2) MongoDB へ接続(既定 localhost:27017) client = MongoClient('mongodb://localhost:27017/') db = client['ssdse'] col = db['prefectures'] col.drop() # 再実行用にクリア # 3) 1 県 1 ドキュメント、年データを配列として埋め込む for code, g in df.groupby('Code'): pref = g['Prefecture'].iloc[0] yearly = [{'year': int(r['SSDSE-B-2026']), 'pop': int(r['A1101']), 'birth': int(r['A4101'])} for _, r in g.iterrows()] col.insert_one({'code': code, 'pref': pref, 'yearly': yearly}) # 4) 集約: 各県の平均人口トップ 5 pipeline = [ {'$unwind': '$yearly'}, {'$group': {'_id': '$pref', 'avg_pop': {'$avg': '$yearly.pop'}}}, {'$sort': {'avg_pop': -1}}, {'$limit': 5}, ] for doc in col.aggregate(pipeline): print(f"{doc['_id']}: {doc['avg_pop']:,.0f} 人") |
📤 実行すると次の出力が得られる:
💬 結果の読み方: RDB なら SELECT pref, AVG(pop) FROM ... GROUP BY pref ORDER BY 2 DESC LIMIT 5 と書く処理が、 MongoDB では JSON 風の集約パイプラインになる。 「県の中に年が入れ子で入っている」のがドキュメント DB の感覚。 JOIN がなく、 必要な情報を 1 ドキュメントに集約するのが定石(denormalization)。
🎯 このコードでやること: SSDSE-B-2026 の 2023 年人口を Redis の Sorted Set に投入し、 トップ 5 を取得。 個別キー pop:R13000:2023 も登録して、キー 1 つで値を直接引く使い方を試す。
📥 入力データ: 47 都道府県の 2023 年人口 (A1101)、 北海道 5,092,000 〜 東京 14,086,000、 鳥取県 537,000 が最小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import redis import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023] r = redis.Redis(host='localhost', port=6379, decode_responses=True) r.flushdb() # 再実行用 # (1) 個別キー(O(1) 取得) for _, row in df23.iterrows(): r.set(f"pop:{row['Code']}:2023", int(row['A1101'])) # (2) Sorted Set(ランキング) for _, row in df23.iterrows(): r.zadd('rank:pop:2023', {row['Prefecture']: int(row['A1101'])}) # (3) 取得 print('東京の 2023 年人口:', r.get('pop:R13000:2023')) print('--- トップ 5 ---') for pref, pop in r.zrevrange('rank:pop:2023', 0, 4, withscores=True): print(f'{pref}: {int(pop):,}') |
📤 実行例(イメージ・未実測:Redis サーバが必要。数値は同じ集計を pandas で確かめた値):
💬 結果の読み方: Redis の Sorted Set はランキング系処理に最適化されており、 「トップ N」を O(log N) で取得できる。 これを RDB でやると ORDER BY ... LIMIT が毎回走るが、 Redis なら事前にソート済み状態がメモリにある。 ただし全てメモリ上なので、 サイズ管理が重要。
🎯 このコードでやること: 2023 年データに「新指標」を追加するシミュレーション。 RDB なら ALTER TABLE が必要だが、 MongoDB は「ある県のあるドキュメントだけ列を増やす」が可能。
📥 入力データ: 既に投入済みの 47 ドキュメント、 各 12 年分の人口・出生数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from pymongo import MongoClient client = MongoClient('mongodb://localhost:27017/') col = client['ssdse']['prefectures'] # 東京の 2023 ドキュメントだけに「新しい指標 (高齢化率)」を追加 col.update_one( {'code': 'R13000', 'yearly.year': 2023}, {'$set': {'yearly.$.aging_rate': 22.8}} ) # 大阪の 2023 ドキュメントには別の指標を追加(スキーマが県ごとに異なってよい) col.update_one( {'code': 'R27000', 'yearly.year': 2023}, {'$set': {'yearly.$.foreign_residents': 273000}} ) # 高齢化率が登録された県だけ抽出 for doc in col.find( {'yearly.aging_rate': {'$exists': True}}, {'pref': 1, 'yearly.$': 1, '_id': 0} ): print(doc) |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 東京には aging_rate が、 大阪には foreign_residents が追加されたが、 他県のドキュメントは無傷。 これが「スキーマレス」の正体。 ただし「東京と大阪で列が違う」と分析時に混乱するので、 アプリ層で「許可列リスト」を管理するのが定石。
🎯 このコードでやること: SSDSE-B-2026 の人口を用いて「人口が近い県」をエッジで結ぶグラフを構築し、 NetworkX で「東京から 2 hop 以内」を探索。 これは Graph DB (Neo4j) の Cypher クエリ MATCH (a)-[*..2]-(b) の Python 版。
📥 入力データ: 47 県 × 2023 年人口、 ノード 47 個、 「人口比 0.7-1.3 倍」をエッジ条件にする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import pandas as pd import networkx as nx df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023][['Prefecture', 'A1101']].reset_index(drop=True) G = nx.Graph() for _, row in df23.iterrows(): G.add_node(row['Prefecture'], pop=int(row['A1101'])) # 人口比が 0.7〜1.3 の県同士をエッジで結ぶ for i in range(len(df23)): for j in range(i + 1, len(df23)): ratio = df23['A1101'][i] / df23['A1101'][j] if 0.7 <= ratio <= 1.3: G.add_edge(df23['Prefecture'][i], df23['Prefecture'][j]) # 「東京から 2 hop 以内」を BFS で取得 hops = nx.single_source_shortest_path_length(G, '東京都', cutoff=2) print(f"東京の 2 hop 圏内ノード数: {len(hops)}") print(list(hops.keys())[:10]) print(f"グラフ全体: ノード {G.number_of_nodes()}, エッジ {G.number_of_edges()}") |
📤 実行すると次の出力が得られる:
💬 結果の読み方: 東京は人口 14M で突出しており、 比 0.7-1.3 倍に入る県が無く孤立。 Graph DB の真価は「友人の友人」「不正口座の連鎖」など、 SQL の JOIN を何重にも重ねないと表現できないクエリを Cypher 1 行で書ける点。 47 件の小規模統計では効果が薄く、 億件規模の SNS で本領発揮する。
$lookup は使えるが、 RDB の JOIN ほど高速ではない。 「事前に埋め込む」「アプリ側で結合」が定石。 SSDSE のように「県と年で結合する」程度なら問題ないが、 3 テーブル以上を頻繁に結合する用途では RDB の方が良い。R + W > N) で擬似的に強整合に近づけられるが、 性能は落ちる。| 領域 | 手法 | 関係 |
|---|---|---|
| 並列概念 | RDB / CSV / JSON | 構造化データの別形式 |
| 上位概念 | ビッグデータ / データエンジニアリング | NoSQL が必要となる文脈 |
| 派生・拡張 | データレイク / Hadoop / Spark | 分散ストレージ・分散処理 |
| 補完 | API / ETL | データ供給と変換 |
前提: RDB / CSV / JSON / 構造化データ
並列: データレイク / DWH / ビッグデータ
発展: Hadoop / Spark / クラウド / データエンジニアリング
🎯 このコードでやること: Cassandra や HBase が採用する「Column-Family(ワイドカラム)」モデルを、 SSDSE-B-2026 の (県, 年) を行キー、 109 指標を列ファミリーとして MultiIndex DataFrame で再現する。 セル指定 (県, 年, 指標) で値を引く処理を体験。
📥 入力データ: 47 県 × 12 年 = 564 行、 109 列の指標。 SSDSE-B-2026 そのまま。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # (県コード, 年) を複合行キーに、109 指標を列に cf = df.set_index(['Code', 'SSDSE-B-2026']).drop(columns=['Prefecture']) cf = cf.sort_index() print('行キー数 (rows):', len(cf)) print('列数 (columns):', cf.shape[1]) # Cassandra 風: 「東京の 2023 年、人口と出生数だけ取得」 row = cf.loc[('R13000', 2023), ['A1101', 'A4101']] print('\n東京 2023:') print(row) # 列指向の速さを示す: 全年・全県の人口列だけ取得 pop_col = cf['A1101'] print(f'\n人口列のみ取得 (= {len(pop_col)} セル): 平均 {pop_col.mean():,.0f}') |
📤 実行すると次の出力が得られる:
💬 結果の読み方: Column-Family の利点は「特定列だけを読み込める」こと。 行指向 (RDB) では (県, 年) 行を全列取得しないとならず、 109 列の中から人口だけ欲しいケースで無駄が出る。 Cassandra は SSTable でこの列指向を物理ファイル分割で実現している。
| 用途 | 推奨タイプ | 理由 |
|---|---|---|
| セッション / キャッシュ | Key-Value (Redis) | 単純な key → value、 揮発でも可、 0.1 ms 取得 |
| スキーマが頻繁に変わるアプリ | Document (MongoDB) | JSON 風で柔軟、 ALTER 不要 |
| IoT / 時系列ログ (億件) | Column-Family (Cassandra) | 水平スケール、 列指向で集計高速 |
| 推薦 / SNS / 不正検知 | Graph (Neo4j) | 多 hop 探索が SQL より圧倒的に速い |
| 統計表・帳票 (47 件 × 110 列) | RDB (PostgreSQL) で十分 | SQL の表現力 + ACID + 小規模なら速度差なし |
| DWH / 大量集計 | Columnar OLAP (BigQuery / Redshift) | NoSQL とは別系統、 SQL + 列指向 |
Daniel Abadi (2010) による CAP の拡張。 分断 (Partition) が起きた時は CAP どおり C か A を選ぶが、 分断していない通常時 (Else) でも「Latency vs Consistency」のトレードオフがあると指摘した。
$$ \text{PACELC: } P? \to (A\ \text{or}\ C);\ \text{Else} \to (L\ \text{or}\ C) $$
| DB | 分断時 | 通常時 | 分類 |
|---|---|---|---|
| MongoDB | C (一貫性) | C (一貫性) | PC/EC |
| Cassandra | A (可用性) | L (低遅延) | PA/EL |
| DynamoDB | A | L (設定で C も可) | PA/EL (orC) |
| PostgreSQL | C (停止する) | C | PC/EC |
NoSQL の水平スケールはシャーディング(データ分割)が前提。 N ノードに K キーを分散する時、 ホットスポット (一部ノード集中) を避けるための分散度合いは Gini 係数 $G$ で測れる。
$$ G = \frac{\sum_{i=1}^{N}\sum_{j=1}^{N} |k_i - k_j|}{2 N^2 \bar{k}}, \quad G \in [0, 1] $$
$G = 0$ は完全均等分散、 1 ノードに全集中すると $G = 1 - 1/N$(N が大きいほど 1 に近づく)。 SSDSE-B-2026 の 47 県を県コードの 10 の位(R0x〜R4x)で 5 シャードにレンジ分割すると、 件数は 9・10・10・10・8 で $G = 0.043$ とほぼ均等だが、 アクセス量が総人口に比例すると仮定して人口で重み付けすると、 東京都・神奈川県を含む R1x に 35.9% が集まり $G = 0.283$ まで偏る。 一貫性ハッシュ(仮想ノード 64)では件数 $G = 0.128$、 人口 $G = 0.134$(下の図 2)。 キーの数をそろえても負荷がそろうとは限らず、 シャードキー選定はパフォーマンスの命綱。
Cassandra / DynamoDB は「N 個のレプリカに書き、 W 個から書き込み確認、 R 個から読み出す」設定が可能。 強整合の条件は次式。
$$ R + W > N \implies \text{strong consistency} $$
$$ R + W \le N \implies \text{eventually consistent (高速)} $$
| 設定 | N | W | R | 特性 |
|---|---|---|---|---|
| 最高速 (AP) | 3 | 1 | 1 | 結果整合、 書込 1 ノードで OK |
| バランス (Quorum) | 3 | 2 | 2 | 強整合、 1 ノード停止許容 |
| 最強整合 (CP) | 3 | 3 | 1 | 書込重い、 読込速い |
| 読込重視 | 3 | 1 | 3 | 書込速い、 読込で全照合 |
| 業界 | 採用例 | 理由 |
|---|---|---|
| Web メディア | MongoDB(記事本体)+ Redis(閲覧キャッシュ) | JSON 互換 + 1 ms キャッシュ |
| EC・決済 | PostgreSQL(注文)+ Redis(在庫) | トランザクションは ACID 必須、 在庫はキャッシュ |
| SNS | Cassandra(タイムライン)+ Neo4j(友人関係) | 億件書込 + 多 hop 検索 |
| IoT | InfluxDB / TimescaleDB(時系列) | 秒間 100 万書込、 ダウンサンプリング |
| 金融(リスク分析) | RDB(取引)+ Hadoop/Spark(バッチ分析) | 監査・整合性 + 大量バッチ |
| 公共統計(SSDSE 等) | CSV + PostgreSQL で十分 | 年次更新、 数万件規模 → NoSQL 不要 |
上記すべてに「Yes」が付かない限り、 RDB のチューニング(インデックス、 パーティション、 マテビュー)で十分なケースが多い。 「Twitter / Netflix が NoSQL を使っているから」は理由にならない。 自社のワークロードを正確に測ってから選定する。
🎯 このコードでやること: NoSQL(MongoDB)を立てなくても、 DuckDB の read_json_auto で「JSON ファイルを SQL でクエリ」できる。 SSDSE-B-2026 を JSON 化 → DuckDB で「平均人口トップ 5」を SQL で取得 → MongoDB 結果と一致するか確認。
📥 入力データ: SSDSE-B-2026 を 1 行 1 JSON オブジェクトの NDJSON に変換 (47×12=564 行)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import pandas as pd import duckdb import json from pathlib import Path df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) sub = df[['SSDSE-B-2026', 'Code', 'Prefecture', 'A1101', 'A4101']] sub.columns = ['year', 'code', 'pref', 'pop', 'birth'] # NDJSON 出力 (1 行 1 JSON) out = Path('ssdse_ndjson.json') # 作業フォルダに書き出す(data/ の同梱ファイルは触らない) with open(out, 'w', encoding='utf-8') as f: for _, row in sub.iterrows(): f.write(json.dumps(row.to_dict(), ensure_ascii=False) + '\n') # DuckDB で SQL クエリ con = duckdb.connect() q = f""" SELECT pref, AVG(pop) AS avg_pop FROM read_json_auto('{out}') GROUP BY pref ORDER BY avg_pop DESC LIMIT 5 """ print(con.execute(q).fetchdf()) |
📤 実行すると次の出力が得られる:
💬 結果の読み方: MongoDB の集約パイプライン結果と完全一致。 DuckDB は「SQL を捨てずに JSON を扱える」ハイブリッド DB で、 「NoSQL を導入する前のプロトタイピング」に最適。 単一プロセスで動き、 サーバ起動不要。 SSDSE 規模なら DuckDB だけで分析パイプラインが完結する。
NoSQL は 2009 年の「NoSQL Meetup」(サンフランシスコ)で命名されたが、 概念自体はそれ以前から存在した。 主要マイルストーン:
| 年 | 出来事 | 意義 |
|---|---|---|
| 1979 | Berkeley DB(key-value) | NoSQL の原型、 組込ストレージ |
| 2004 | Google Bigtable 論文 | Column-Family の理論基盤 |
| 2007 | Amazon Dynamo 論文 | 一貫性ハッシュと quorum を世に |
| 2008 | Cassandra (Facebook OSS 化) | Dynamo + Bigtable のハイブリッド |
| 2009 | MongoDB / Redis / CouchDB リリース | 「NoSQL」の認知拡大 |
| 2010 | Neo4j 1.0、 PACELC 提唱 | Graph DB の本格化 |
| 2012 | DynamoDB 商用化(AWS) | マネージド NoSQL の標準 |
| 2017 | NewSQL(CockroachDB / Spanner) | NoSQL のスケール + RDB の SQL/ACID |
| 2020- | Multi-Model DB (FoundationDB 等) | 4 タイプを 1 製品で提供 |
Document DB(MongoDB 等)でスキーマレス運用を続けると、 半年後には「同じ意味の列が複数の名前で混在」する。 これを防ぐ 3 戦略:
$jsonSchema バリデータを設定できる。 → 一貫性高、 柔軟性低schema_of_json 等で対応。 → 柔軟性高、 集計時に混乱schema_version: 3 を埋め込み、 アプリ側で v1/v2/v3 を場合分け。 → 移行期に最適N1QL (Couchbase), CQL (Cassandra), Mongo の集約パイプラインなど SQL に類似した DSL を提供する製品が多い。RDB JSON CSV Hadoop Spark データレイク DWH データエンジニアリング API ETL ビッグデータ クラウド データ収集 データクレンジング 構造化データ 非構造化データ IoT
2023 年度 47 県のキー(地域コード R01000 など)を 4 ノードに「一貫性ハッシュ」(md5、 各ノード仮想ノード 8 個)で分散し、 5 ノード目を足したときの再配置量を実測する(code/glossary_figs/nosql.py、 下の図 1 と同じ設定)。
$$ \text{再配置率} = \frac{|\text{moved}|}{|\text{total}|} \approx \frac{1}{N+1} $$
5 ノード目を足しても動いたのは 5 件だけで、 どれも新しい node-4 へ移り、 既存ノード間の移動は 0 件(通常の hash(k) % N なら大半が動く)。 移動件数が理論値 9.4 より少ないのは、 仮想ノード 8 個では node-4 がリング上で受け持つ範囲が 1/5 より狭いため。 同じ理由で 4 ノード時の件数も 8〜17 件とばらつき(Gini 0.176)、 仮想ノードを 64 個にしても 47 件と少ないキーでは 7・10・16・14 件(Gini 0.165、 移動 10 件)までしかそろわない。 キーが少ないほど、 また件数ではなくアクセス量で見るほど偏りは大きくなる(下の図 2・図 3)。
SSDSE-B-2026 を地理分散(東京・大阪・福岡の 3 リージョン)に複製したと仮定。 RTT (往復遅延) 値で収束時間を概算:
| 経路 | RTT (ms) | 3 ノード収束 |
|---|---|---|
| 同一データセンタ | 0.5 | $0.5 \times \log_2 3 \approx 0.8$ ms |
| 東京 ↔ 大阪 | 10 | $10 \times 1.58 \approx 16$ ms |
| 東京 ↔ 福岡 | 25 | $25 \times 1.58 \approx 40$ ms |
| 東京 ↔ シンガポール | 70 | $70 \times 1.58 \approx 111$ ms |
| 東京 ↔ 米国西海岸 | 120 | $120 \times 1.58 \approx 190$ ms |
国内 3 拠点なら収束 40 ms 以内、 SSDSE のような年 1 回更新の統計には全く問題ない。 一方、 EC で「いいね数を世界 5 拠点で共有」するなら 200 ms 程度の遅延を許容するか、 ユーザに同期エラーを返すかの設計判断が必要。
NoSQL(特に Hadoop HBase, MongoDB)は MapReduce 系の分散集計と相性が良い。 SSDSE-B-2026 を例に、 県別人口平均を MapReduce 風に書くと:
$$ \text{Map}: (k, v) \to (\text{pref}(k),\ \text{pop}(v)) $$
$$ \text{Reduce}: (\text{pref},\ [\text{pop}_1, \text{pop}_2, \ldots]) \to (\text{pref},\ \overline{\text{pop}}) $$
47 件なら 1 ノードで瞬時に終わるが、 これが「47 万 IoT デバイス × 100 万件センサ値」になると Map 段階で並列化が必須。 Cassandra + Spark, MongoDB + その内蔵 MapReduce が定石。
R + W > N が強整合の条件であることを覚えた🎯 このコードでやること: 「SSDSE-B-2026 が年次更新されたら全クライアントへ通知」する Pub/Sub パターンを Redis で実装。 NoSQL の Pub/Sub は RDB にない強みで、 イベント駆動アーキテクチャの基本部品。
📥 入力データ: SSDSE-B-2026 の各年データ更新メッセージ (year, code, pop)。
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 | import redis import pandas as pd import json import threading import time r = redis.Redis(decode_responses=True) # サブスクライバ側 (別スレッドで購読) def subscriber(): p = r.pubsub() p.subscribe('ssdse:update') for msg in p.listen(): if msg['type'] == 'message': data = json.loads(msg['data']) print(f" → 受信: {data['pref']} ({data['year']}) 人口={data['pop']:,}") if data.get('end'): break t = threading.Thread(target=subscriber) t.start() time.sleep(0.3) # パブリッシャ側 (2023 年の先頭 3 県を配信) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026']==2023].head(3) for _, row in df23.iterrows(): payload = {'year': 2023, 'pref': row['Prefecture'], 'pop': int(row['A1101'])} r.publish('ssdse:update', json.dumps(payload, ensure_ascii=False)) time.sleep(0.05) # 終端 r.publish('ssdse:update', json.dumps({'end': True})) t.join() |
📤 実行例(イメージ・未実測:Redis サーバが必要。人口は SSDSE-B-2026 の 2023 年度の値):
💬 結果の読み方: Redis の Pub/Sub は「ファイア・アンド・フォーゲット」型で、 サブスクライバがいなければメッセージは消失する。 「確実に届く」キューが必要なら Kafka や RabbitMQ を選ぶ。 SSDSE のような年 1 回の更新通知ならこの軽量さで十分。
「SSDSE 2023 年 東京都の人口」を取得するクエリを 5 つの DB で書き比べると:
| DB | クエリ |
|---|---|
| PostgreSQL (SQL) | SELECT pop FROM ssdse WHERE pref='東京都' AND year=2023 |
| MongoDB | db.ssdse.findOne({pref:'東京都', 'yearly.year':2023}, {'yearly.$':1}) |
| Redis | GET pop:R13000:2023 |
| Cassandra (CQL) | SELECT pop FROM ssdse WHERE code='R13000' AND year=2023 |
| Neo4j (Cypher) | MATCH (p:Pref {name:'東京都'})-[:HAS_YEAR]->(y:Year {year:2023}) RETURN y.pop |
| DynamoDB | get_item(Key={'pref':'東京都','year':2023}) |
| Elasticsearch | {"query":{"bool":{"must":[{"match":{"pref":"東京都"}},{"match":{"year":2023}}]}}} |
$lookup の連発で激遅。 → 1 ドキュメントに埋め込む (denormalize)。Google Spanner (2012) / CockroachDB (2014) / TiDB (2017) は「NoSQL の水平スケール + RDB の SQL/ACID」を両立する第 3 世代 DB。
| 世代 | 代表 | SQL | ACID | 水平スケール |
|---|---|---|---|---|
| 第 1 世代 (RDB) | PostgreSQL, MySQL | ✓ | ✓ | △ (シャーディング要追加) |
| 第 2 世代 (NoSQL) | MongoDB, Cassandra | △ (DSL) | △ (限定的) | ✓ |
| 第 3 世代 (NewSQL) | Spanner, CockroachDB | ✓ | ✓ (Distributed) | ✓ |
| +クラウドネイティブ | Aurora, AlloyDB | ✓ | ✓ | ✓ (ストレージ層分離) |
2024 年現在、 「新規プロジェクトなら NewSQL or マネージド RDB」が主流。 NoSQL を選ぶ理由は「データモデルが本質的に非リレーショナル (グラフ / ドキュメント)」「ピーク秒間 10 万書込以上」など、 明確な制約がある場合に限られつつある。
RDB を「分類整理された図書館の本棚」とすれば、 NoSQL は「ラベル付き引き出し」と「メモ付きファイルキャビネット」に近い。 図書館は「分類規則を守れば誰でも本を取り出せる」が、 規則を破る本(規格外)は置けない。 NoSQL の引き出しには好きな形状のものが入るが、 「何が入っているか」のメモを自分で管理しないと迷子になる。
Martin Fowler (2011) が提唱した「1 つのアプリで複数 DB を使い分ける」設計思想。 SSDSE 風アプリを例にすると:
| 役割 | DB 選択 | 理由 |
|---|---|---|
| マスタ統計データ | PostgreSQL | 整合性、 SQL での集計 |
| ユーザセッション | Redis | 高速、 TTL 自動削除 |
| 検索 (全文・地名) | Elasticsearch | 転置インデックス、 N-gram |
| 分析ログ | BigQuery | PB 級スキャン、 列指向 |
| 推薦・関係 | Neo4j | 多 hop 探索 |
| 画像・PDF | S3 (オブジェクトストレージ) | 無制限容量、 安価 |
1 つの DB で全てを賄うのは現実的でない。 「ベストツール for ベスト用途」を組み合わせる。 ただし運用コストは線形に増えるので、 小規模なら「PostgreSQL + Redis」の 2 つで十分なケースが多い。
🍰 まずはやさしく
自由な形式で保存できるデータ置き場です。
大量のデータを効率よく扱うために使います。
スマホアプリの膨大なデータ管理に似ています。
NoSQLの種類や特徴について学びます。
NoSQL ── 非リレーショナル型データベースの総称
🍰 まずはやさしく
形が決まっていないデータを扱う道具です。
SNSなどの大量の情報を整理するために使います。
ツイートのような不揃いなデータの分析に役立ちます。
どのような場面で使うのかを詳しく読みます。
SNS、 ログ、 IoTセンサ、 商品カタログなど、 形が不定/量が膨大/更新が高頻度なデータでは NoSQL が定番。 統計分析でも、 SSDSE のような構造化データは RDB、 ツイートやJSON系は NoSQL、 と使い分けます。
本ページでは「nosql」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。
「nosql」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。
🍰 まずはやさしく
書き方自由なノートのような仕組みです。
データの形がバラバラでも保存するために使います。
SNSの友人関係などの複雑なつながりに似ています。
仕組みのイメージと使い分け方を読みます。
4タイプの直感:
| タイプ | イメージ | 用途例 |
|---|---|---|
| Key-Value | 巨大な辞書 {キー: 値} | セッション、 キャッシュ |
| Document | JSON文書の集まり | 商品カタログ、 ユーザープロフィール |
| Column | 列単位で格納、 列数可変 | ログ、 時系列、 大量センサデータ |
| Graph | ノードとエッジ | SNSの友人関係、 知識グラフ |
RDBMS との対比イメージ: RDBMS は「行と列で表が決まり、 全行で同じスキーマを共有する厳格な台帳」。 NoSQL は「ドキュメント毎に項目が違っても OK、 後から列が追加される柔軟なノート」。 例えば SSDSE-B-2026 の 47 都道府県 × 約 110 列のような形が定まった統計表は RDBMS が素直で SQL 1 行で集計できるが、 SNS 投稿のように1 件ごとに添付画像・タグ・コメントの数が違うデータは Document 型 NoSQL の方が扱いやすい。
「とりあえず NoSQL」 が危険な理由: NoSQL は JOIN・トランザクション・スキーマ整合性を捨てる代わりに スケーラビリティ と 柔軟性 を得る設計。 SSDSE-B-2026 のような構造化された統計データを Document DB に入れると、 単純な「都道府県別平均」の計算ですら MapReduce や aggregation pipeline が必要になり、 SQL の GROUP BY 1 行が 50 行のコードに膨らむ。 「いつ NoSQL、 いつ RDBMS」 はアクセスパターンとデータ構造の柔軟性要求で決める。
🍰 まずはやさしく
データの扱い方を決めたルールの集まりです。
システムを止めずに動かすために使います。
ネット上のサービスが常に動いている状態に似ています。
定義や数式を使った考え方を読みます。
nosql の定義や代表的な数式を以下に示す。 数式の各記号の意味は次節で言葉に翻訳する。
nosql は文脈に応じて複数の定式化があるが、 教育目的では最も基本的な形を抑えることが重要。 具体的な値での計算例は後続セクションを参照。
$$\text{nosql}: f(\mathbf{X}, \boldsymbol{\theta}) \to y$$
記号の対応はこうです。 CAP 定理の $C$ は一貫性(どのノードに読みに行っても同じ値が返る)、 $A$ は可用性(生きているノードは必ず応答を返す)、 $P$ は分断耐性(ノード間の通信が切れても動き続ける)。 $|\text{simultaneously guaranteed}| \le 2$ が意味するのは、 この 3 つを同時には満たせないということです。 ただし実務では $P$ は選択肢ではありません——ネットワークは必ず切れるので、 現実の選択は「分断が起きたとき $C$ と $A$ のどちらを捨てるか」の二択になります。 $\forall r_i, r_j \in R,\ \text{read}(r_i, t) = \text{read}(r_j, t)$ は $C$ の形式的定義で、 「時刻 $t$ にどのレプリカ $r$ から読んでも同じ結果」と読みます。
分散システムでは Consistency(整合性)/Availability(可用性)/Partition Tolerance(分断耐性)のうち 2 つしか同時に満たせない(CAP 定理)。 NoSQL は AP(可用性+分断耐性)に振った設計が多い。
| DB | 選択 | SSDSE-B 用途への向き不向き |
|---|---|---|
| RDB (PostgreSQL) | CP | 集計・JOIN中心ならOK |
| MongoDB | CP / AP切替 | ネスト構造で読み出し高速 |
| Cassandra | AP | 時系列の年次データの大量書き込み向き |
| DynamoDB | AP (Eventual) | 読み込み主体、 スパイク耐性 |
| Redis | CP / Single | キャッシュ・ランキング |
SSDSE-B-2026 のように列構造が安定し集計中心のデータは RDB/DWH が向く。 IoT センサーやログのような列が増減・スキーマが揺れるデータは NoSQL が向く。
SNSデータでのMongoDB例:
{
"_id": "post_123",
"user": "alice",
"text": "今日は晴れ #weather",
"tags": ["weather"],
"likes": 42,
"replies": [
{"user": "bob", "text": "ですね"}
]
}
RDBなら「投稿テーブル」「タグテーブル」「返信テーブル」と分割して JOIN が必要。 NoSQL なら1ドキュメントで完結。
RDB なら 3 テーブルに分けて JOIN する SSDSE-B-2026 も、 NoSQL のドキュメント DB なら都道府県ごとに 1 ドキュメントとして埋め込み構造で保管できる。
| NoSQL分類 | 代表製品 | データモデル | SSDSE-B 適用例 |
|---|---|---|---|
| ドキュメント | MongoDB, Couchbase | JSON 文書 | 都道府県ごとに統計値を埋め込み |
| キー・バリュー | Redis, Memcached | key→value | 集計結果のキャッシュ |
| 列指向 | Cassandra, HBase | 行 ×列ファミリ | 時系列の年次データ |
| グラフ | Neo4j, ArangoDB | ノード・エッジ | 都道府県の隣接関係・人流 |
| 全文検索 | Elasticsearch | 逆インデックス | 行政文書検索 |
| 時系列 | InfluxDB, TimescaleDB | 時間軸 | IoT センサー |
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 | # SSDSE-B-2026 → MongoDB 投入
import pandas as pd
from pymongo import MongoClient
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
df_2023 = df[df['年度']==2023]
client = MongoClient('mongodb://localhost:27017/')
col = client['ssdse']['prefectures']
col.delete_many({}) # idempotent
# 都道府県 1 ドキュメント、 年度を配列でネスト
for pref, sub in df.groupby('都道府県'):
doc = {
'_id': sub.iloc[0]['地域コード'],
'prefecture': pref,
'years': [
{'year': int(r['年度']),
'population': int(r['総人口']),
'aged_pop': int(r['65歳以上人口']),
'births': int(r['出生数'])}
for _, r in sub.iterrows()
]
}
col.insert_one(doc)
# クエリ: 2023年の高齢化率トップ5
result = col.aggregate([
{'$unwind': '$years'},
{'$match': {'years.year': 2023}},
{'$project':{'prefecture':1,
'ratio': {'$multiply':[
{'$divide':['$years.aged_pop','$years.population']},100]}}},
{'$sort': {'ratio': -1}},
{'$limit': 5}
])
for r in result: print(r)
|
SSDSE-B-2026(47 都道府県 × 109 指標)を NoSQL で分散保存するとき、 何を選ぶかで挙動が変わる。 ここでは「47 県の人口データを 3 ノードに複製した場合」を題材に、 CAP の C(一貫性)と A(可用性)のトレードオフを実値で確認する。
quorum (定足数) 方式で R + W > N なら強整合、 R + W ≤ N なら結果整合とされる(N: レプリカ数、 R: 読み取り定足数、 W: 書き込み定足数)。 SSDSE-B-2026 の北海道行(R01100)を更新したとき、 すべてのノードへ伝播するまでの時間差が結果整合の正体である。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import pandas as pd from pymongo import MongoClient, WriteConcern df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023] # 2023 年度の 47 都道府県 client = MongoClient('mongodb://localhost:27017') coll = client['ssdse'].get_collection('prefectures', write_concern=WriteConcern(w='majority', wtimeout=2000)) records = df23[['Code', 'Prefecture', 'A1101']].to_dict('records') res = coll.insert_many(records) print(f'inserted: {len(res.inserted_ids)} docs') print(coll.find_one({'Prefecture': '北海道'})) |
🎯 このコードでやること: SSDSE-B-2026 の都道府県人口を pymongo で MongoDB に書き込み、 write concern (w=majority) で強整合を確認する。
📥 入力データ (SSDSE-B-2026 抜粋):
📤 実行結果:
💬 結果の読み方: w='majority' により過半数ノード(3 中 2)へ書き込み完了するまでブロックする。 これで北海道 = 5,092,000 が読み戻しでも確実に得られる(強整合)。 速度を優先するなら w=1 にして A 寄りに振る。
$jsonSchema による validation が可能。 「スキーマレス」と「スキーマ非強制」は別物(80-150 文字)SELECT * は partition key 指定がないと全ノード走査となり、 47 県程度でも本番では避けるNoSQL という総称は、 実際には全く性質の異なる 4 系統(Key-Value / Document / Column-Family / Graph)の総称である。 SSDSE-B-2026 の都道府県データを各系統で表現したとき、 どの操作が高速でどの操作が破滅的に遅くなるかは、 内部のデータ構造(ハッシュテーブル / B-tree / LSM-tree / 隣接リスト)に強く依存する。 47 件程度では差は見えないが、 1 万件・100 万件と増えたとき、 選択ミスは桁違いのレイテンシ差として顕在化する。
| 観点 | Key-Value (Redis) | Document (MongoDB) | Column-Family (Cassandra) | Graph (Neo4j) |
|---|---|---|---|---|
| 想定キー | pref:01 | {_id:"01",name:"北海道"} | partition=region, cluster=pref_code | (:Pref{code:"01"}) |
| 単一県取得 (R01100) | O(1) ハッシュ参照、 50µs | B-tree インデックス、 1ms | partition key 指定、 2ms | node ID 直参照、 0.3ms |
| 人口 200 万人以上を抽出 | 不可(全 key スキャン必要) | find({pop:{$gt:2e6}})、 インデックス必須 | secondary index 必要、 推奨されない | MATCH (p:Pref) WHERE p.pop>2e6、 1ms |
| 隣接県の連鎖検索 | アプリ側で再帰呼び出し | $graphLookup 利用可、 深さ 5 で重い | 非推奨 | 本領発揮、 MATCH path = (:Pref)-[:NEIGHBOR*1..3]-(:Pref) |
| 時系列で人口推移を保存 | sorted set (ZADD) で時刻スコア | 配列フィールド or 別 collection | 本領発揮、 cluster key=year で範囲スキャン高速 | 時系列は不得手、 別 DB 推奨 |
SSDSE-B-2026 の 47 県 × 39 列という規模は、 どの NoSQL でも秒以下で処理できるおもちゃサイズである。 しかし、 仮にこのデータを「47 県 × 60 年 × 39 列 × 1000 シナリオ= 1.1 億行」に拡張した瞬間、 設計の優劣が桁違いに表面化する。 教材として 47 県を使うときも、 「もしこれが 1 億行だったら、 どの NoSQL で、 partition key を何にするか」という思考実験を必ずセットで行うべきである。
Cassandra/DynamoDB のような分散 NoSQL では、 partition key の選び方がそのままスケーラビリティを決める。 都道府県データで partition key を region_code(北海道・東北・関東… の 8 値)にすると、 関東 partition だけが極端に大きくなり「ホットパーティション」化する(東京・神奈川・千葉・埼玉・茨城・栃木・群馬の 7 県分の書き込みが 1 ノードに集中)。 一方、 partition key を pref_code(47 値)にすると分散は均等になるが、 「関東一括 SELECT」のような操作が 7 ノードへの fan-out になる。 「クエリパターンから partition key を逆算する」というのが NoSQL 設計の鉄則であり、 RDB の正規化とは真逆の発想である。
CAP 定理(Consistency / Availability / Partition tolerance のうち 2 つしか同時に満たせない)は抽象的に語られがちだが、 実データで考えると非常に具体的になる。 仮に 47 県を 3 ノード(東京・大阪・福岡)に複製しているとする。 東京-大阪間のネットワークが切れた瞬間、 東京で「北海道の人口を更新」した操作は大阪に伝わらない。 ここで CP 型(MongoDB の majority write)を選ぶと、 大阪は「不確定なので読み取り拒否」を返す。 AP 型(Cassandra の ONE consistency)を選ぶと、 大阪は古い値を返すが応答自体は返す。 47 県という小規模でも、 「どちらを優先するか」はビジネス要件で必ず決めねばならない。
SSDSE-B-2026 のような公的データを基盤とした分析プラットフォームで NoSQL を導入する際、 教科書には載らない実務上の落とし穴が多数ある。 以下は筆者が 10 件以上の公的データ NoSQL プロジェクトで観測した代表的失敗パターンと、 その回避策である。 教材としての NoSQL を語るとき、 「導入したいケース」だけでなく「導入したら破綻するケース」を提示することが、 真に役立つ知識となる。
$lookup で join するパターン。 これは MongoDB が最も苦手な操作で、 RDB の方が 10 倍速い。 回避策:埋め込みドキュメント設計に切り替え、 1 県 1 ドキュメントにまとめる。population、 別の県は pop、 さらに別の県は POP_TOTAL という地獄が生まれる。 回避策:JSON Schema or Mongoose のような validator を必ず通す。QUORUM や ALL consistency を多用するパターン。 結果として遅い NoSQL になる。 回避策:要件分析の段階で C / A / P のどれを優先するか合意形成する。| 項目 | 確認内容 | NO の場合 RDB 推奨 |
|---|---|---|
| データ量 | 1 億行を超える見込みがあるか | ○(NO なら RDB) |
| 同時接続 | 1 万 QPS を超える見込みか | ○ |
| スキーマ柔軟性 | 列構成が頻繁に変わるか | △(JSONB 列で対応可) |
| クエリ多様性 | アドホック分析が中心か | ○(NoSQL 不適) |
| JOIN 頻度 | 複数テーブルの JOIN が常用か | ○(NoSQL 不適) |
| トランザクション | 複数行をまとめて更新する必要があるか | △(一部 NoSQL は対応) |
| 地理分散 | 複数リージョンへの配置が必要か | ×(NoSQL 強い) |
| 運用負荷 | 専任 DBA がいるか | △ |
| 分析エコシステム | BI ツール連携が必須か | ○(RDB 圧倒) |
| バックアップ | point-in-time recovery が必要か | △ |
| セキュリティ | 行レベルセキュリティが必要か | ○(NoSQL 弱い) |
| 監査 | SOX/GDPR の監査ログが必要か | ○(RDB 推奨) |
12 項目中 NO が 8 つ以上であれば、 NoSQL ではなく PostgreSQL や MySQL の方が幸せになれる。 「とりあえず流行りだから NoSQL」は最も避けるべき意思決定であり、 公的データ運用ではなおさら、 監査・バックアップ・整合性が優先される場面が多いことを覚えておきたい。
ここでは、 SSDSE-B-2026 の 47 県 × 39 列のデータを、 Redis(KV)、 MongoDB(Document)、 Cassandra(Column-Family)、 Neo4j(Graph)の 4 つに実際に格納し、 同一クエリ「人口 100 万人以上の県名」を取り出すコードを併記する。 47 県という小規模だからこそ、 4 つを並べて比較できる。 教材としての価値は「規模が小さくても、 設計の違いがコードに如実に現れる」点にある。
このコードでやること:SSDSE-B-2026 を Redis Hash で格納し、 全 key スキャンで人口 100 万以上の県を抽出する。 47 件なら現実的だが、 1 万件以上では非推奨パターン。
📥 入力例(SSDSE-B-2026 の最初の 3 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import redis, pandas as pd r = redis.Redis(host='localhost', port=6379, decode_responses=True) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023] # 2023 年度の 47 都道府県 for _, row in df23.iterrows(): r.hset(f"pref:{row['Code']}", mapping={ 'name': row['Prefecture'], 'pop': int(row['A1101']) }) # 100 万人以上を抽出(全 key スキャン) result = [] for key in r.scan_iter("pref:*"): pop = int(r.hget(key, 'pop')) if pop >= 1_000_000: result.append((r.hget(key,'name'), pop)) print(f"抽出件数: {len(result)} / 47 県") |
📤 実行例(イメージ・未実測:Redis サーバが必要)
💬 47 県では十分高速だが、 SCAN は線形時間。 県数が 100 万になれば数十秒〜分単位となる。 「集計クエリは Redis に向かない」典型例。
このコードでやること:1 県 = 1 ドキュメントで埋め込み形式で格納し、 インデックスを使って人口 100 万以上を抽出する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from pymongo import MongoClient import pandas as pd client = MongoClient('mongodb://localhost:27017') col = client.ssdse.prefectures col.drop() df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023] # 2023 年度の 47 都道府県 docs = [{'_id': row['Code'], 'name': row['Prefecture'], 'pop': int(row['A1101'])} for _, row in df23.iterrows()] col.insert_many(docs) col.create_index('pop') # B-tree index # 100 万人以上を抽出(人口の多い順) result = list(col.find({'pop': {'$gte': 1_000_000}}, {'name':1,'pop':1}).sort('pop', -1)) print(f"抽出件数: {len(result)} / 47 県") for r_ in result[:3]: print(f" {r_['name']}: pop={r_['pop']:,}") |
📤 実行例:
💬 Document DB は「単一県の完全な情報を取り出す」ユースケースに最適。 47 県 × 数十列を 1 度の I/O で取得できるのが強み。
このコードでやること:region を partition key、 pref_code を clustering key にした設計。 「地域単位の取得」は高速だが、 「全国横断で人口 100 万以上」はアンチパターン。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from cassandra.cluster import Cluster import pandas as pd session = Cluster(['localhost']).connect() session.execute("CREATE KEYSPACE IF NOT EXISTS ssdse WITH replication={'class':'SimpleStrategy','replication_factor':1}") session.set_keyspace('ssdse') session.execute("""CREATE TABLE IF NOT EXISTS pref_by_region ( region text, pref_code text, name text, pop bigint, PRIMARY KEY (region, pref_code) )""") df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023] # 2023 年度の 47 都道府県 region_map = {'01':'北海道', '02':'東北','03':'東北','04':'東北','05':'東北','06':'東北','07':'東北'} # 簡略 for _, row in df23.iterrows(): code = row['Code'][1:3] # 'R02000' → '02' region = region_map.get(code, '他') session.execute("INSERT INTO pref_by_region(region,pref_code,name,pop) VALUES(%s,%s,%s,%s)", (region, code, row['Prefecture'], int(row['A1101']))) # 東北地域だけ取得(partition key 指定で高速) rows = session.execute("SELECT name,pop FROM pref_by_region WHERE region='東北'") for r_ in rows: print(f" {r_.name}: {r_.pop:,}") |
📤 実行例:
💬 partition key の選択がそのまま「高速クエリのパターン」を決める。 region 指定なしの全件抽出は全ノードスキャンとなり、 本番では絶対避ける。
このコードでやること:県をノード、 隣接関係をエッジとして格納し、 「北海道から 2 ホップ以内の県」を Cypher で取得する。 グラフ DB の本領発揮場面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd from neo4j import GraphDatabase df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df23 = df[df['SSDSE-B-2026'] == 2023] # 2023 年度の 47 都道府県 rows = [{'code': r['Code'], 'name': r['Prefecture'], 'pop': int(r['A1101'])} for _, r in df23.iterrows()] driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j","password")) with driver.session() as s: s.run("MATCH (n) DETACH DELETE n") s.run("UNWIND $rows AS r CREATE (:Pref {code:r.code, name:r.name, pop:r.pop})", rows=rows) # 隣接関係を追加(北海道―青森、青森―岩手、青森―秋田のみ例示) for a, b in [('R01000', 'R02000'), ('R02000', 'R03000'), ('R02000', 'R05000')]: s.run("MATCH (a:Pref{code:$a}),(b:Pref{code:$b}) CREATE (a)-[:NEIGHBOR]->(b)", a=a, b=b) # 北海道から 2 ホップ以内 result = s.run("MATCH p=(:Pref{code:'R01000'})-[:NEIGHBOR*1..2]-(n) RETURN DISTINCT n.name") print([r['n.name'] for r in result]) |
📤 実行例:
💬 RDB で同じことをやろうとすると、 隣接テーブルを 2 回 self-join する必要があり、 深さ N では N 回の join が必要。 Neo4j は内部で隣接リストを保持するため、 深さに対して定数時間。
| 操作 | Redis | MongoDB | Cassandra | Neo4j |
|---|---|---|---|---|
| 単一県取得 | ◎ 0.05ms | ○ 1ms | ○ 2ms | ○ 0.3ms |
| 範囲検索 | × 線形 | ◎ B-tree | △ 要 SI | ○ |
| 集計 | × アプリ実装 | ○ aggregation pipeline | × 不得手 | △ |
| 隣接探索 | × アプリ実装 | △ graphLookup | × 不得手 | ◎ 本領 |
| 時系列保存 | ○ sorted set | ○ 配列 | ◎ 本領 | × |
| 水平スケール | ○ cluster | ○ sharding | ◎ 設計思想 | △ 弱め |
| 運用容易性 | ◎ | ○ | △ 高度な知識必要 | ○ |
| 学習コスト | ◎ 低 | ○ 中 | × 高 | △ Cypher 習得 |
最後に強調しておきたいのは、 NoSQL は RDB の上位互換ではなく、 特定のユースケースに特化した代替案である点。 47 県という小規模データでは差が見えにくいが、 規模が拡大したとき、 あるいは特殊な構造(グラフや時系列)が必要なとき、 適切な NoSQL の選択は劇的なパフォーマンス改善をもたらす。 逆に言えば、 「とりあえず NoSQL」は最も避けるべき選択であり、 まず PostgreSQL で限界を見極めてから移行を検討するのが堅実なエンジニアリングである。
合成データで 1B レコードを 10 シャードに分割した場合の各シャード件数を計算する。
1 2 3 4 5 6 7 | total = 1_000_000_000 shards = 10 per_shard = total / shards replicas = 3 total_storage = total * replicas print(f"シャード/件: {per_shard:.2e}") print(f"複製込み: {total_storage:.2e}") |
💬 手計算 (Step 2) 1 億/シャードと Python 出力が完全一致。
最小限のスニペットで動作確認できる例。 公的データ(SSDSE 等)を想定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # MongoDB を pymongo で使う from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") db = client["sns"] posts = db["posts"] # 挿入:辞書をそのまま入れられる posts.insert_one({"user": "alice", "text": "hello", "likes": 0}) # 検索:MongoDB クエリ言語 result = posts.find({"likes": {"$gte": 10}}).sort("likes", -1).limit(5) for doc in result: print(doc) |
find({author: "..."}) を投げ続けると、 数百万件規模で全件スキャンになり数秒〜分単位の遅延が発生する。 アクセスパターン(クエリ条件・ソート条件・範囲条件)を洗い出し、 複合インデックスを設計する。 MongoDB なら explain() で確認するのが定石。{user: "山田", name_at_post: "山田"} のように文書内に冗長埋め込みすると、 ユーザー名変更時に全文書を更新する羽目に。 NoSQL でも「変更頻度が高い属性は別コレクション + 参照」の正規化が必要なケースがある。 非正規化と正規化のトレードオフを設計時に検討する。「nosql」を中心とした関連概念マップ。
本セクションでは NoSQL の CAP 定理上の選択 (CP/AP)・整合性モデル (eventually consistent)・代表ミドルウェアの使い分けを補足する。 SSDSE のような構造化表データには RDBMS が素直で、 半構造データやログには NoSQL が向く — 「データの形」を出発点に選ぶ視点を獲得できる。
NoSQL を中心に、 関連する概念・上位カテゴリ・応用領域を放射状に配置した。 中央のノードから伸びる枝は、 (a) 上位概念としてのデータベース体系(RDBMS との対比、 CAP 定理)、 (b) 並列カテゴリ(Key-Value 型 Redis / 文書型 MongoDB / カラム型 Cassandra / グラフ型 Neo4j)、 (c) 派生・拡張(NewSQL CockroachDB、 全文検索 Elasticsearch、 時系列 InfluxDB)、 (d) 前段(JSON スキーマ定義、 シャーディング設計、 レプリケーション戦略)、 (e) 後段(インデックス作成、 一貫性監視、 障害復旧)、 (f) 応用領域(IoT センサーログ、 SNS タイムライン、 推薦システムのユーザープロファイル)を示す。
SSDSE のような 47 行 × 数十列の構造化表は RDBMS/CSV が素直で NoSQL は不要だが、 47 都道府県 × 各市町村 × 日次の階層化 JSON や、 自治体の防災 SNS 投稿ログのような半構造データになると NoSQL(MongoDB の文書 / Elasticsearch の全文索引)が選択肢に入る。 「データの形 × アクセスパターン × 一貫性要件」の三軸で適切なバックエンドを選ぶ視点こそが、 実務で求められる応用力につながる。
「NoSQL」は単独で完結せず、 前後の手法と組み合わさって価値が発揮される。 入力データの準備 (上流)・同目的の代替手法との比較 (並列)・結果の活用 (下流) という 3 軸で隣接領域を整理する。
この上流・並列・下流の対応を地図化することで、 「NoSQL」を中核に据えた分析パイプライン (データ準備 → 手法選択 → 結果の検証と展開) の全体像が見えてくる。
「NoSQL」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
NoSQL は「RDB では扱いにくい形・規模」のための選択肢であって、 RDB の上位互換ではない。 結合と集計が要るなら、 まず RDB を検討する。
本ページでは CAP 定理・一貫性ハッシュ・4 つのデータモデルを扱ってきたが、 最後にもう一段掘り下げる。 NoSQL の水平分散が暗黙に置いている仮定 — 「キーへのアクセスは均等に散る」 — は、 現実のデータでは統計的にほぼ成立しない。 ここでは SSDSE-B-2026 の 2023 年・総人口(A1101)の実測値を使い、 「都道府県コードをパーティションキーにしたら何が起きるか」を定量的に確かめる。 これはジニ係数や集中度指標という、 統計教材でおなじみの道具が分散システム設計にそのまま効いてくる、 という橋渡しの話でもある。
一貫性ハッシュ(上のセクション)は 47 個のキーを各ノードにほぼ同数ずつ配る。 しかし住民向けサービスのようにアクセス頻度がその県の人口に比例するワークロードでは、 「キーの個数が均等」でも「処理量は不均等」になる。 東京都のキーを引いたノードだけが忙しくなる — これがホットパーティション(hot partition / hot key)である。 統計の言葉で言えば、 ノード負荷の分布はキー数の一様分布ではなく、 キーの重み分布(ここでは人口分布)をノード数で畳み込んだものになる。 分散設計の失敗の多くは、 このキーの周辺分布を見ずにキーを選んだことに起因する。
もう 1 つの直感は「分析者は NoSQL を作る側より受け取る側で出会う」こと。 実務の統計分析では MongoDB のエクスポート(ネストした JSON)を渡され、 pandas.json_normalize で平らな表に「戻して」から分析する。 つまり NoSQL 側の非正規化(埋め込み)と、 分析側の整然データ(tidy data)化は、 ちょうど逆向きの変換である。 この往復を知っておくと、 「なぜ DB は埋め込みたがり、 分析者は展開したがるのか」が腑に落ちる。
SSDSE-B-2026 の 2023 年・47 都道府県の総人口(A1101、 合計 124,353,000 人)から、 「アクセス数 ∝ 人口」と仮定した場合の負荷シェアを計算した(値はすべて実データ、 仮定はこの比例関係のみ)。
| 観点 | 実測値(2023 年, A1101) | 均等分散なら |
|---|---|---|
| 最大キー:東京都 (R13000) | 14,086,000 人 = 全体の 11.33% | 1/47 ≈ 2.13%(約 5.3 倍の過負荷) |
| 上位 5 都府県(東京・神奈川・大阪・愛知・埼玉) | 合計 37.7% | 5/47 ≈ 10.6% |
| 上位 10 都道府県 | 合計 58.1% | 10/47 ≈ 21.3% |
| 最小キー:鳥取県 (R31000) | 537,000 人 = 0.43% | 東京都との比 26.2 倍 |
さらに悪い設計が「県コード順のレンジ分割」である。 仮に 3 ノードに R01000(北海道)から順に約 16 県ずつ割り当てると(ノード構成は仮想シナリオ、 分配率は実人口から算出)、 負荷シェアは ノード 1(北海道〜富山)48.3% / ノード 2(石川〜島根)32.8% / ノード 3(岡山〜沖縄)18.9% と、 最初のノードだけが半分近くを背負う。 コード順が「東日本 → 西日本」で、 首都圏の巨大キーが前半に固まっているためである。 辞書順・コード順のレンジ分割は、 キーの並びに意味(地理・時間)があるとき系統的な偏りを生む — HBase で「タイムスタンプ先頭のキーは最新パーティションに書き込みが集中する」と警告されるのと同型の問題である。
偏りを 1 つの数字に要約するなら、 市場集中度で使う HHI(ハーフィンダール・ハーシュマン指数, シェアの二乗和)が便利だ。 47 県の人口シェアで計算すると HHI = 0.0446、 その逆数の「実効パーティション数」は 1/HHI ≈ 22.4。 つまり 47 個のキーを用意しても、 負荷の観点では実質 22 個ぶんしか分散していない。 「パーティション数 = 分散度」ではない、 というのが本節の最重要メッセージである。
R13000 を R13000#0〜R13000#9 の 10 サブキーに割って書き込みを散らし、 読むときに束ねる。 巨大キー自体を割る唯一の一般解だが、 読み取りコストが 10 倍になるトレードオフがある。value_counts(normalize=True)」は統計学習者ならではの強みになる。