データ ポイント

Entity Framework を使用した SQL Azure のネットワーク待機時間の短縮

Julie Lerman

Julie Lermanローカルに管理している SQL Server データベースをマイクロソフトのクラウド ベースの SQL Azure データベースに切り替えることは、一見難しく思えるかもしれません。しかし、実際は簡単です。接続文字列を切り替えるだけで、作業は完了します。開発者はこういった状況を歓迎しますが、これは "ただ機能するだけ" です。

データベースを切り替えることにより、ネットワーク待機時間が生じます。これは、アプリケーションのパフォーマンス全体に大きな影響を及ぼす可能性があります。さいわい、ネットワーク待機時間による影響を正しく理解すれば、Entity Framework を使用して、SQL Azure によるパフォーマンスへの影響を軽減することができます。

データ アクセス コードのプロファイリング

今月のコラムでは、Visual Studio 2010 のプロファイラー ツール (msdn.microsoft.com/library/ms182372) を使用して、ローカル ネットワーク上と SQL Azure アカウント上にある AdventureWorksDW データベースに対する、さまざまなデータ アクセス操作を比較します。また、このプロファイラーで、Entity Framework を使用してデータベースから数人の顧客を読み込む呼び出しについても調べます。最初のテストでは、Entity Framework 4.0 の遅延読み込み機能を使用して、顧客だけをクエリしてから、クエリした顧客の販売情報を取得します。次のテストでは、Include メソッドを使用して、顧客とその顧客の販売情報を同時に読み込みます。図 1 に、このようなクエリを行って結果を列挙するのに使用したコンソール アプリケーションを示します。

図 1 パフォーマンスを調べるために設計したクエリ

{

  using (var context = new AdventureWorksDWEntities())

  {

    var warmupquery = context.Accounts.FirstOrDefault();

  }

  DoTheRealQuery();

}



private static void DoTheRealQuery()

{

  using ( var context=new  AdventureWorksDWEntities())

  {

    var query =   context.Customers.Where(c => c.InternetSales.Any()).Take(100);

    var customers = query.ToList();

    EnumerateCustomers(customers);

  }

}



private static void EnumerateCustomers(List<Customer> customers)

{

  foreach (var c in customers)

  {

    WriteCustomers (c);

    

  }

}



private static void WriteCustomer(Customer c)

{

  Console.WriteLine

 ("CustomerName: {0} First Purchase: {1}  # Orders: {2}",

    c.FirstName.Trim() + "" + c.LastName,  c.DateFirstPurchase, c.InternetSales.Count);

}

まず、Entity Data Model (EDM) メタデータのメモリへの読み込み、ビューのプリコンパイルなど、1 回しか行わない操作による負荷を取り除くためのクエリを実行します。次に、DoTheRealQuery メソッドから Customers エンティティのサブセットにクエリし、一覧にクエリを実行して、結果を列挙します。列挙中にその顧客の販売情報にアクセスされますが、(ここでは) データベースに戻ってから顧客ごとに反復処理を行って販売情報を取得する、遅延読み込みを強制的に実行します。

ローカル ネットワークのパフォーマンスの調査

ローカル ネットワーク上で、社内設置型の SQL Server データベースに対してこのクエリを実行すると、クエリの最初の呼び出しに 233 ミリ秒かかります。これは、顧客しか取得していないためです。コードから遅延読み込みを強制する列挙を実行すると 195 ミリ秒かかります。

ここで、Customers と同時に InternetSales を一括読み込みするため、クエリを次のように変更します。

context.Customers.Include("InternetSales").Where(c => c.InternetSales.Any()).Take(100);

これで、これら 100 人の顧客とそのすべての販売レコードが、データベースから返されるようになります。データ量は大きく増加します。

query.ToList の呼び出しは約 350 ミリ秒かかるようになり、100 人の顧客だけを返すよりもほぼ 33% 長くなります。

この変更による影響は他にもあります。コードで顧客を列挙するにつれて、販売データがメモリ内に読み込まれます。つまり、Entity Framework は、データベースに対してさらに 100 回のラウンドトリップを行う必要がなくなります。列挙と詳細情報の書き出しにかかる時間は、遅延読み込みが実行されていたときにかかった時間の 70% ほどにすぎません。

データ量と、コンピューターやローカル ネットワークの処理速度を考慮すると、全体的に見て、この特定のシナリオでの遅延読み込みは、データを一括読み込みする場合よりも少し速くなります。ただし、それでも一括読み込みの速度は十分速いため、これら 2 つの方法の差はほとんど目に付きません。これらは、Entity Framework によって、非常に高速に実行されるように見えます。

図 2 に、ローカル ネットワーク環境における一括読み込みと遅延読み込みの比較を示します。ToList 列は、クエリ実行時間を計測したものです。つまり、"var customers = query.ToList();" というコード行を計測したものです。Enumeration は、EnumerateCustomers メソッドを計測したものです。最後に、Query & Enumeration 列は、DoTheRealQuery メソッド全体を計測したものです。つまり、実行、列挙、コンテキストのインスタンス作成と、クエリ自体の宣言を組み合わせた時間です。

image: Comparing Eager Loading to Lazy Loading from a Local Database

図 2 ローカル データベースでの一括読み込みと遅延読み込みの比較

SQL Azure データベースへの切り替え

ここで、接続文字列をクラウドの SQL Azure データベースに切り替えます。コンピューターとクラウド データベース間でネットワーク待機時間が発生するため、ローカル データベースへのクエリ実行よりも時間がかかるのは驚くことではありません。このシナリオでは、待機時間を避けることはできません。ただし、注目すべき点は、さまざまなタスク間で待機時間の増加が同じにならないことです。要求の種類によっては、他の要求よりもはるかに待機時間が長いように見えます。図 3 をご覧ください。

image: Comparing Eager Loading to Lazy Loading from SQL Azure

図 3 SQL Azure からの一括読み込みと遅延読み込みの比較

一括読み込みグラフは、こちらも、事前に顧客だけを読み込む場合よりも低速です。しかし、遅延読み込みにかかった時間と比較すると、ローカル データベースの場合は約 30% 遅かったのに対して、SQL Azure では 3 倍近く時間がかかっています。

ただし、図 3 からわかるように、ネットワーク待機時間の影響を最も受けるのは遅延読み込みです。一括読み込みで InternetSales データがメモリ内に格納された後、データの列挙は最初のテストと同じくらい高速になります。しかし、遅延読み込みでは、クラウド データベースへのラウンドトリップがさらに 100 回行われます。ネットワークの待機時間が原因で、これらの各ラウンドトリップにさらに長い時間がかかり、これらが組み合わさることで、結果の取得にかかる時間が目に見えて遅くなります。

この列挙には、メモリ内の列挙よりも桁違いの時間がかかります。データベースへの毎回のラウンドトリップで 1 人の顧客の InternetSales データを取得すると膨大な時間がかかります。全体としては、確かに、Customers だけを事前に読み込む方がはるかに高速ですが、この環境では、遅延読み込みですべてのデータを取得するとほぼ 6 倍の時間がかかります。

SQL Azure が不利だと言っているのではありません。実際には、SQL Azure は高いパフォーマンスを発揮します。ここでは、Entity Framework のクエリ メカニズムを選択すると、待機時間の問題によってパフォーマンス全体に悪影響が出る可能性があることを指摘しています。

このデモの特定のユース ケースは、アプリケーションにとって典型的なケースではありません。というのも、一般に、大量のオブジェクト対しては関連データの遅延読み込みは行わないためです。しかし、今回の場合は、(一括読み込みによって) 大量のデータを一度に返す方が、同じ量のデータをデータベースに何回もラウンドトリップを行って返すよりも、はるかに効率的であることが明らかです。ローカル データベースを使用しているときは、データがクラウドにあるときほど顕著にこの差があらわれないため、ローカル データベースを SQL Azure に切り替える際にはこの点を考慮する価値があります。

返すデータの形状によっては (たとえば、多数の Include メソッドを使用する複雑なクエリでは)、一括読み込みの負荷が高まるときも、遅延読み込みの負荷が高まるときもあります。読み込みを分散するのが理にかなうときもあります。つまり、パフォーマンスのプロファイルで得た情報に基づき、一部のデータを一括読み込みして、残りのデータを遅延読み込みします。多くの場合、最終的な解決策は、アプリケーションをクラウドに移行することです。Windows Azure と SQL Azure は、一体となって機能するよう設計されています。アプリケーションを Windows Azure に移行し、そのアプリケーションのデータを SQL Azure から取得すると、パフォーマンスを最大限に高めることができます。

投影を使用したクエリ結果の調整

ローカルに実行しているアプリケーションを使用している場合、シナリオによっては、クエリを修正すれば、データベースから返されるデータの量をさらに絞り込める場合があります。検討すべき技法の 1 つは、投影を使用することです。この技法では、返される関連データをはるかに細かく制御できます。Entity Framework の一括読み込みと遅延読み込みでは、どちらも返される関連データにフィルターを適用したり並べ替えたりすることはできません。しかし、投影を使用すれば、これらを実行できます。

たとえば、次の変更後のバージョンの TheRealQuery メソッドのクエリでは、InternetSales エンティティから販売数が 1,000 以上のサブセットのみが返されます。

private static void TheRealQuery()

    {

      using ( var context=new  AdventureWorksDWEntities())

      {

        Decimal salesMinimum = 1000;

        var query =

            from customer in context.Customers.Where(c => 

            c.InternetSales.Any()).Take(100)

            select new { customer, Sales = customer.InternetSales };

        IEnumerable customers = query.ToList();

        context.ContextOptions.LazyLoadingEnabled = false;

        EnumerateCustomers(customers);

      }

    }

このクエリでは、先ほどのクエリと同じ 100 人の顧客が、合計 155 件の InternetSales のレコードと共に返されます。これに対して、SalesAmount フィルターなしでは 661 件の販売レコードが返されます。

投影と遅延読み込みに関して注意しなければならないことがあります。データを投影する場合、コンテキストは関連データが読み込まれていることを認識しません。この現象は、Include メソッド、Load メソッド、または遅延読み込みでデータが読み込まれたときのみ発生します。したがって、先ほど TheRealQuery メソッドで行ったように、列挙する前に遅延読み込みを無効にしておくことが重要です。さもないと、コンテキストは、InternetSales データが既にメモリ内にあっても、このデータを遅延読み込みするため、列挙に必要以上に長い時間がかかります。

次の変更した列挙メソッドでは、この点を考慮しています。

private static void EnumerateCustomers(IEnumerable customers)

{

  foreach (var item in customers)

  {

    dynamic dynamicItem=item;

    WriteCustomer((Customer)dynamicItem.customer);

  }

}

このメソッドでは、C# 4 の "動的" 型を使用して、遅延バインドを実行します。

図 4 は、より細かく調整された投影によって実現される大幅なパフォーマンスの向上を示しています。

image: Comparing Eager Loading to a Filtered Projection from SQL Azure

図 4 SQL Azure からの一括読み込みとフィルターを適用した投影の比較

フィルターを適用したクエリの処理速度が、より多くのデータを返す一括読み込みクエリよりも高速になることは当然だと思われるかもしれません。もっと興味深い点は、一括読み込みクエリと比較した場合、フィルターを適用した投影の処理が SQL Azure データベースでは約 70% 速くなるのに対し、ローカル データベースでは 15% 程度しか速くならなかったことです。一括読み込みされた InternetSales コレクションでメモリ内の列挙が高速になるのは、投影されたコレクションと比べて、Entity Framework が InternetSales コレクションに内部アクセスするためだと考えられます。しかし、この場合の列挙は完全にメモリ内で行われるため、ネットワーク待機時間の影響は受けません。全体的に見ると、投影で見られるこの改善点は、投影によって生じる列挙の小さな代償を補って余りあります。

ネットワーク上で、投影に切り替えてクエリ結果を調整することは不要なように思われるかもしれませんが、SQL Azure に対しては、この種の調整によってアプリケーションのパフォーマンスが大幅に向上する場合があります。

クラウドをぜひご使用ください

ここで説明したシナリオは、SQL Azure を使用し、ローカルにホストされるアプリケーションやサービスを中心に考えています。これとは別に、Windows Azure を使用して、アプリケーションやサービスをクラウドでホストする場合もあります。たとえば、エンド ユーザーは、Silverlight を使用することで、Windows Communication Foundation を実行して SQL Azure のデータにアクセスする、Windows Azure Web Role (Windows Azure Web ロール)を使用する場合があります。この場合は、クラウド ベースのサービスと SQL Azure 間のネットワーク待機時間は発生しません。

どのような製品を組み合わせて使う場合でも、覚えておく必要がある最も重要なポイントは、アプリケーションが期待どおりに機能を維持しても、ネットワーク待機時間によってパフォーマンスに影響が出る可能性があることです。  

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

この記事のレビューに協力してくれた技術スタッフの Wayne Berry、Kraig Brockschmidt、Tim Laverty、および Steve Yi に心より感謝いたします。