NoSQL — スキーマが消えたのではなく責任が移った
一言でいうと
NoSQLはリレーショナルの上位互換ではなく、特定の制約を手放して特定の能力を得る取引であり、何を手放したのかを知らずに選ぶと、その代償を後で払うことになります。
なぜ必要なのか
リレーショナルデータベースは強力ですが、いくつかのことを前提にします。スキーマがあらかじめ決まっていて、データが1台に収まるか、少なくとも結合が可能な範囲内にあり、強い一貫性が必要だという前提です。
2000年代後半、Webサービスがこの前提を1つずつ守れなくなり、別の選択肢が登場しました。核心的な動機は水平スケーリングでした。1台をより大きくする代わりに、複数台に分けるには、結合やトランザクションのような機能が障害になり、それを手放した設計が登場しました。
どう動くのか
分類ごとの性格は、次のように分かれます。
| 分類 | データモデル | 強み | 代償 |
|---|---|---|---|
| キーバリュー | キー1つに値1つ | 単純な照会が非常に速い | 値の内部にクエリできない |
| ドキュメント | JSONに似たドキュメント | スキーマが柔軟、1つのドキュメントにすべて入れて照会できる | ドキュメント間の結合と整合性はアプリの責任 |
| ワイドカラム | 行キー + カラムファミリー | 大量の書き込み、時系列 | クエリパターンを事前に決めて設計する必要がある |
| グラフ | ノードとエッジ | 関係の探索 | 大量の集計は弱い |
ここで、最も広まった誤解を押さえておく必要があります。「スキーマがない」という言葉は、スキーマがなくなったという意味ではなく、スキーマを強制する主体が、データベースからアプリケーションに移ったという意味です。フィールド名をタイプミスしたドキュメントが静かに入り、数か月後に、そのフィールドを読むコードが静かに失敗します。データベースが防いでくれていたものを、今後はコードレビューとテストが防がなければなりません。
CAPもよく誤用されます。ネットワーク分断が実際に起きた瞬間に、一貫性と可用性のどちらを選ぶかという問題であり、平常時に3つのうち2つを選ぶという話ではありません。分断がないときは、ほとんどのシステムが両方を提供します。
現場での姿
選択の基準は流行ではなく、クエリパターンです。いくつかの実用的なサインがあります。
- 複数のエンティティを組み合わせて見るクエリが頻繁に変わるなら、リレーショナルが依然として有利です。ドキュメントストアは、保存するときに決めた形でだけうまく読めます。
- 一度書いて、その形のまま頻繁に読むなら、ドキュメントストアが自然です。
- キャッシュやセッションのように、キーだけでアクセスするなら、キーバリューストアが合います。
- 関係の深さをたどるクエリ(友達の友達、権限の継承)なら、グラフが圧倒的に優れています。
そして、現代のリレーショナルデータベースが、かなりの部分を吸収したという点も考慮する必要があります。PostgreSQLのjsonbは、ドキュメントストアの柔軟性をかなり提供しながら、トランザクションと結合を維持します。ただし、乱用してはいけません。照会や並べ替え、制約が必要なフィールドはカラムに切り出し、残りの元データだけをjsonbに残すのが、実用的な境界です。jsonbの中の値には正確な統計がなく、プランナーの行数推定がずれ、その結果、結合方式の選択が狂うからです。
選ぶ前に答えるべき5つの質問
分類ごとの性格を暗記するよりも、今作ろうとしているものについて、次の5つを先に書き出してみるほうが、はるかに早く答えにたどり着けます。
- どんなクエリで読むのか。キー1つで探すのか、条件で絞るのか、関係をたどるのか。ドキュメントストアとワイドカラムは、保存するときに決めたアクセス経路でだけうまく読めるので、この答えが「まだわからない」なら、リレーショナルが安全です。
- データがどれくらい大きくなるのか。1台に収まる大きさなら、分散の代償を払う理由はありません。最近のサーバー1台は、数テラバイトを扱います。
- 一緒に変わる必要があるものは何か。このリストが長ければ、トランザクションが必要だという意味です。
- 古い値を少しの間見せてもよいか。閲覧数やおすすめリストなら問題ありませんが、残高と在庫はいけません。
- 誰が運用するのか。新しいストアは、新しいバックアップ手順、新しいメトリクス、新しい障害の種類を一緒に連れてきます。
5つの質問に答えてみると、ほとんどの場合、結論が1つに集まります。最初はリレーショナル1つで始め、特定のクエリパターンがはっきりと痛くなったら、その部分だけを別のストアに切り離すことです。セッションはキーバリューストアに、全文検索は検索エンジンに移す、といった具合です。逆の順序、つまり最初から複数のストアを敷いて始める方式は、まだ存在すらしない問題に、運用の負担から先に払う選択です。
分散になった瞬間に変わること
1台で動いていたデータベースが複数台に分かれると、慣れ親しんだ性質がいくつか、静かに消えます。これを知らずに移すと、コードはそのままなのに、結果だけがおかしくなります。
1つ目は、書いた値をすぐに読めない場合があることです。レプリカが複数あるストアでは、書き込みは一方に、読み取りは他方に行くことがあります。プロフィールを保存してすぐに照会する画面が、古い値を見せてしまう事故は、ここから生まれます。ほとんどのストアは、「たった今自分が書いたものは自分が読める」をオプションとして提供しているので、そのオプションをオンにするか、書き込み直後の読み取りだけをプライマリに送れば済みます。オプションの名前は製品ごとに違いますが、代償は同じです。その読み取りは遅くなります。
2つ目は、複数のドキュメントを一度に変えるのが難しくなることです。リレーショナルではトランザクション1つで終わっていた作業が、ドキュメントが別々のノードに散らばっていると、部分的にしか成功しないことがあります。そのため、ドキュメントストアは、一緒に変わる必要があるものは、同じドキュメントに入れるという原則で設計します。注文と注文明細を1つのドキュメントに入れる理由が、性能ではなく原子性だという点が重要です。
3つ目は、シャードキーを後から変えられないことです。データをどのノードに送るかを決めるキーは、事実上、元に戻せない決定です。誤って選ぶと、特定のノードにだけ負荷が集中し(ホットシャード)、そのノードを増やしても解決しません。代表的な失敗は、時刻をシャードキーに使うことです。最近のデータがすべて1つのノードに集中して、ノードを10台に増やしても、9台が遊びます。
まとめると、次のとおりです。リレーショナルからNoSQLに移す決定は、ストアを選ぶことではなく、データベースが代わりに守ってくれていたルールのうち、何をアプリケーションが自分で守るのかを決めることです。そのリストを書き出さずに移ると、移った後に、1つずつ事故として発見することになります。
続くクイズで確認すること
各分類が何を手放し、何を得るのか、そして「スキーマなし」が実際に何を意味するのかを説明できるか確認します。