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

🔖 キーワード索引

NoSQLMongoDBRedisスキーマレス水平分散BASE

🔖 キーワード索引(追補)

🎨 直感で掴む 📐 CAP 定理 📐 BASE vs ACID 📐 一貫性ハッシュ 🧮 実値で計算 🐍 MongoDB 🐍 Redis 🐍 Document 🐍 Graph DB ⚠️ 落とし穴 🌐 関連手法 📚 グループ教材

💡 30 秒で分かる結論(追補)

📍 文脈ボックス:あなたが今見ているもの

この記事は データエンジニアリング の「ストレージ選定」段階で参照する。 上位概念は ビッグデータ、 並列概念は RDB(リレーショナル DB)、 派生概念は データレイク / Hadoop / Spark。 SSDSE-B-2026 を題材に「47 件規模の統計表でも NoSQL を使ってみる」演習を通じて、 RDB と NoSQL の境界線を体感する。

🎨 直感で掴む:NoSQL の 4 タイプを「47 県データ」に当てはめる

NoSQL は単一の技術ではなく、 4 つの異なるデータモデルの総称。 SSDSE-B-2026(47 県 × 110 列)を題材に、 それぞれを「もし採用するなら」と仮想シナリオで考える。

タイプ代表 OSSSSDSE 適用例向くケース
Key-ValueRedis, DynamoDB, Memcachedpref:R13000 → 14086000(東京の人口を 1 ms で引く)セッション、 キャッシュ、 ランキング
DocumentMongoDB, CouchDB, Firestore1 県 = 1 ドキュメント(JSON)、 列が県ごとに違っても OKスキーマが流動的、 ネストデータ
Column-FamilyCassandra, HBase, ScyllaDB「県 × 年 × 110 指標」の時系列ワイド表(10 億セル超でも線形拡張)時系列、 IoT、 ログ
GraphNeo4j, Amazon Neptune, JanusGraph県間の人口流入関係(隣接・人流)をエッジで表現、 「東京から 2 hop 内の県」を 1 クエリSNS、 推薦、 不正検知

🎮 触って比較:同じ「ユーザーと注文」を 4 つのデータモデルで持つ

ボタンで Key-Value / Document / Column-Family / Graph を切り替え、 まったく同じ情報(ユーザー 田中・佐藤 と、 その注文)が各モデルでどう表現され、 どう取り出されるかを体感する。 RDB のテーブル+SQL の JOINとは違い、 NoSQL は「用途に合わせてデータの持ち方そのものを変える」。

📦 データの持ち方 —

      
🎯 取得操作 —

        
        
      

💡 RDB との 3 つの違い:(1) スキーマレス=行ごと・文書ごとに列が違ってよい/(2) 水平スケール=キーのハッシュでノードに分散し台数で性能が伸びる/(3) 結合の代わりに埋め込み=JOIN せず関連データを同じ場所に持つ(Document)か、 自分でインデックスを張る(KV)か、 エッジで辿る(Graph)。

🎨 直感
「表は 1 種類」ではなく、 用途に合わせて多様なデータの持ち方を選ぶ。 キーで一発なら KV、 まとまりで持つなら Document、 列で集計するなら Column、 関係を辿るなら Graph。
⚠️ 落とし穴
結合が苦手(JOIN 相当をアプリ側ループで書くと N+1 で遅い)/結果整合性(書いた直後に古い値が返る)/スキーマレスの負債(列名 pop/POP/population が混在し後で破綻)。
🚀 発展
CAP 定理BASE でトレードオフを理解し、 ポリグロット永続化(1 システム内で KV+Document+Graph を適材適所で併用)へ。 ビッグデータ基盤の定石。

📐 CAP 定理(数式・形式化)

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} $$

🔬 数式を言葉で読み解く

📐 BASE vs ACID(トランザクション特性)

頭文字ACID(RDB 主流)BASE(NoSQL 主流)
AAtomicity(不可分性)Basically Available(基本的に利用可)
CConsistency(一貫性)Soft state(状態は緩い)
IIsolation(独立性)
D / EDurability(永続性)Eventually consistent(結果整合)

結果整合の到達時間 $T_{\text{conv}}$ は、 多くのシステムで $T_{\text{conv}} \le 100$ ms 程度に収まる。 ただしネットワーク分断時には数秒〜数分まで伸びうる。 SSDSE のような月次更新統計表ならこの程度の遅延は無視できる。

$$ 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 を 4 種 NoSQL に投入

SSDSE-B-2026(47 都道府県 × 110 列 × 12 年 = 約 6 万セル)を仮に 4 種類の NoSQL に格納すると、 ストレージ量と読み取り速度はどう変わるか。

DB格納形態概算サイズ「東京の 2023 年人口」取得
Rediskey=pop:R13000:2023, value=14086000約 4 MB (47×110×12×60 byte)0.1 ms(メモリ)
MongoDB1 県 1 ドキュメント、 年は配列約 8 MB (BSON オーバヘッド込)1〜3 ms(インデックス必要)
Cassandrakey=(県, 年), 列=指標名約 12 MB (SSTable+メタ)2〜5 ms(ディスク)
Neo4j県ノード×47、 年エッジ×564、 指標プロパティ約 20 MB (エッジ重)3〜10 ms(パターン照合)
参考: RDB (PostgreSQL)long 形式テーブル (県, 年, 指標, 値)約 6 MB1 ms(PK インデックス)
SSDSE-B-2026 (12 年分): 47 県 × 110 列 × 12 年 = 62,040 セル Redis: 62040 × 60 byte ≈ 3.7 MB MongoDB: 47 県 × (110 列 × 12 年 × 30 byte) ≈ 7.4 MB ノード追加時の再配置 (N=3 → 4): 62040 / 4 ≈ 15510 セルのみ移動

🐍 Python 実装

① MongoDB に SSDSE-B-2026 を投入(Document 型)

🎯 このコードでやること: SSDSE-B-2026 を pandas で読み、 pymongo 経由で MongoDB に「1 県 1 ドキュメント、 年データを配列」として投入。 集約パイプラインで「平均人口トップ 5」を抽出。

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

SSDSE-B-2026 Code Prefecture A1101 (年, 人口 [人]) 2023 R01000 北海道 5092000 2022 R01000 北海道 5140000 2023 R13000 東京都 14086000 2023 R27000 大阪府 8763000
 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} 人")

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

東京都: 13,956,000 人 神奈川県: 9,222,000 人 大阪府: 8,829,000 人 愛知県: 7,512,000 人 埼玉県: 7,344,000 人

💬 結果の読み方: RDB なら SELECT pref, AVG(pop) FROM ... GROUP BY pref ORDER BY 2 DESC LIMIT 5 と書く処理が、 MongoDB では JSON 風の集約パイプラインになる。 「県の中に年が入れ子で入っている」のがドキュメント DB の感覚。 JOIN がなく、 必要な情報を 1 ドキュメントに集約するのが定石(denormalization)。

② Redis でランキング・キャッシュ(Key-Value 型)

🎯 このコードでやること: SSDSE-B-2026 の 2023 年人口を Redis の Sorted Set に投入し、 トップ 5 を取得。 個別キー pop:R13000:2023 も登録して 0.1 ms 取得を体験。

📥 入力データ: 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):,}')

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

東京の 2023 年人口: 14086000 --- トップ 5 --- 東京都: 14,086,000 神奈川県: 9,229,000 大阪府: 8,763,000 愛知県: 7,477,000 埼玉県: 7,331,000

💬 結果の読み方: Redis の Sorted Set はランキング系処理に最適化されており、 「トップ N」を O(log N) で取得できる。 これを RDB でやると ORDER BY ... LIMIT が毎回走るが、 Redis なら事前にソート済み状態がメモリにある。 ただし全てメモリ上なので、 サイズ管理が重要。

③ MongoDB のスキーマレス利点(年ごとに列が増えても OK)

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

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

{'pref': '東京都', 'yearly': [{'year': 2023, 'pop': 14086000, 'birth': 86348, 'aging_rate': 22.8}]}

💬 結果の読み方: 東京には aging_rate が、 大阪には foreign_residents が追加されたが、 他県のドキュメントは無傷。 これが「スキーマレス」の正体。 ただし「東京と大阪で列が違う」と分析時に混乱するので、 アプリ層で「許可列リスト」を管理するのが定石。

④ Graph DB 風: NetworkX で県間の関係を表現(Neo4j の代用)

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

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

東京の 2 hop 圏内ノード数: 1 ['東京都'] グラフ全体: ノード 47, エッジ 142

💬 結果の読み方: 東京は人口 14M で突出しており、 比 0.7-1.3 倍に入る県が無く孤立。 Graph DB の真価は「友人の友人」「不正口座の連鎖」など、 SQL の JOIN を何重にも重ねないと表現できないクエリを Cypher 1 行で書ける点。 47 件の小規模統計では効果が薄く、 億件規模の SNS で本領発揮する。

⚠️ 落とし穴(5 件)

  1. JOIN が苦手: MongoDB の $lookup は使えるが、 RDB の JOIN ほど高速ではない。 「事前に埋め込む」「アプリ側で結合」が定石。 SSDSE のように「県と年で結合する」程度なら問題ないが、 3 テーブル以上を頻繁に結合する用途では RDB の方が良い。
  2. 整合性 vs 可用性のトレードオフ: CAP 定理により、 ネットワーク分断時には「正しい古い値」か「新しいが壊れているかも」のどちらかを選ぶしかない。 金融取引なら CP、 SNS の「いいね」数なら AP、 と用途で使い分ける。
  3. スキーマレスの罠: 「列を自由に増やせる」のは魅力だが、 半年後には「型がバラバラで集計できない」「同じ意味の列が別名で複数ある」事態になる。 必ずアプリ層でスキーマ検証 (JSON Schema 等) を導入する。
  4. 結果整合を「即時整合」と勘違い: Cassandra に書いた直後に別ノードで読むと、 古い値が返ることがある。 「読み取り直後に書いた値が見える」を前提にしたアプリは壊れる。 Quorum 設定 (R + W > N) で擬似的に強整合に近づけられるが、 性能は落ちる。
  5. 運用負荷を甘く見る: NoSQL は「シャーディング設計」「レプリケーション設定」「コンパクション調整」など、 RDB より運用知識が要る。 月次バッチ集計なら PostgreSQL + パーティショニングで十分なことが多い。「RDB が遅い」と感じたら、 まずインデックスとクエリ最適化を疑うべき。
領域手法関係
並列概念RDB / CSV / JSON構造化データの別形式
上位概念ビッグデータ / データエンジニアリングNoSQL が必要となる文脈
派生・拡張データレイク / Hadoop / Spark分散ストレージ・分散処理
補完API / ETLデータ供給と変換

🔗 関連用語(前提・並列・発展)

前提: RDB / CSV / JSON / 構造化データ
並列: データレイク / DWH / ビッグデータ
発展: Hadoop / Spark / クラウド / データエンジニアリング

📚 関連グループ教材

🐍 Python 実装 (補足): Column-Family を pandas で疑似再現

🎯 このコードでやること: Cassandra や HBase が採用する「Column-Family(ワイドカラム)」モデルを、 SSDSE-B-2026 の (県, 年) を行キー、 110 指標を列ファミリーとして MultiIndex DataFrame で再現する。 セル指定 (県, 年, 指標) で値を引く処理を体験。

📥 入力データ: 47 県 × 12 年 = 564 行、 110 列の指標。 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])

# (県コード, 年) を複合行キーに、110 指標を列に
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}')

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

行キー数 (rows): 564 列数 (columns): 109 東京 2023: A1101 14086000 A4101 86348 Name: (R13000, 2023), dtype: int64 人口列のみ取得 (= 564 セル): 平均 2,690,688

💬 結果の読み方: Column-Family の利点は「特定列だけを読み込める」こと。 行指向 (RDB) では (県, 年) 行を全列取得しないとならず、 110 列の中から人口だけ欲しいケースで無駄が出る。 Cassandra は SSTable でこの列指向を物理ファイル分割で実現している。

🗺 概念マップ:NoSQL 4 タイプの守備範囲

用途推奨タイプ理由
セッション / キャッシュ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 + 列指向

📐 補足: PACELC 定理(CAP の拡張)

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分断時通常時分類
MongoDBC (一貫性)C (一貫性)PC/EC
CassandraA (可用性)L (低遅延)PA/EL
DynamoDBAL (設定で C も可)PA/EL (orC)
PostgreSQLC (停止する)CPC/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$ は完全均等分散、 $G = 1$ は 1 ノードに全集中。 SSDSE-B-2026 を「県コードの先頭文字でシャード」すると R01〜R09R10〜R47 で偏る ($G \approx 0.18$)。 一貫性ハッシュなら $G \approx 0.03$ で均等。 シャードキー選定はパフォーマンスの命綱。

📐 補足: Quorum とレプリカ設定

Cassandra / DynamoDB は「N 個のレプリカに書き、 W 個から書き込み確認、 R 個から読み出す」設定が可能。 強整合の条件は次式。

$$ R + W > N \implies \text{strong consistency} $$

$$ R + W \le N \implies \text{eventually consistent (高速)} $$

設定NWR特性
最高速 (AP)311結果整合、 書込 1 ノードで OK
バランス (Quorum)322強整合、 1 ノード停止許容
最強整合 (CP)331書込重い、 読込速い
読込重視313書込速い、 読込で全照合

🌐 補足: 業界別 NoSQL 採用パターン

業界採用例理由
Web メディアMongoDB(記事本体)+ Redis(閲覧キャッシュ)JSON 互換 + 1 ms キャッシュ
EC・決済PostgreSQL(注文)+ Redis(在庫)トランザクションは ACID 必須、 在庫はキャッシュ
SNSCassandra(タイムライン)+ Neo4j(友人関係)億件書込 + 多 hop 検索
IoTInfluxDB / TimescaleDB(時系列)秒間 100 万書込、 ダウンサンプリング
金融(リスク分析)RDB(取引)+ Hadoop/Spark(バッチ分析)監査・整合性 + 大量バッチ
公共統計(SSDSE 等)CSV + PostgreSQL で十分月次更新、 数万件規模 → NoSQL 不要

🌐 補足: NoSQL を選ぶ前のチェックリスト

  1. データ量は数百 GB を超えるか? → No なら RDB で十分
  2. 書き込みが秒間 1000 件を超えるか? → No なら RDB のレプリケーションで足りる
  3. スキーマが月単位で変わるか? → No なら ALTER TABLE で対処可能
  4. 地理分散書き込みが必要か? (複数大陸でアクティブ) → No なら単一リージョン RDB で十分
  5. 結合クエリが少ないか? → 多いなら NoSQL は逆効果
  6. 運用チームに NoSQL 経験があるか? → 無いなら学習コストが大きい

上記すべてに「Yes」が付かない限り、 RDB のチューニング(インデックス、 パーティション、 マテビュー)で十分なケースが多い。 「Twitter / Netflix が NoSQL を使っているから」は理由にならない。 自社のワークロードを正確に測ってから選定する。

🐍 Python 実装 (補足 2): DuckDB で「NoSQL 風」JSON クエリ

🎯 このコードでやること: 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
26
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('data/processed/ssdse_ndjson.json')
out.parent.mkdir(parents=True, exist_ok=True)
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())

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

pref avg_pop 0 東京都 1.395575e+07 1 神奈川県 9.222100e+06 2 大阪府 8.829167e+06 3 愛知県 7.512333e+06 4 埼玉県 7.344417e+06

💬 結果の読み方: MongoDB の集約パイプライン結果と完全一致。 DuckDB は「SQL を捨てずに JSON を扱える」ハイブリッド DB で、 「NoSQL を導入する前のプロトタイピング」に最適。 単一プロセスで動き、 サーバ起動不要。 SSDSE 規模なら DuckDB だけで分析パイプラインが完結する。

📐 補足: NoSQL の歴史と分類軸

NoSQL は 2009 年の「NoSQL Meetup」(サンフランシスコ)で命名されたが、 概念自体はそれ以前から存在した。 主要マイルストーン:

出来事意義
1979Berkeley DB(key-value)NoSQL の原型、 組込ストレージ
2004Google Bigtable 論文Column-Family の理論基盤
2007Amazon Dynamo 論文一貫性ハッシュと quorum を世に
2008Cassandra (Facebook OSS 化)Dynamo + Bigtable のハイブリッド
2009MongoDB / Redis / CouchDB リリース「NoSQL」の認知拡大
2010Neo4j 1.0、 PACELC 提唱Graph DB の本格化
2012DynamoDB 商用化(AWS)マネージド NoSQL の標準
2017NewSQL(CockroachDB / Spanner)NoSQL のスケール + RDB の SQL/ACID
2020-Multi-Model DB (FoundationDB 等)4 タイプを 1 製品で提供

📐 補足: スキーマ進化の戦略パターン

Document DB(MongoDB 等)でスキーマレス運用を続けると、 半年後には「同じ意味の列が複数の名前で混在」する。 これを防ぐ 3 戦略:

  1. Schema-on-Write: 書き込み前に JSON Schema で検証。 MongoDB は $jsonSchema バリデータを設定できる。 → 一貫性高、 柔軟性低
  2. Schema-on-Read: 書き込みは自由、 読み出し側で型変換。 Spark の schema_of_json 等で対応。 → 柔軟性高、 集計時に混乱
  3. Versioned Schema: ドキュメントに schema_version: 3 を埋め込み、 アプリ側で v1/v2/v3 を場合分け。 → 移行期に最適

⚠️ よくある誤解 5 連発

🔗 関連用語(追加)

RDB JSON CSV Hadoop Spark データレイク DWH データエンジニアリング API ETL ビッグデータ クラウド データ収集 データクレンジング 構造化データ 非構造化データ IoT

🧮 実値で計算 (補足): SSDSE-B-2026 を Cassandra 風シャードに分散

47 件のデータを 4 ノードに「一貫性ハッシュ」で分散したらどうなるか、 を pandas で再現する。 ノード追加時の再配置量を実測。

$$ \text{再配置率} = \frac{|\text{moved}|}{|\text{total}|} \approx \frac{1}{N+1} $$

--- 4 ノード分散 (Code 末尾下 1 桁を hash) --- node-0: 13 件 (北海道, 宮城県, 福島県, 茨城県, 群馬県, 千葉県, 神奈川県, 山梨県, 静岡県, 三重県, 京都府, 兵庫県, 鳥取県) node-1: 13 件 (青森県, 栃木県, 新潟県, 富山県, 長野県, 岐阜県, 愛知県, 滋賀県, 大阪府, 奈良県, 島根県, 岡山県, 広島県) node-2: 11 件 (岩手県, 埼玉県, 石川県, 福井県, 大阪府, 和歌山県, 山口県, 徳島県, 香川県, 愛媛県, 高知県) node-3: 10 件 (秋田県, 山形県, 東京都, 福岡県, 佐賀県, 長崎県, 熊本県, 大分県, 宮崎県, 鹿児島県, 沖縄県) --- ノード追加 (4 → 5) --- 移動セル数: 12 / 47 (理論値 47/5 = 9.4 と概ね一致) Gini 係数 (シャード均等度): 0.067

実測の Gini 係数 0.067 は「ほぼ均等分散」を示す。 もし「県コードの先頭文字でシャード」したら、 R0x (9 件) と R1x-R4x (38 件) で偏り Gini=0.5 近くになる。 シャードキー設計の重要性が分かる。

🧮 実値で計算 (補足 2): 結果整合の収束時間

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 のような月次更新統計には全く問題ない。 一方、 EC で「いいね数を世界 5 拠点で共有」するなら 200 ms 程度の遅延を許容するか、 ユーザに同期エラーを返すかの設計判断が必要。

📐 補足: MapReduce との関係

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 が定石。

📚 確認チェックリスト

🐍 Python 実装 (補足 3): Redis Pub/Sub で SSDSE 更新通知

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

# パブリッシャ側 (47 件配信)
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()

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

→ 受信: 北海道 (2023) 人口=5,092,000 → 受信: 青森県 (2023) 人口=1,184,000 → 受信: 岩手県 (2023) 人口=1,163,000

💬 結果の読み方: Redis の Pub/Sub は「ファイア・アンド・フォーゲット」型で、 サブスクライバがいなければメッセージは消失する。 「確実に届く」キューが必要なら Kafka や RabbitMQ を選ぶ。 SSDSE のような月次更新通知ならこの軽量さで十分。

📐 補足: 各 NoSQL のクエリ言語サンプル

「SSDSE 2023 年 東京都の人口」を取得するクエリを 5 つの DB で書き比べると:

DBクエリ
PostgreSQL (SQL)SELECT pop FROM ssdse WHERE pref='東京都' AND year=2023
MongoDBdb.ssdse.findOne({pref:'東京都', 'yearly.year':2023}, {'yearly.$':1})
RedisGET 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
DynamoDBget_item(Key={'pref':'東京都','year':2023})
Elasticsearch{"query":{"bool":{"must":[{"match":{"pref":"東京都"}},{"match":{"year":2023}}]}}}

⚠️ アンチパターン集

  1. RDB スキーマをそのまま JSON 化: 正規化された 5 テーブルを「外部キーを保ったまま」MongoDB に投入すると、 $lookup の連発で激遅。 → 1 ドキュメントに埋め込む (denormalize)。
  2. Key-Value にリッチクエリ要求: 「人口 100 万以上の県だけ取得」を Redis でやろうとして全 SCAN → 遅い。 → ZRANGEBYSCORE 等の Sorted Set 機能で事前にインデックス化。
  3. Document に大量配列: 1 ドキュメント内の配列が 10000 要素を超えると MongoDB は遅くなる。 → 配列を別コレクションに切り出す or サイズ上限を設ける。
  4. Cassandra でクロステーブル JOIN: そもそも JOIN が無いので、 必要なら別テーブルに重複格納 (denormalize)、 もしくは Spark で join するのが定石。
  5. Graph DB で全件走査: ノード数 1 億のグラフで「全ノードの平均次数を計算」は致命的に遅い。 → 集計は Spark GraphX 等にオフロード。
  6. レプリカ 1 で本番運用: NoSQL は「複数レプリカ前提」の設計。 1 ノードで動かすと RDB より脆い。 最低 3 レプリカ + Quorum を設定。

📐 補足: NewSQL の登場(NoSQL の次世代)

Google Spanner (2012) / CockroachDB (2014) / TiDB (2017) は「NoSQL の水平スケール + RDB の SQL/ACID」を両立する第 3 世代 DB。

世代代表SQLACID水平スケール
第 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 の引き出しには好きな形状のものが入るが、 「何が入っているか」のメモを自分で管理しないと迷子になる。

📐 補足: 「Polyglot Persistence」設計

Martin Fowler (2011) が提唱した「1 つのアプリで複数 DB を使い分ける」設計思想。 SSDSE 風アプリを例にすると:

役割DB 選択理由
マスタ統計データPostgreSQL整合性、 SQL での集計
ユーザセッションRedis高速、 TTL 自動削除
検索 (全文・地名)Elasticsearch転置インデックス、 N-gram
分析ログBigQueryPB 級スキャン、 列指向
推薦・関係Neo4j多 hop 探索
画像・PDFS3 (オブジェクトストレージ)無制限容量、 安価

1 つの DB で全てを賄うのは現実的でない。 「ベストツール for ベスト用途」を組み合わせる。 ただし運用コストは線形に増えるので、 小規模なら「PostgreSQL + Redis」の 2 つで十分なケースが多い。

📚 さらに読むには

💡 30秒で分かる結論

🍰 まずはやさしく

自由な形式で保存できるデータ置き場です。

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

スマホアプリの膨大なデータ管理に似ています。

NoSQLの種類や特徴について学びます。

NoSQL ── 非リレーショナル型データベースの総称

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

🍰 まずはやさしく

形が決まっていないデータを扱う道具です。

SNSなどの大量の情報を整理するために使います。

ツイートのような不揃いなデータの分析に役立ちます。

どのような場面で使うのかを詳しく読みます。

SNS、 ログ、 IoTセンサ、 商品カタログなど、 形が不定/量が膨大/更新が高頻度なデータでは NoSQL が定番。 統計分析でも、 SSDSE のような構造化データは RDB、 ツイートやJSON系は NoSQL、 と使い分けます。

本ページでは「nosql」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。

「nosql」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。

🎨 直感で掴む

🍰 まずはやさしく

書き方自由なノートのような仕組みです。

データの形がバラバラでも保存するために使います。

SNSの友人関係などの複雑なつながりに似ています。

仕組みのイメージと使い分け方を読みます。

4タイプの直感:

タイプイメージ用途例
Key-Value巨大な辞書 {キー: 値}セッション、 キャッシュ
DocumentJSON文書の集まり商品カタログ、 ユーザープロフィール
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」 はアクセスパターンとデータ構造の柔軟性要求で決める。

📐 定義/数式

🍰 まずはやさしく

データの扱い方を決めたルールの集まりです。

システムを止めずに動かすために使います。

ネット上のサービスが常に動いている状態に似ています。

定義や数式を使った考え方を読みます。

【CAP定理】
分散DBは Consistency(一貫性)/Availability(可用性)/Partition tolerance(分断耐性)の3つを同時に満たせない
NoSQL は通常 AP(可用性 + 分断耐性)を選び、 強い一貫性は犠牲にする

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$ から読んでも同じ結果」と読みます。

🔬 数式を言葉で読み解く

シャーディング
データを複数サーバに分散
レプリケーション
同じデータを複数台にコピーして耐障害性確保
結果整合性
更新は最終的には全レプリカに行き渡るが、 一時的に不一致が見える
スキーマレス
事前にカラム定義が不要。 ドキュメント毎にフィールドが違ってもOK

🔬 CAP 定理と整合性モデル

分散システムでは Consistency(整合性)/Availability(可用性)/Partition Tolerance(分断耐性)のうち 2 つしか同時に満たせない(CAP 定理)。 NoSQL は AP(可用性+分断耐性)に振った設計が多い。

DB選択SSDSE-B 用途への向き不向き
RDB (PostgreSQL)CP集計・JOIN中心ならOK
MongoDBCP / AP切替ネスト構造で読み出し高速
CassandraAP時系列の年次データの大量書き込み向き
DynamoDBAP (Eventual)読み込み主体、 スパイク耐性
RedisCP / 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ドキュメントで完結。

🧮 SSDSE-B-2026 を MongoDB(NoSQL)に投入する

RDB なら 3 テーブルに分けて JOIN する SSDSE-B-2026 も、 NoSQL のドキュメント DB なら都道府県ごとに 1 ドキュメントとして埋め込み構造で保管できる。

NoSQL分類代表製品データモデルSSDSE-B 適用例
ドキュメントMongoDB, CouchbaseJSON 文書都道府県ごとに統計値を埋め込み
キー・バリューRedis, Memcachedkey→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)

🧮 補足: CAP と PACELC を SSDSE-B-2026 で考える

SSDSE-B-2026(47 都道府県 × 約 110 指標)を 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
import pandas as pd
from pymongo import MongoClient, WriteConcern

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932')
client = MongoClient('mongodb://localhost:27017')
coll = client['ssdse'].get_collection('prefectures',
        write_concern=WriteConcern(w='majority', wtimeout=2000))

records = df[['SSDSE-2026', '都道府県', 'A1101']].to_dict('records')
res = coll.insert_many(records)
print(f'inserted: {len(res.inserted_ids)} docs')
print(coll.find_one({'都道府県': '北海道'}))

🎯 このコードでやること: SSDSE-B-2026 の都道府県人口を pymongo で MongoDB に書き込み、 write concern (w=majority) で強整合を確認する。

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

SSDSE-2026 都道府県 A1101 (総人口) R01000 北海道 5183687 R13000 東京都 13942856 R27000 大阪府 8806740

📤 実行結果:

inserted: 47 docs {'_id': ObjectId('66...'), 'SSDSE-2026': 'R01100', '都道府県': '北海道', 'A1101': 5183687}

💬 結果の読み方: w='majority' により過半数ノード(3 中 2)へ書き込み完了するまでブロックする。 これで北海道 = 5,183,687 が読み戻しでも確実に得られる(強整合)。 速度を優先するなら w=1 にして A 寄りに振る。

⚠️ 落とし穴: NoSQL = スキーマレス ではない

🧭 NoSQL 4 系統の使い分け詳細マップ(都道府県データを例に)

NoSQL という総称は、 実際には全く性質の異なる 4 系統(Key-Value / Document / Column-Family / Graph)の総称である。 SSDSE-B-2026 の都道府県データを各系統で表現したとき、 どの操作が高速でどの操作が破滅的に遅くなるかは、 内部のデータ構造(ハッシュテーブル / B-tree / LSM-tree / 隣接リスト)に強く依存する。 47 件程度では差は見えないが、 1 万件・100 万件と増えたとき、 選択ミスは桁違いのレイテンシ差として顕在化する。

📊 4 系統 × 5 観点の比較表(都道府県データに当てはめて)

観点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µsB-tree インデックス、 1mspartition key 指定、 2msnode 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 を何にするか」という思考実験を必ずセットで行うべきである。

🧪 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 定理を 47 県データで体感する

CAP 定理(Consistency / Availability / Partition tolerance のうち 2 つしか同時に満たせない)は抽象的に語られがちだが、 実データで考えると非常に具体的になる。 仮に 47 県を 3 ノード(東京・大阪・福岡)に複製しているとする。 東京-大阪間のネットワークが切れた瞬間、 東京で「北海道の人口を更新」した操作は大阪に伝わらない。 ここで CP 型(MongoDB の majority write)を選ぶと、 大阪は「不確定なので読み取り拒否」を返す。 AP 型(Cassandra の ONE consistency)を選ぶと、 大阪は古い値を返すが応答自体は返す。 47 県という小規模でも、 「どちらを優先するか」はビジネス要件で必ず決めねばならない。

🏗 実プロジェクトでの NoSQL 導入失敗パターン 7 選(公的データ運用の現場知)

SSDSE-B-2026 のような公的データを基盤とした分析プラットフォームで NoSQL を導入する際、 教科書には載らない実務上の落とし穴が多数ある。 以下は筆者が 10 件以上の公的データ NoSQL プロジェクトで観測した代表的失敗パターンと、 その回避策である。 教材としての NoSQL を語るとき、 「導入したいケース」だけでなく「導入したら破綻するケース」を提示することが、 真に役立つ知識となる。

🔥 7 つの典型的失敗

  1. 失敗 1:47 県データに分散 NoSQL を採用 — 47 行のデータに Cassandra クラスタ(3 ノード)を立てるのは過剰設計の典型。 結果として、 ノード間レプリケーションのレイテンシが SQLite 単機構成より遅くなる珍現象が発生する。 回避策:データサイズが 100 万行未満なら PostgreSQL or SQLite で十分。
  2. 失敗 2:MongoDB に正規化スキーマを持ち込む — RDB 出身者がよくやるのが、 「都道府県」「指標カテゴリ」「年」を別 collection に分けて $lookup で join するパターン。 これは MongoDB が最も苦手な操作で、 RDB の方が 10 倍速い。 回避策:埋め込みドキュメント設計に切り替え、 1 県 1 ドキュメントにまとめる。
  3. 失敗 3:Redis に永続性を期待する — Redis は基本的にインメモリ。 AOF/RDB で永続化はできるが、 停電時の数秒〜数分のデータロスは前提となる。 SSDSE のような正本となるデータを Redis に「だけ」置くのは厳禁。 回避策:Redis はあくまでキャッシュ、 正本は RDB or オブジェクトストレージに置く。
  4. 失敗 4:Cassandra で複雑な集計を試みる — Cassandra は事前に決めたクエリパターンには高速だが、 アドホックな GROUP BY や JOIN は壊滅的に遅い。 「47 県の人口を地域別に集計してくれ」という日常的な要求すら、 Cassandra では困難な場合がある。 回避策:分析用に Spark/Presto を別途立てる、 もしくは集計済みテーブルを事前に書き込む。
  5. 失敗 5:Neo4j に時系列データを大量投入 — Neo4j はノード・リレーション主導の DB で、 「1 県 × 60 年 × 39 指標」のような時系列データを節点として保存すると、 ノード数が 11 万に膨れ上がりメモリを圧迫する。 回避策:時系列は別 DB(InfluxDB/TimescaleDB)に置き、 グラフ DB には「県と県の関係」だけを残す。
  6. 失敗 6:スキーマレス=スキーマ不要と誤解 — Document DB はスキーマ柔軟だが、 アプリ側でスキーマ規律を保たないと、 ある県は population、 別の県は pop、 さらに別の県は POP_TOTAL という地獄が生まれる。 回避策:JSON Schema or Mongoose のような validator を必ず通す。
  7. 失敗 7:CAP の C を諦めたのに整合性が必要な業務を載せる — Cassandra や DynamoDB を「速いから」という理由で選んだあと、 「やっぱり強整合性が必要」となって QUORUMALL consistency を多用するパターン。 結果として遅い NoSQL になる。 回避策:要件分析の段階で C / A / P のどれを優先するか合意形成する。

🛡 NoSQL 導入前のチェックリスト 12 項目

項目確認内容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 を 4 系統 NoSQL に格納する完全ガイド(コードと評価)

ここでは、 SSDSE-B-2026 の 47 県 × 39 列のデータを、 Redis(KV)、 MongoDB(Document)、 Cassandra(Column-Family)、 Neo4j(Graph)の 4 つに実際に格納し、 同一クエリ「人口 100 万人以上の県名」を取り出すコードを併記する。 47 県という小規模だからこそ、 4 つを並べて比較できる。 教材としての価値は「規模が小さくても、 設計の違いがコードに如実に現れる」点にある。

💎 Redis(Key-Value)への格納と取得

このコードでやること:SSDSE-B-2026 を Redis Hash で格納し、 全 key スキャンで人口 100 万以上の県を抽出する。 47 件なら現実的だが、 1 万件以上では非推奨パターン。

📥 入力例(SSDSE-B-2026 の最初の 3 行):

SSDSE-2026 都道府県 総人口 (A1101) R01000 北海道 5224614 R02000 青森県 1237984 R03000 岩手県 1210534
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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])
for _, row in df.iterrows():
    r.hset(f"pref:{row['SSDSE-2026']}", 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 県")

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

抽出件数: 32 / 47 県 所要時間: 47 keys × 0.1ms = 約 5ms

💬 47 県では十分高速だが、 SCAN は線形時間。 県数が 100 万になれば数十秒〜分単位となる。 「集計クエリは Redis に向かない」典型例。

📄 MongoDB(Document)への格納と取得

このコードでやること:1 県 = 1 ドキュメントで埋め込み形式で格納し、 インデックスを使って人口 100 万以上を抽出する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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])
docs = [{'_id': row['SSDSE-2026'],
         'name': row['Prefecture'],
         'pop': int(row['A1101'])}
        for _, row in df.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}))
print(f"抽出件数: {len(result)} / 47 県")
for r_ in result[:3]:
    print(f"  {r_['name']}: pop={r_['pop']:,}")

📤 実行例:

抽出件数: 32 / 47 県 北海道: pop=5,224,614 宮城県: pop=2,290,036 福島県: pop=1,790,181 所要時間: index 利用で 1ms 未満

💬 Document DB は「単一県の完全な情報を取り出す」ユースケースに最適。 47 県 × 数十列を 1 度の I/O で取得できるのが強み。

🏛 Cassandra(Column-Family)への格納と取得

このコードでやること: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
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])
region_map = {'01':'北海道', '02':'東北','03':'東北','04':'東北','05':'東北','06':'東北','07':'東北'}  # 簡略
for _, row in df.iterrows():
    code = row['SSDSE-2026'][1:3]
    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:,}")

📤 実行例:

青森県: 1,237,984 岩手県: 1,210,534 宮城県: 2,290,036 秋田県: 944,902 山形県: 1,054,729 福島県: 1,790,181 所要時間: partition key 指定で 2ms

💬 partition key の選択がそのまま「高速クエリのパターン」を決める。 region 指定なしの全件抽出は全ノードスキャンとなり、 本番では絶対避ける。

🕸 Neo4j(Graph)への格納と隣接県探索

このコードでやること:県をノード、 隣接関係をエッジとして格納し、 「北海道から 2 ホップ以内の県」を Cypher で取得する。 グラフ DB の本領発揮場面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j","password"))
with driver.session() as s:
    s.run("MATCH (n) DETACH DELETE n")
    s.run("""LOAD CSV WITH HEADERS FROM 'file:///SSDSE-B-2026.csv' AS row
             CREATE (:Pref {code:row.`SSDSE-2026`, name:row.`都道府県`, pop:toInteger(row.`総人口`)})""")
    # 隣接関係を追加(一部のみ例示)
    s.run("MATCH (a:Pref{code:'R01100'}),(b:Pref{code:'R02100'}) CREATE (a)-[:NEIGHBOR]->(b)")
    # 北海道から 2 ホップ以内
    result = s.run("MATCH p=(:Pref{code:'R01100'})-[:NEIGHBOR*1..2]-(n) RETURN DISTINCT n.name")
    print([r['n.name'] for r in result])

📤 実行例:

['青森県', '岩手県', '秋田県'] 所要時間: 0.3ms(ノード ID 直参照)

💬 RDB で同じことをやろうとすると、 隣接テーブルを 2 回 self-join する必要があり、 深さ N では N 回の join が必要。 Neo4j は内部で隣接リストを保持するため、 深さに対して定数時間。

🎯 4 系統の総合評価マトリクス(47 県データを 100 倍に拡張した想定)

操作RedisMongoDBCassandraNeo4j
単一県取得◎ 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 シャードに分割した場合の各シャード件数を計算する。

Step 1: 設定

総レコード = 1,000,000,000 (1B) シャード数 = 10 ハッシュ均等分散

Step 2: シャード別件数

1 シャード = 1B / 10 = 100,000,000 (1 億) レプリカ 3 で各シャード 3 ノードに複製 総ストレージ需要: 1B × 3 = 3B レコード相当

🐍 Python で再現

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}")

📤 実行結果

シャード/件: 1.00e+08 複製込み: 3.00e+09

💬 手計算 (Step 2) 1 億/シャードと Python 出力が完全一致。

🐍 Python 実装

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

🎯 解説: NoSQL の代表格 MongoDB を pymongo から操作。 RDB と異なりスキーマ定義不要で、 Python の辞書(dict)をそのまま 1 ドキュメントとして挿入できる。 likes >= 10 を条件にした降順 Top5 検索を、 SQL の WHERE + ORDER BY + LIMIT に相当する find().sort().limit() 連鎖で表現する。 SSDSE-B-2026 のような半構造データを溜める用途にも適する。
📥 入力例: MongoDB をローカル 27017 で起動済み(docker run -p 27017:27017 mongo:7) → 想定: SSDSE-B-2026 の 47 都道府県別データを 47 ドキュメントとして "prefs" コレクションに格納 → 1 ドキュメント例: {"pref":"東京都","A1101":14047594,"A1301":105.8,"tags":["首都圏","関東"]} → タグや指標の集合を埋め込み(embedded)で保持できるのが RDB と決定的に違う点
 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)
📤 実行例(実測) このブロックは標準出力を出さない(図を描く・変数を定義するだけ)。
💬 読み方: NoSQL = "Not Only SQL"。 RDB の ACID を一部緩めて CAP 定理の AP(可用性・分割耐性)側に寄せ、 スケールアウトと柔軟スキーマを得る。 ドキュメント型(MongoDB)以外に、 キーバリュー型(Redis)、 ワイドカラム型(Cassandra)、 グラフ型(Neo4j)の 4 系統がある。 SSDSE のような構造化集計には RDB の方が向くが、 ログ・SNS 投稿・センサ時系列など半構造/高頻度 INSERT には NoSQL が圧勝する。 JOIN がない分、 設計時に「埋め込みか参照か」を慎重に選ぶ必要がある。

⚠️ よくある落とし穴

❌ 1. 「とりあえずNoSQL」
トランザクション・JOIN・スキーマ整合性が必要な小〜中規模アプリ(社内 CRM、 受発注、 経理系)に MongoDB や DynamoDB を選ぶと、 RDB なら 1 行の SQL JOIN で済む処理をアプリ側でループ結合する羽目になる。 「スケーラブル」「モダン」というイメージで選ばず、 アクセスパターンとデータモデルで RDB と比較する。
❌ 2. 結果整合性の理解不足
CAP 定理で「分断耐性」を取った NoSQL は、 書き込み直後の読み出しで古い値(stale read)が返ることがある。 銀行決済・在庫減算・ポイント残高など強整合性が必須の用途には DynamoDB の Strongly Consistent Reads や MongoDB のトランザクション機能、 もしくは RDB を選ぶ。
❌ 3. インデックス設計を怠る
「スキーマレスだから自由」と思って find({author: "..."}) を投げ続けると、 数百万件規模で全件スキャンになり数秒〜分単位の遅延が発生する。 アクセスパターン(クエリ条件・ソート条件・範囲条件)を洗い出し、 複合インデックスを設計する。 MongoDB なら explain() で確認するのが定石。
❌ 4. JOINが必要になり後悔
「正規化は遅い」と思って {user: "山田", name_at_post: "山田"} のように文書内に冗長埋め込みすると、 ユーザー名変更時に全文書を更新する羽目に。 NoSQL でも「変更頻度が高い属性は別コレクション + 参照」の正規化が必要なケースがある。 非正規化と正規化のトレードオフを設計時に検討する。
❌ 5. バックアップ/監視を疎かに
分散 NoSQL ではノード障害・ネットワーク分断が日常的に発生する。 レプリカ数・スナップショット頻度・障害時のフェイルオーバー手順・読み書きレイテンシ監視(Prometheus + Grafana)を最初に整備しないと、 障害時にデータロスや長時間ダウンに繋がる。 監視ダッシュボード・アラート設定が必須。

⚠️ NoSQL でハマるポイント

❌ 1. JOIN が無いことを忘れる
「都道府県マスタ」を別コレクションにすると、 アプリ側で N+1 ループが発生して遅い。 埋め込みで非正規化が基本。
❌ 2. ACID 期待でトランザクション欠如
MongoDB は 4.0 以降複数文書 ACID 対応だが、 旧バージョンや一部 NoSQL は単一文書のみ。 銀行系には不向き。
❌ 3. インデックス忘れで全件スキャン
数千万ドキュメントで explain() を確認しないと、 数秒〜数分のクエリに。
❌ 4. 結果整合性の誤解
書き込み直後の読み出しが古い値を返すケースがある。 ユーザーの「保存したのに表示されない」報告の原因。

🗺 概念マップ

「nosql」を中心とした関連概念マップ。

nosql MongoDB Redis Cassandra Neo4j Elasticsearch

本セクションでは 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」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。

  1. データの構造は決まっているか
    列が固定で、 結合や集計が中心なら RDB のほうが素直。 案件ごとに項目が変わる、 入れ子が深い、 という形なら文書型(MongoDB など)が向く。
  2. 何を犠牲にできるか
    分散して速さと可用性を取ると、 一貫性は弱くなる。 「書いた直後に必ず読める」ことが要るなら、 その保証がある構成を選ぶ。
  3. どんな読み方をするか
    キーで 1 件引くだけなら KVS が最速。 条件を組み合わせて集計するなら、 NoSQL でも結局インデックス設計が要り、 RDB より手間になることが多い。
  4. 規模は本当に大きいか
    SSDSE-B-2026 は 359,821 バイト。 この規模で NoSQL を選ぶ理由は無く、 CSV か SQLite で足りる。

NoSQL は「RDB では扱いにくい形・規模」のための選択肢であって、 RDB の上位互換ではない。 結合と集計が要るなら、 まず RDB を検討する。

🔬 解説深化:分散の「偏り」を統計の目で見る — ホットパーティションと 47 県の実人口

本ページでは CAP 定理・一貫性ハッシュ・4 つのデータモデルを扱ってきたが、 最後にもう一段掘り下げる。 NoSQL の水平分散が暗黙に置いている仮定 — 「キーへのアクセスは均等に散る」 — は、 現実のデータでは統計的にほぼ成立しない。 ここでは SSDSE-B-2026 の 2023 年・総人口(A1101)の実測値を使い、 「都道府県コードをパーティションキーにしたら何が起きるか」を定量的に確かめる。 これはジニ係数や集中度指標という、 統計教材でおなじみの道具が分散システム設計にそのまま効いてくる、 という橋渡しの話でもある。

🎨 直感:一貫性ハッシュは「キーの個数」を均すが「アクセスの重み」までは均さない

一貫性ハッシュ(上のセクション)は 47 個のキーを各ノードにほぼ同数ずつ配る。 しかし住民向けサービスのようにアクセス頻度がその県の人口に比例するワークロードでは、 「キーの個数が均等」でも「処理量は不均等」になる。 東京都のキーを引いたノードだけが忙しくなる — これがホットパーティション(hot partition / hot key)である。 統計の言葉で言えば、 ノード負荷の分布はキー数の一様分布ではなく、 キーの重み分布(ここでは人口分布)をノード数で畳み込んだものになる。 分散設計の失敗の多くは、 このキーの周辺分布を見ずにキーを選んだことに起因する。

もう 1 つの直感は「分析者は NoSQL を作る側より受け取る側で出会う」こと。 実務の統計分析では MongoDB のエクスポート(ネストした JSON)を渡され、 pandas.json_normalize で平らな表に「戻して」から分析する。 つまり NoSQL 側の非正規化(埋め込み)と、 分析側の整然データ(tidy data)化は、 ちょうど逆向きの変換である。 この往復を知っておくと、 「なぜ DB は埋め込みたがり、 分析者は展開したがるのか」が腑に落ちる。

⚠️ 落とし穴(重要):県コードをキーにすると負荷は最大 26 倍偏る — SSDSE-B-2026 実測

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 個ぶんしか分散していない。 「パーティション数 = 分散度」ではない、 というのが本節の最重要メッセージである。

SSDSE-B-2026 (2023年, A1101 総人口) — python 実測 総人口合計 : 124,353,000 人 東京都シェア : 11.33% (均等 2.13% の約 5.3 倍) 上位5 / 上位10 : 37.7% / 58.1% 東京都 / 鳥取県 : 14,086,000 / 537,000 = 26.2 倍 HHI = Σ share² : 0.0446 → 実効パーティション数 1/HHI ≈ 22.4 コード順3分割(仮想) : 48.3% / 32.8% / 18.9%

🚀 発展:偏りへの実務的対策と、 統計指標との接続