Open10

NoSQL のデータ設計を調べて理解が深まったこと

ryokkkkeryokkkke

概要

NoSQL、今までしっかりと使ったことがなくて、実際ちゃんと設計しようとすると色々疑問が出てきました。

  • 非正規化どこまでやれば良いの?
  • というかそもそも非正規化するもの?
  • ベストプラクティスがわからん...

ので、ググってみたところとっても良い文献にすぐに当たることが出来て(自分が特別 DB に強いわけではないというのもあり)気づきが多かったのでまとめておきます。
NoSQL 設計の基本的な内容なので、既に運用している方には何を今更、な内容だと思います。

「恐らく今 NoSQL を使うと良さそう、だけどあんまり知見がない」な方には理解を深めるお役に立つかもしれません。

全体的にふわっとしてます。運用していく中で認識が変わる部分も多いと思います。参考程度に。

主には GCP Datastore (Datastore モードの Firestore)を使うことを前提としているので、ドキュメントストアに着目してます。

ryokkkkeryokkkke

先に結論

結論としての今の理解を先に書くと

  • NoSQL において非正規化はごく一般的なモデリング手法の一つ。
  • NoSQL は RDB と同じように(あるいはRDB よりもさらに)、最初の設計が肝。データの客観的な整理整頓よりも、よりアプリケーション目線に立った(=アプリケーションから見て主観的に)、場面ごとのクエリを重視する設計をすることになる。
ryokkkkeryokkkke

以降気づきの連投。

非正規化にとって正規化は必須プロセス

https://atmarkit.itmedia.co.jp/ait/articles/1103/25/news131_3.html

しかし、テーブルとテーブルの関係を整理しないまま安易に作成したデータ構造と、いったん正規化を済ませたうえで、目的に合わせて冗長化したデータ構造はまったく違うものです。データ構造の正規化は決してゴールではありませんが、設計の1プロセスとして正規化を経由した方がいいのは間違いありません。

こちら、個人的に非常に納得感のある文章でした。
つまり、スキーマレスだからと言って適当にデータを放り込んでOKということではなく、きちんと設計した上で適切な構造を作ること。 そして「きちんと設計する」際ににとても重要なプロセスとして正規化が存在するということ。

ryokkkkeryokkkke

join のタイミングの違いという観点

RDB に慣れていると正規化こそが真っ当な道で、「まずは正規化したテーブルで運用する。そこから本当にアプリケーション要件的に問題が出てきたら必要に応じて正規化を崩すことも視野に入れる」というような意識が(特に開発を短いスパンで回し、サービス仕様が割とスピーディーに変わる現場において。そして設計時にまだ DB のパフォーマンスボトルネックになりうる要素が判明していない場面において)あると思うんですが。

NoSQL における設計は、基本的にはその正規化を崩す作業を最初の設計時から既に行うことが多くなりそうですね。なぜなら NoSQL では基本的に DB 側に実行時の集計機能、つまり join が存在しないから。

https://gist.github.com/matope/2396234#3アプリケーションサイドjoin-application-side-join

"知りたいこと指向"なNoSQLの性質の結果として、joinはしばしば、リレーショナルモデルがクエリ実行時に実行されるのに反し、設計時に行われることになる。

これもものすごく分かりやすい表現だなと思いました。
つまり、RDBは実行時に join するが、 NoSQL では設計時に join する、というイメージ。

正規化 + 実行時 join は非常に多くのケースにまんべんなく対応できる側面がありますが、それは DB 側の処理と実行時クエリに負担を寄せているという側面があります。(この多くのケースに対応できるという特徴がRDBを今の地位に押し上げた一つの要因でもあるでしょう)
それに対して 非正規化(ここではニアリーイコール設計時 join)は実行時の自由を相対的に削る代わりにパフォーマンスをあげる という選択ができるのが NoSQL 設計におけるメリットであり、難しさでもあるということですね。

ryokkkkeryokkkke

クエリ重視

そうなってくると、次に気になるのは「一体このアプリケーション(もしくは機能)って、どういうデータの扱い方するんだっけ?」ということです。

まあもちろん、RDB だって設計時にそれも考えるんですけどね...
NoSQL の場合最初から非正規化することが前提な節はあるので、RDB よりも早期に「この機能で行うデータの処理(CRUD)はどういう特徴があるか」を考える必要が出てきます。

https://gist.github.com/matope/2396234#nosqlデータモデリングの一般的な注意

NoSQLデータモデリングはリレーショナルモデリングと異なり、しばしばアプリケーション特有のクエリからスタートする。

そう、基本的に RDB の場合はそのドメインのデータとそれぞれの関係性を正確に DB 設計に反映させるという側面があると思うのですが、NoSQL はもっと現場主義で、ドメインレイヤーを通り過ぎて実装のレイヤーで思考する必要がありますね。
RDB との比較という話で言うと、パフォーマンスを理由に選択されることが多いと思うのである意味では当然っちゃ当然でしょうか。

リレーショナルモデリングは利用可能なデータ構造駆動だ。この設計をするにあたって重要なテーマは"自分はどの答えを持っているか?"

NoSQLデータモデリングはアプリケーション特有のアクセスパターン、すなわち、サポートされるクエリの型駆動だ。メインテーマは"自分は何を知りたいのか?"

ryokkkkeryokkkke

1:N で具体的に非正規化(埋め込み)の考察

非正規化の例として 1:N が分かりやすいので取り上げます。DB はドキュメントタイプを想定します。

RDB であれば一般的には二つのテーブルを作り、N 側のテーブルは親である 1 側のデータの id を持つ、とかでしょう。

これをドキュメントストアで実装する場合、非正規化の一つの選択肢として、 1 側のエンティティの中に N のリストを埋め込む 、というものがあります。
こうすると、RDB であれば実行時に行っていた join が設計時に行われており、実行時 join を減らすことができる というわけですね。

https://gist.github.com/matope/2396234#2集計aggregates

エンティティのネストを使い、one-to-manyリレーションを最小化し、結果的にjoinを削減する。

ただし、もちろん 1:N なら全部これをすれば良いというわけではなく、データの CRUD を考えた時にこの構造の得意不得意があると思います。
(この辺り、自分の考察で少々根拠がゆるいのでご注意ください。)
(「更新」は主に C, U, D 全般を指して使っています)

  • データのライフサイクル的に、1:N の N 側の更新の頻度が少ないことが分かっている場合により適している ハズ。
    • 1:1 なんかは数が少ないの代表例なので積極的に埋め込みを検討して良いと思う。
  • N 側(子側)の単体の更新頻度が高い場合は、ネストが深いほど効率が悪い。
    • 1:1 であっても、更新頻度、読み取り頻度ともに高い場合は無理に埋め込む必要は無いと思う。
  • 同様に、 N 側の単体の Read 頻度が高い場合もネストが深いほど効率が悪い。
    • Read に際して余分なデータが多い。
  • また検索性が落ちるので、アプリケーションで N 側のデータに対して絞り込みを行う場合は注意が必要。
    • 例えば Datastore の場合、埋め込んだデータに対してはインデックスが効かないので、全件取得が現実的ではないデータでかつ絞り込みがある場合はよく考慮した方が良いだろう。
    • まあそもそもそんなに細かい検索を NoSQL 上で行うのは間違っているのではっていう話はあると思う。
  • 場合によっては、N 側の id だけのリストを埋め込むとかもありだなと思う。
    • あとはリスト表示に必要なデータだけを埋め込んでおいて、詳細では個別に id で本体を Read するとか。
    • その場合の N 側の親の参照の方法もいくつかある。
      • id を持っていても良いし、持ってなくても良いし、ネストしている場合は祖先全ての id を持っている可能性もある。
ryokkkkeryokkkke

RDB が不得意なデータ構造

https://gist.github.com/matope/2396234#2集計aggregates

この図はあるeコマースビジネスドメインでの製品エンティティのモデリングを図解している。まず、全ての製品はID,Price,Descriptionをそなえることがわかる。
次に、異なるタイプの製品は、BookのAuthor,JeansのLengthのように異なる属性を持つことに気づく。これらの属性のうちいくつかは、Music AlbumのTracksのように、one-to-manyまたはmany-to-manyの性質を持っている。
例えば、Jeans属性はブランドと特定の製造者の間で一貫していない。この問題をリレーショナル正規化データモデルで解決することは可能であるが、その解決方法はエレガントさからはほど遠いものになってしまう。

("図"はリンクに飛んでご覧ください)

これ、ありますよね〜過去にこのタイプで苦しんだ記憶が蘇る🙂
ハードスキーマな RDB では「だいたい同じだけど部分的に違う」みたいな構造にはあんまり柔軟には対応できないですよね。
DB 設計に限らず、アプリケーションコード側も型がしっかりしてる言語の場合は結構大変だったりしますね。

ドキュメントストアの場合、その少し違う構造のままを保存できます。

同じようなデータ構造としては、イベント駆動な設計におけるイベントデータのモデルなんかがありますね。
イベントとしての共通なスキーマと、イベントの種類ごとに異なるスキーマを持つ、みたいな構造。

そもそもアプリケーションとしてその少し違う部分を別々に扱うことがないのであれば、わざわざその構造を別のスキーマ(=正規化されたスキーマ)に分解して保存するのではなく、 その構造のまま保存してその構造のままクラインとに返す、ができる ということですね。

もちろん上の 1:N の時と同様、横串で検索したいとかそういう目的には弱いと思うので、これも絶対的な解法ではないことには注意が必要でしょう。

ryokkkkeryokkkke

埋め込みとトランザクション

https://gist.github.com/matope/2396234#4アトミックな集計atomic-aggregate

パワフルなトランザクション機構がリレーショナルデータベースの不可欠な部分である理由の一つは、正規化されたデータは典型的には複数箇所の更新が必要とされるからである。

これもとってもわかりやすい表現ですね。
正規化してリレーションで結ぶやり方だと、 アプリケーション側では一回の操作として扱うデータが永続化層側的には分離している、ということが高頻度で起きる ので、複数のものを同時に更新しても大丈夫な仕組みが DB 側に必要で、それがトランザクションの Atomic 特性だというわけですね。

で、 NoSQL はどうかと言うと、もちろん RDB と比較して Atomicity には弱いわけですが、 埋め込み(非正規化)をしていれば、アプリケーション側での一回の操作が永続化層的にも一回の操作で済むのでそもそも DB 側に複数エンティティにまたがる Atomiciy が不要 、ということが実現可能な場合があるんですね。

これはなるほどと思いました。RDB に慣れてると「トランザクションが無いって...」みたいな気持ちになりがちな気がするんですが、その必然性の理由を辿るとなぜトランザクションが無くても(もしくは RDB ほど強力ではなくとも)データストアとして成り立つのかが腑に落ちます。

多分、この辺って DB 一般について知見が豊富な方であれば、そもそもトランザクションを使うことの方がややこしい話なのだ、みたいな気持ちになるんだろうなと思いました。
自分は RDB 前提、みたいなところから入っているのでこういうところで根本の理解が進むととても面白いですね。もっと勉強しよう。

繰り返しになりますが、これも「NoSQL には常にトランザクションが不要!」という話ではありません。アプリケーション側の操作が必ずしも DB 側の構造と一致しているわけではないからです。

ドメインに対して広い範囲のデータをカバーしようと思ったら、NoSQL だとしてもトランザクションは必須でしょうね。
そもそもどこまでを NoSQL DB で扱うのか、の判断にはこうした Atomicity の話を考慮するのも有効そうです。

ryokkkkeryokkkke

RDB との併用

NoSQL のこうした性質を鑑みると、

  • サービス全体で色々な形で CRUD されるデータは RDB
  • 特定の機能領域の中でパフォーマンスのボトルネックになったり、RDB でのモデリングが非効率な場面で NoSQL

みたいな併用は素直な気がします。

ただ、併用するとなった時に RDB と NoSQL のデータの関連付けをどうするかっていう問題は出てきそうですね。
NoSQL 側が User の id を参照したとして、もちろん外部キー制約的な機能を DB 側(RDB と NoSQL をまたがっている)で実現するのは無理なので、アプリケーションレイヤーでその辺の制約を実装する必要がありそうです。
あるいは外部キー制約が必要のないデータに絞るか。

RDB と NoSQL で重複したデータがあるみたいな状況は相当地獄な気がするので避けた方が無難でしょうね...その同期の仕組みを自前実装するとかするとバグの温床になりそう。

レポジトリ層と NoSQL

併用時、アプリケーションコード側も少し悩ましかったりするでしょうか。
RDB と NoSQL 、クエリの考え方が相当違うので、同じ OR マッパー的なオブジェクトを介して DB を操作するのが非効率な場面が相当多そう。特に NoSQL 側が機能要件に特化していればいるほど(つまり正規化を崩しているほど)。
課金体系も全然違ったりしますし、もはや統一的に扱うのはデメリットの方が多いような。

なので、あまり統一的な仕組みを作ることに躍起にならない方が良いような気がしています。
もはや NoSQL のクエリってユースケース層(あるいはサービス層)と直結しているようなイメージなので、その間に抽象化の仕組みを挟む方がしんどそう。

まあこの辺はさすがに運用してる方の中には既にベストプラクティスがあるでしょうし、先人にお聞きするのが良さそうですね。

GraphQL と NoSQL

同じ路線として、 GraphQL と NoSQL も相性がとっても悪そうに感じました。
GraphQL 側的にはリゾルバは宣言的に(≒汎用的に)記述する割に、NoSQL サイドは対比として手続的な処理に対する解をそのまま保持しているようなものなので、パフォーマンスのために設計時 join したデータをクライアントとのインターフェース(GraphQL)のために実行時に再度分解するみたいなことになりますね。
GraphQL のスキーマを NoSQL のスキーマに合わせてしまうとかすると良いのかもしれませんが、もはやそれは REST で良いのではみたいな話になりそう。
統一的に GraphQL を採用しているマイクロサービスな構造のサービスとかって、この変どうするんだろう?

ryokkkkeryokkkke

埋め込みと権限

埋め込み、権限のことは考えないといけないですね。
大元のエンティティを取得できる権限があれば基本的には埋め込まれたデータも取得できてしまうので、途中で「埋め込んだデータの一部に閲覧制限をかけたい!」となった際には結構大変そう。

DB とクライアントの間にサーバを用意しているならもちろんそこでフィルタリングはできますが、取得に無駄が出ることになるでしょう。