Cutting Edge

ドキュメント、データベース、および最終的な一貫性

Dino Esposito

Dino Espositoここ何年かインターネットに接続できない洞窟にこもっていたとしましょう。そして俗世に戻ったところ、新しいプロジェクトを担当することになりました。最初のテクニカル ミーティングでは、チームのメンバーどうしで活発な議論が交わされ、ドキュメント データベースを強く推す意見を耳にしました。

ドキュメント データベースって一体何だろう。データを保存するのは昔からプレーンなリレーショナル SQL Server インスタンスだったはずだけど。ドキュメント データベースってテーブルにどんなメリットがあるんだろう。このような疑問をすべて理解しようと、さまざまな意見に熱心に耳を傾けました。でも、次のプロジェクトでリレーショナル ストレージではなくドキュメント データベースを使用すべきかどうかを実際にどうやって決めようか悩んでいます。

今回のコラムでは、ドキュメント データベースと NoSQL ストアについて、一般論としてどちらかの立場を明確に支持するつもりはありません。とは言え、物事を行うときにいつも最善の方法を探る方や、コストを抑えながら具体的で測定可能なメリットを常に求める方の懐疑的な物の見方のよい例になればとは考えています。

SQL に代わるもの

リレーショナル モデルや構造化照会言語 (SQL) には 40 年を超える歴史があります。急速に変化するこの業界では驚くほどの長さです。オブジェクト指向言語やモデル パターンの登場で、オブジェクト指向データベースが旧態依然としたリレーショナル モデルを追い越すかに思えましたが、リレーショナル モデルはここ数十年にわたって追随を許していません。

オブジェクト指向データベースは追い越すことができませんでしたが、オブジェクト関係マッピング (ORM) ツールの登場が状況を変化させています。.NET 空間では NHibernate が最初に業界標準として登場しましたが、ここ最近は Entity Framework も肩を並べる勢いです。他にも、さまざまなベンダーが作成した同様の商用フレームワークや、多数の支援者に支えられたオープン ソースのフレームワークもあります。それでも、オブジェクト モデルにはリレーショナル モデルを隅に追いやるほどの強力な根拠はないことは、最初に指摘しておきましょう。

しかし、多くの企業が NoSQL ストアを使用しています。そこで、「NoSQL はどこで使用していますか」と尋ねてみることをお勧めします。「NoSQL ストアを活用する方法を教えてください」と尋ねるよりも良い答えが見つかります。実際のテクノロジのユース ケースを調査して自身に合うかどうかを確認する方が、特定のテクノロジを使用する理由をやみくもに探すよりもお勧めです。NoSQL ストアが使用されている状況を調べると、次のような共通する側面が分かります。

  • 大量のデータ (予測不可能なほど膨大になることもある) を含み、ユーザー数が数百万人を超える可能性がある
  • 毎秒数千のクエリが実行される
  • 構造化されていないデータや半構造化されたデータが存在する (さまざまな形式で書き込まれるポリモーフィックなデータを同じように扱う必要がある)
  • クラウド コンピューティングや仮想ハードウェアのようにきわめてスケーラビリティが高い

上記のいずれの条件にも当てはまらないプロジェクトの場合、NoSQL の具体的なメリットを見つけることはあまり期待できません。条件に当てはまらない場合に NoSQL を使用しても、これまでと同じことを別の方法で行っているにすぎません。

構造上の違い

データベース サーバーに大量の書き込みや読み取りを行う対話性の高いアプリケーションをビルドする必要がある場合には NoSQL が適しています。リレーショナル モデルでは、テーブル間のリレーションシップが正しく機能するのは、関係するすべてのテーブルに固定スキーマがある場合に限られます。また、関係するすべてのテーブルは、ドメイン モデルの忠実かつ現実的な表現を提供する必要があります。

サイズやアクセス頻度に関係なく、データが "構造化されたテーブル" にうまく適合する場合は、これまでどおり優れた SQL Server が効果的に機能するとかなり確信できます。伝統的な SQL テクノロジは廃れたわけではなく、時間の経過と共に機能は向上しています。SQL Server 2014 の列ストアの機能を利用すると、多数の列を処理できます。列数が多くなると、クエリのパフォーマンスに影響する可能性があります。また、列数が膨大になると、半構造化されたデータからリレーショナル モデルへのマップを試みることになる場合もあります。

列ストアとは、列と行の通常のセットからなるプレーンなテーブルです。ただし、コンテンツは列単位に物理的にレイアウトされ、保存されます。その結果、特定の列のすべてのデータが同じ SQL 物理ページに保存されるようになります。選択した多数の列からすべてのデータをクエリする必要がある場合、この新しいストレージ形式を利用すれば、保存層の設計を見直すことなく大幅にパフォーマンスが向上します。

最近は、構造化されたテーブルへの適合が困難な、より難解で複雑なデータ モデルを扱うアプリケーションが増えています。やがて、アプリケーションの設計者は、柔軟性に欠ける SQL スキーマの限界内でこのように部分的に構造化されたデータを保存したりクエリするために回避策や妥協案を考えることが本当に必要なのかどうかについて疑問を持つようになります。リレーショナル スキーマに適合するようにデータをモデル化できたとしても、よく使う多数のクエリに JOIN ステートメントが必要になります。そのため、テーブル数が予想を超えて増えた場合、必然的にクエリのパフォーマンスが低下します。この問題は多くの顧客がアプリケーションを使用すると発生するので、応答の遅延はアプリケーションが関係するビジネスに影響を与えるおそれがあります。

SQL と NoSQL という 2 つ異なるオプションがあり、どちらでも選択できるように思えます。しかし、SQL と NoSQL は趣が異なるだけの同じテクノロジではありません。どちらもデータの永続性を扱いますが、構造上多くの違いがあります。

NoSQL データベースでは、固定のデータ スキーマもリレーションシップも使用しません。そのためモデルが決まりません。NoSQL データベースではドメイン空間をモデル化する必要がありません。結果のオブジェクトを論理グループで保存できます。ドメイン モデルを設計し、オブジェクトのグラフを操作します。NoSQL ストアはディスクへのオブジェクトのシリアル化を処理するだけです。

ドキュメントの操作とは

この場合、「ドキュメント」という言葉は、「レコード」という言葉とほぼ同じです。ドキュメントのコレクションは、レコードのテーブルと考えてかまいません。コレクション内の各オブジェクトのスキーマが、同じコレクションの他のオブジェクトのスキーマと異なっていてもかまわない点が大きく違います。ただし、すべてのドキュメントは論理的に関連しています。

この違いは一見するよりも微妙です。たとえば、書籍の著者の略歴を管理するとします。著者によって略歴の長さは違います。Author1 を説明するデータの一部が、Author2 を説明するデータと異なっているかもしれません。略歴を属性のコレクションにすると、実際には固定スキーマといくつかのオプション列を用意することになります。略歴を、Word 文書形式の履歴書、出版書籍の一覧とコメントを含む XML ストリーム、YouTube インタビューへのリンク、人口動態統計などのいくつかの属性といったファイルから成るコレクションにすると、スキーマを定義するのは難しくなり、SQL ストアという厳密な境界に収めることはほぼ不可能です。

NoSQL が役に立つかどうかを判断する尺度としては、データのポリモーフィズムのレベルが優れています。たとえば、イベント ソース アーキテクチャに従ったシステムをビルドするとします。イベント ソース アーキテクチャでは、アプリケーションの保存モデルはイベントのプレーンな履歴です。すべてのユーザー アクションは、サーバー側で 1 つ以上のイベントを発生させます。アプリケーションの状態を追跡するには、イベントを記録する必要があります。

たとえば、電子商取引のシナリオでは、ユーザーが注文を送信するとワークフローが開始されます。このシナリオでは、注文の送信完了、注文の検証完了、注文の拒否、注文の作成完了、注文の処理完了、注文の出荷中、注文の出荷完了、注文の返品、注文の更新完了などのドメイン イベントが多数生成されます。これらのイベントはすべて 1 つの注文に関係します。イベントのシーケンスを処理することで、注文の現在状態を組み立てたり、組み立て直すことができます。

現実の電子商取引システムの注文の処理は、状態列が最新の情報に更新されるように Orders テーブルを管理するだけでは済みません。注文を表すデータで行われるすべての操作を追跡する必要があります。このような操作はそれぞれまったく異なり、関係するデータも異なります。たとえば、注文の返品イベントには関係するデータはほとんどありません。注文の送信完了イベントでは、ユーザーから送信された一連の情報すべてを記録する可能性があります。注文の更新完了イベントでは、おそらく既存の注文に対して行われた変更だけを記録します。

注文はまったく異なるイベントのリストになる可能性があるため、よりドキュメントに近くなります。各イベントは保存するオブジェクトにすぎません。多くの電子商取引システムでは、このような問題に対処するためにおそらくリレーショナル ストアを使用しています。しかし、このようなイベントのリストの保存に NoSQL アプローチを使用するのもかなり興味深く、成功が期待されます。イベント ソース アーキテクチャの詳細については、Microsoft Download Center (bit.ly/1lesmzm、英語) からダウンロードできる無料の電子書籍『Exploring CQRS and Event Sourcing』(Microsoft、2013 年) を参照してください。

最終的な一貫性は問題になるか

最終的な一貫性とは、SQL と NoSQL との構造上の違いに関連するもう 1 つのテーマです。データ モデルでドキュメントを簡単に認識できるとしても、最終決定の真の判断基準になるのは配置済みのアプリケーションに対する最終的な一貫性の影響です。

最終的な一貫性とは、同じデータに対する読み取りと書き込みが同じにならなくなるタイミングです。十分な期間にわたって特定のオブジェクトに更新を行っていなければ、クエリが最後のコマンドで書き込んだ内容を返すことを保証するという意味では、ほとんどの NoSQL システムには最終的な一貫性があります。

多くの場合、最終的な一貫性はまったく問題になりません。一般に、境界が決まっているコンテキスト (境界コンテキスト) 内ではできるだけ一貫性を保つ必要がありますが、複数の境界コンテキストにまたがるレベルまで一貫性を保つ必要はあまりありません。システムが拡張されても一貫性を保つテクノロジもありますが、通常は一貫性が保たれていることを当てにはできません。

最終的な一貫性が問題になるかどうかを確認するシンプルなテストがあります。コマンドがあるデータを書き込んでも、その後の読み取りで古いデータが返されるシナリオをどのように考えますか。書き込んだばかりのデータを必ず読み取る必要がある場合、次の 2 つのオプションがあります。

  • NoSQL データベースを使用しない
  • NoSQL データベースを構成して一貫性を持たせる

よく使われている .NET NoSQL データベースの RavenDB を使用することを想定した次のコード スニペットについて考えてみます。

DocumentSession
  .Store(yourObject);
DocumentSession
  .Query<YourObjectType>()
  .Where(t => t.Id == id)

1 行目は、オブジェクトを RavenDB アーカイブに保存しています。2 行目は、保存したオブジェクトと同じ ID プロパティ値に対して同じストア セッションでクエリを行い、直前に保存したオブジェクトの読み取りを試みています。既定のデータベース構成のままでは、直前に書き込んだオブジェクトとは異なるオブジェクトが読み取られます。

RavenDB に関する限り、ストアへの書き込みと、クエリ エンジンが使用するインデックスの更新はそれぞれ独立した操作です。インデックスの更新は定期的な操作として行われます。同じオブジェクトに対してしばらくの間更新を行わなければ、この不整合は数秒もかからずに解消されます。以下に一貫性を実際に強制する方法を示します。

_instance.Conventions.DefaultQueryingConsistency =
  ConsistencyOptions.AlwaysWaitForNonStaleResultsAsOfLastWrite;

このようにすると、インデックスが更新されるまで読み取り結果は返されなくなります。その結果、簡単な読み取りでも完了に数秒かかることになります。このことから、NoSQL ストアは業界のニッチな分野に対応していることがわかります。しかし、課題もあります。NoSQL ではアーキテクチャ上の一部の問題に対処し、他は無視します。

インデックスの更新を "待機する" よりも優れた方法がありますが、一般に、採用する方法は直面しているシナリオによって異なります。そのため、次のようにクエリ時に必要な一貫性の種類を決定するようにします。

using( var session = store.OpenSession() )
{
  var query = session.Query<Person>()
    .Customize(c=> c.WaitForNonStaleResultsAsOfLastWrite() )
    .Where( p => /* condition */ );
}

ここでカスタマイズした WaitForNonStaleResultsAsOfLastWrite クエリでは、最後のドキュメント書き込みにインデックスを設定するために関連インデックスを待機し、クエリを実行した後にサーバーに到着する最終的なドキュメントは無視するようにサーバーに指示しています。

これは、インデックスが常に古くなる、書き込み頻度の高いシナリオに有効です。これは仕様です。WaitForNonStaleResultsXxxx メソッドには、動作が異なり、やや異なるシナリオを解決する多くのメソッドがあります。他にも、最終的な一貫性を完全に受け入れ、返された結果が古いかどうかをサーバーに単純に尋ねる方法もあります。これは次のように動作します。

using( var session = store.OpenSession() )
{
  RavenQueryStatistics stats;
  var query = session.Query<Person>()
    .Statistics( out stats )
    .Where( p => /* condition */ );
}

この例では、インデックスを待機しません。返された結果が古いかどうかを判断できるようにクエリ統計を出力することも、サーバーに依頼します。解決を試みるシナリオがある場合は、十分な情報を集めて最善の決断を下すようにします。

多様な保存

結局のところ、リレーショナル ストアは古い技術だからいずれかの NoSQL 製品に置き換えろと言っているわけではありません。ポイントは、システムの構造やデータの特性を理解して、最善のアーキテクチャを導き出すことです。NoSQL ストアを使用すべきと考えられる最も具体的なアプリケーションは、イベント ソース アーキテクチャのコンテキストでのイベント ストアです。

リレーショナル ストアを使用すべきかどうか簡単に答えが見つからないシステムでの最善策は、多様な保存について考えることです。NoSQL とリレーショナル データベースのいずれか 1 つを選択するのではなく、NoSQL データベースとリレーショナル データベースの両方の長所を組み合わせたストレージ レイヤーを検討します。この種のストレージ システムでは、さまざまな問題にそれぞれ最適な方法で対処します。

企業のコンテコストでは、異なる種類のデータの保存には、異なるストレージ テクノロジを使用する必要があります。これは、特にサービス指向アーキテクチャに当てはまります。この場合、各サービスはおそらく独自のストレージ レイヤーを保持することになります。1 つのテクノロジや製品でストレージを統一する理由はありません。多様な保存では、さまざまなトレージ テクノロジや製品を習得する必要がありますが、トレーニングのコストはそれほどかかりません。


Dino Esposito  は、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014 年) および『Programming ASP.NET MVC 5』(Microsoft Press、2014 年) の共著者です。JetBrains の .NET Framework および Android プラットフォームのテクニカル エバンジェリストでもあります。世界各国で開催される業界のイベントで頻繁に講演しており、software2cents.wordpress.com (英語) や Twitter (twitter.com/despos、英語) でソフトウェアに関するビジョンを紹介しています。

この記事のレビューに協力してくれた以下の技術スタッフの Mauro Servienti (Managed Designs) に心より感謝いたします。