次の方法で共有


データ ポイント

昔のデータベースとは違う Windows Azure テーブル ストレージ

Julie Lerman

コード サンプルのダウンロード

Julie LermanWindows Azure テーブル ストレージは、開発者の中に多くの混乱を持ち込んでいます。これまで開発者がデータ ストレージを操作してきた経験のほとんどは、リレーショナル データベースによるものです。リレーショナル データベースには、さまざまなテーブルがあり、どのテーブルにもあらかじめ定義された一連の列が含まれ、通常、1 つ以上の列が ID キーに指定されます。テーブルどうしのリレーションシップは、これらのキーを使用して定義します。Windows Azure で情報を格納する方法はいくつかありますが、構造化したデータの保存に重点を置いているのは SQL Azure と Windows Azure テーブル ストレージの 2 つです。SQL Azure はリレーショナル データベースで、SQL Server にかなり近い機能を備えています。テーブルには、スキーマ、キー、リレーションシップ、およびその他の制約が定義され、SQL Server などのデータベースと同様、接続文字列を使用して接続します。

これに対して、Windows Azure テーブル ストレージは、リレーショナル データベースの操作に慣れてい開発者からするとやや不可解に思えます。Windows Azure テーブル ストレージを使用するアプリケーションの作成についてわかりやすく説明した資料はたくさんありますが、結局 Windows Azure テーブルとは一体何かを正確に理解することなく妄信している開発者がまだまだたくさんいます。

今月のコラムでは、これまでのリレーショナル思考の立場から Windows Azure テーブル ストレージの中核となる概念をいくつか説明し、リレーショナル モードから抜け出せない開発者の妄信をしっかりとした土台へと変えるお手伝いをします。また、どのようにデータを照会および更新するかに基づいてテーブルを設計する際に重要となる方針もいくつか紹介します。

取得と保存の効率を高めるデータの格納方法

Windows Azure テーブルのサービスは、膨大な量のデータを格納する能力を備えながらも、アクセスと保存の効率を高めるように設計されています。このサービスは、ストレージをシンプルにし、リレーショナル データベースを操作する際には避けては通れなかった問題 (制約、ビュー、インデックス、リレーションシップ、ストアド プロシージャ) を避けることができます。扱うのは、とにかくデータだけです。Windows Azure テーブルでは、照会の効率を高めるキーが使用されます。その中の PartitionKey というキーを使用すると、テーブル サービスが、複数のサーバーにテーブルを展開するタイミングだと判断したときに、負荷分散が実行されます。テーブルには、スキーマが指定されません。テーブルは、単に行 (またはエンティティ) を構造化したコンテナーで、行がどのように見えるかは関係ありません。ある特定の種類の行を格納するテーブルを作成することもできますが、1 つのテーブルにさまざまな構造を持つ行を格納することもできます (図 1 参照)。

image: A Single Windows Azure Table Can Contain Rows Representing Similar or Different Entities

図 1 類似エンティティを表す行も異種エンティティを表わす行も含められる、1 つの Windows Azure テーブル

すべての出発点はドメイン クラス

一般にデータベースを開発する場合、まず、データベースを作成し、そこにテーブルを定義して、テーブルごとに特定の構造 (それぞれデータ型が具体的に指定された列) と、他のテーブルとのリレーションシップを定義します。その後、アプリケーションからテーブルに対してデータを読み書きします。

しかし、Windows Azure テーブルのサービスでは、データベースは設計せず、クラスのみを設計します。クラスと 1 つ以上のクラスが所属するコンテナー (テーブル) を定義してから、オブジェクトのインスタンスを作成して、そのインスタンスを行としてストアに保存できます。

各クラスには、クラスに必要なプロパティに加え、Windows Azure テーブル サービスがどのように機能するか決めるのに重要な 3 つのプロパティ、PartitionKey、RowKey、TimeStamp を含める必要があります。PartitionKey と RowKey はどちらも文字列で、実行時に、クエリとトランザクションの効率を最もバランスよく保ち、それと同時にスケーラビリティも得られるようにそれらを定義する能力 (おそらく科学) があります。最大のメリットを得られるように PartitionKey と RowKey を定義する方法について詳しく理解するには、Jai Haridas による PDC09 セッション「Windows Azure テーブルと Windows Azure Queues の詳細」(microsoftpdc.com/sessions/svc09、英語) をご覧になることを強くお勧めします。

パフォーマンスとスケーラビリティを高める PartitionKey と RowKey

多くの開発者は、主キー、外部キー、そしてその 2 つのキー間の制約という体系に慣れています。Windows Azure テーブル ストレージでは、こうした考え方に縛られないようにする必要があります。そうしないと、Windows Azure テーブル ストレージにおけるキーの体系を把握するのが難しくなります。

Windows Azure のテーブルでは、PartitionKey 文字列プロパティと RowKey 文字列プロパティがテーブルのインデックスとして連携します。そのため、この 2 つのプロパティを定義するときは、データをどのように照会するかを考える必要があります。同時に、これらのプロパティは一意性をもたらし、行の主キーとしての役割を果たします。テーブル内の各エンティティでは、PartitionKey と RowKey の組み合わせが一意になるようにする必要があります。

ただし、PartitionKey を定義するときは、どのように照会するかを考えるだけでは不十分です。それは、PartitionKey が、負荷分散やスケーラビリティを実現するために物理的にテーブルをパーティション分割するのにも使用されるためです。たとえば、食べ物に関する情報を含むテーブルがあるとしましょう。このテーブルには、Vegetable (野菜)、Fruit (果物)、Grain (穀物) というように、食べ物の種類に対応する PartitionKey が定義されているとします。夏になると、Vegetable (野菜) パーティションの行はとても忙しくなります (データが別のパーティションのデータよりも頻繁にアクセスされる、いわゆる "ホット" なパーティションになります)。そこで、サービスは、Vegetable パーティションを別のサーバーに移動して、そのパーティションに行われる多くの要求をうまく処理することで、Food テーブルの負荷分散を実現できます。

そのパーティションへのアクセスが、1 台のサーバーの処理能力を上回ると予想される場合は、Vegetable_Root や Vegetable_Squash といった、さらに細かいパーティションを作成することを検討します。これは、負荷分散の粒度の単位が PartitionKey だからです。負荷分散が行われるとき、同じ PartitionKey 値を持つ行はすべてひとくくりにされます。テーブル内のすべてのエンティティがそれぞれ異なるパーティションに区切られるようにテーブルを設計することもできます。

PartitionKey とクエリについて

先ほど Vegetable PartitionKey をさらに細かいパーティションに分けることを提案したとき、Vegetable という文字列をキー名の後ろではなく、前に付けたことに注目してください。これが、クエリの効率を高めるもう 1 つのメカニズムです。Microsoft .NET Framework から Windows Azure テーブルへのクエリでは、LINQ to REST と、WCF Data Services の System.Data.Services.Client.DataServiceContext から派生するコンテキストを使用します。緑色のかぼちゃを探している場合は、テーブル全体を検索してリソースを消費することなく、次のように Vegetable_Squash パーティション内を検索できます。

var query = _serviceContext.FoodTable.AsTableServiceQuery()
.Where(c => c.PartitionKey=="Vegetable_Squash"&& c.Color == "Green");

(WCF Data Services によって返される) OData のクエリと、Windows Azure テーブルに対するクエリの大きな違いは、後者では文字列関数がサポートされていない点です。文字列の一部を検索する場合は、String.CompareTo を使用して、文字列の冒頭の文字を検索する必要があります。ですが、Vegetable カテゴリ全体をクエリする場合は、次のように CompareTo メソッドを使用して、PartitionKey の開始位置でプレフィックスの検索を実行できます。

var query = _serviceContext.FoodTable.AsTableServiceQuery()
            .Where(c => c.PartitionKey.CompareTo("Vegetable")>=0
            && c.PartitionKey.CompareTo("Vegetablf")<0
            && c.Color == "Green");

これにより、Vegetable で始まるパーティションのみを検索するよう限定されます。検索の対象はそれ以上にもそれ以下にもなりません (2 つ目の述語で、Vegetable ではなく Vegetablf を使用することで上限値が定義され、Yogurt や VegetableLike などのパーティションにある食べ物が返されなくなります)。今月のコラムのコード サンプルでは、この置換を動的に実行する方法をご覧になれます。

テーブル全体をスキャンするための並列クエリ

では、種類にかかわらず、あらゆる緑色の食べ物を探すとしたらどうでしょう。Windows Azure では、テーブル全体をスキャンする必要があることになります。さらに、テーブルのサイズが大きいと、Windows Azure にはもう 1 つ厄介な点があり、一度に 1,000 行しか返すことができません (または、5 秒しか処理できません)。Windows Azure では、これらの結果を "継続キー" と共に返し、再度クエリを実行します。この同期処理は冗長になる可能性があります。

これを回避するには、複数のクエリに分けてを実行します。おそらく、次のようにカテゴリの既知の一覧を反復処理して、各クエリをビルドすることになります。

_serviceContext.FoodTable.AsTableServiceQuery()
.Where(c => c.PartitionKey == _category && c.Color == "Green");

これで、すべてのクエリを送信して並列に実行できます。

クエリに関する設計上のその他の考慮事項

RowKey プロパティには、多くの目的があります。RowKey プロパティを PartitionKey と組み合わせることで、テーブル内の各行を一意に定義できます。たとえば、私はもう 1 人 Julie Lerman という人物を知っています (これは本当の話です)。そこで、lerman_julie という PartitionKey を共有するときは、2 人を区別するために RowKey が重要になります。また、RowKey は、インデックスの一部として機能するため、並べ替えを行う際にも役立ちます。それでは、年上の Julie Lerman (私です) と年下の Julie Lerman に役立つ RowKey は何でしょう。GUID は、確かに一意識別には役立ちますが、検索や並べ替えには役立ちません。この場合、値を組み合わせるのがおそらく最適でしょう。

他に私たち 2 人を区別するものは何でしょう。私は米国の反対側に住んでいますが、場所は変わる可能性があるため、キーとして使用するには適しません。これに対し、生年月日は確かに (20 年以上) 異なり、これは変化しない値です。ですが、私と生年月日が同じ Julie Lerman という人が世界のどこかにいる可能性は常にあり、データベースに格納されることがあるかもしれません。かなり信じがたいことですが、起こり得ないわけではありません。調べるべきことをすべて検討した結果、やはり生年月日は、私のアプリケーションで検索や並べ替えを行うのに使用する値ではないことがわかりました。したがって、今回 RowKey はクエリの一部として使用しません。従来の GUID で十分です。こうした判断を、すべての Windows Azure テーブルについて行う必要があります。

キーの定義については知っておくべきことはほかにもたくさんあり、データの取得、データの保存、スケーラビリティ、負荷分散などの要素がすべてかかわってきます。

リレーションシップについての再考

リレーショナル データベースでは、リレーションシップを定義するのに、外部キーと制約のみを使用します。確かに、クラス内で別のクラスを参照する外部キープロパティを定義できますが、Windows Azure テーブル ストレージではリレーションシップが強制されません。リレーションシップを強制するには、やはりコードを使用することになります。

これは、テーブルからクエリと更新 (挿入や削除など) をどのように実行するかに影響します。

クエリを実行するとき、テーブル間で結合は実行できません。そして、データを保存するとき、複数のパーティションまたは複数のテーブルにわたるコマンドをトランザクション処理することはできません。ですが、このコラムの冒頭でも指摘したように、Windows Azure テーブルにはグラフでデータを操作するメカニズムがあり、1 つのテーブルにさまざまなスキーマの行を格納できます。

アプリケーションで、ユーザーが連絡先とアドレスを一緒に操作する必要があれば、連絡先と同じテーブルにアドレスを保存できます。アドレスには、"lerman_julie" などの同じ PartitionKey を含めることが重要です。また、RowKey には "address_12345" などのエンティティの種類を指定する値を含めます。これで、クエリの際に連絡先の種類とアドレスの種類を簡単に区別できます。

Entity Group Transactions (EGT) という機能を活用するために、PartitionKey が共通の行は常にひとまとめに扱われます。この機能により、すべてのエンティティに同じ PartitionKey 値がある限り、1 つのトランザクションが複数のエンティティ間でアトミックに操作を実行できます。関連するデータに対する EGT の利点の 1 つは、単一のトランザクション内のすべてのエンティティで、トランザクション処理された更新を実行できることです。

詳細を理解するための基礎

Windows Azure テーブルはクラウド、つまり "雲の中" に存在していますが、私にとってはまるで霧の中にいるようにわかりにくいものでした。リレーショナル データベースに関してあらかじめ持っていた知識が原因で、Windows Azure テーブルを理解するのにたいへん苦労しました。Windows Azure テーブルの魅力を受け入れて真に理解するために RDBMS の考え方に縛られないようにと、さまざまなことを行いました (また、多くの人々を悩ませてしまいました)。Windows Azure テーブルを理解するためのこうした道程が、皆さんの道程を短くできることを願っています。

Windows Azure テーブルのサービスについて学ぶべきことはまだたくさんあります。マイクロソフトの Windows Azure のチームが、わかりやすいガイダンスを MSDN でいくつか提供しています。先ほど紹介した PDC09 の映像に加え、Windows Azure Storage チームのブログで、blogs.msdn.com/windowsazurestorage/archive/2010/03/28/windows-azure-storage-resources (英語) のリソース ページを参照してください。同チームは、詳細で有益な情報をブログに投稿し続けています。いつか、またはこのコラムが公開されるときには、数え切れないほど多くの質問への回答を見つけられると思います。今後のデータ ポイントのコラムで、具体的な例をいくつか提供できることを楽しみにしています。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女が執筆した『Programming Entity Framework』(O'Reilly Media、2009 年) は絶賛を浴びました。彼女には Twitter.com (julielermanvt、英語) から連絡できます。

この記事のレビューに協力してくれた技術スタッフの Brad Calder と Jai Haridas に心より感謝いたします。