ラベル LINQ to Entities の投稿を表示しています。 すべての投稿を表示
ラベル LINQ to Entities の投稿を表示しています。 すべての投稿を表示

2015年4月3日

Entity Framework のパフォーマンス #1 LINQ to Entities クエリ

Entity Framework (EF) の LINQ to Entities クエリの 1 回目(コールドクエリ)は、かなり遅い。
1 回目なので DB アクセスも当然遅いのだが、それだけでなく、プログラム側でコストの高い初期化処理を行っているため、遅くなっている。

どんな処理を行っているかは下記参照。

パフォーマンス

いくつかの簡単な LINQ to Entities クエリの SQL 構築時間と実行時間を測定してみる。
[環境]
  • Windows 7 64bit
  • Visual Studio 2010 SP1
  • .NET 4.5.2 (TargetFramework は 4.0)
  • Entity Framework 5.0 + Database First + DbContext
    ※ EF5.0 の理由は、VS2010 で DbContext Generator が提供されている最終バージョンのため
    ※ ObjectContext はほぼ使わないと思うのでナシ
    (DbContext は ObjectContext のラッパーなので性能は ObjectContext の方が少し上のはず)

[測定コード]

// テーブル定義
public class KeyTable1
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}
public class KeyTable2
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}
public class KeyTable3
{
    public string Key { get; set; }
    public string Name { get; set; }
    public DateTime Register { get; set; }
}

// 測定処理
static class EfTest
{
    // KeyTable1 の select の SQL 構築時間
    public static void Key1Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable1 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
/* 構築される SQL :
SELECT
[Extent1].[Key] AS [Key],
[Extent1].[Name] AS [Name],
[Extent1].[Register] AS [Register]
FROM [dbo].[KeyTable1] AS [Extent1] */
        }
    }

    // KeyTable2 の select の SQL 構築時間
    public static void Key2Select()
    {
        using( var context = new TestEntities() ){
            var query =from item in context.KeyTable2 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable3 の select の SQL 構築時間
    public static void Key3Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable3 select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable1 の select-where の SQL 構築時間
    public static void Key1SelectWhere()
    {
        using( var context = new TestEntities() ){
            var query =
                from item in context.KeyTable1
                where item.Key == "1"
                select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    // KeyTable1 の select-where-in (自動コンパイル対象外) の SQL 構築時間
    public static void Key1SelectWhereIn()
    {
        var keys = new[] { "1" };
        using( var context = new TestEntities() ){
            var query =
                from item in context.KeyTable1
                where keys.Contains(item.Key)
                select item;
            Measurement.ConsoleElapsedTime(() => query.ToString());
        }
    }

    //  KeyTable1 の select の SQL 実行時間
    public static void ExecuteKey1Select()
    {
        using( var context = new TestEntities() ){
            var query = from item in context.KeyTable1 select item;
            Measurement.ConsoleElapsedTime(() => query.AsEnumerable().GetEnumerator());
        }
    }
}

// 経過時間測定ユーティリティ
static class Measurement
{
    public static void ConsoleElapsedTime(Action act)
    {
        var caller = new StackFrame(1);
        var watch = Stopwatch.StartNew();
        act();
        watch.Stop();
        Console.WriteLine("[{0}] {1}", caller.GetMethod().Name, watch.Elapsed);
    }
}

実験1. 同じテーブルに対する select

Key1Select × 5
[Key1Select] 00:00:00.1411941
[Key1Select] 00:00:00.0002129
[Key1Select] 00:00:00.0001244
[Key1Select] 00:00:00.0001124
[Key1Select] 00:00:00.0001150
1 回目はとにかく遅い。
2 回目は色々なキャッシュが効いて速くなる。(この結果だと 663 倍)
3 回目も少し速くなり、以降横ばい。

実験2. 複数のテーブルそれぞれに対する select

Key1Select × 3 → Key2Select × 3 → Key3Select × 3
[Key1Select] 00:00:00.1421443
[Key1Select] 00:00:00.0002142
[Key1Select] 00:00:00.0001231
[Key2Select] 00:00:00.0072696
[Key2Select] 00:00:00.0001616
[Key2Select] 00:00:00.0001167
[Key3Select] 00:00:00.0070160
[Key3Select] 00:00:00.0001599
[Key3Select] 00:00:00.0001321
1 回目ほどではないが、各テーブルの 1 回目もそれなりに遅い。
2 回目以降はキャッシュが効いている。

実験3. select 後の select-where

Key1Select → Key1SelectWhere × 3
[Key1Select] 00:00:00.1420541
[Key1SelectWhere] 00:00:00.0013068
[Key1SelectWhere] 00:00:00.0001667
[Key1SelectWhere] 00:00:00.0001364
Key2Select → Key1SelectWhere × 3
[Key2Select] 00:00:00.1429743
[Key1SelectWhere] 00:00:00.0074316
[Key1SelectWhere] 00:00:00.0002014
[Key1SelectWhere] 00:00:00.0001372
QueryKey1Where の 1 回目について、前者(先に同じテーブルの SQL を構築していた場合)の方が速い。
つまり、異なるクエリでも、式木が部分的に一致していればキャッシュが効くようになっている。

実験4. 自動コンパイルされないクエリ

Key1Select → Key1SelectWhereIn × 3
[Key1Select] 00:00:00.1426134
[Key1SelectWhereIn] 00:00:00.0021291
[Key1SelectWhereIn] 00:00:00.0010327
[Key1SelectWhereIn] 00:00:00.0010109
2 回目以降は早くなっているが、自動コンパイルクエリの対象外であるため、実験3. の 2 回目より遅い。

※自動コンパイルクエリについて
EF 5.0 + .NET 4.5 (TargetFramework は 4.0 でも可) の環境から、LINQ to Entities クエリは自動でコンパイルされてキャッシュされるようになっている。(以前は CompiledQuery を手動で実装する必要があった)
しかし、IN 句を構築するための Enumerable.Contains を式木に含めると自動コンパイルの対象外となる。
理由は、LINQ to Entities クエリから SQL が確定できないため。(下記参照)

実験5. SQL 構築後の SQL 実行

ExecuteKey1Select × 3
[ExecuteKey1Select] 00:00:00.1694211
[ExecuteKey1Select] 00:00:00.0020680
[ExecuteKey1Select] 00:00:00.0016365
Key1Select → ExecuteKey1Select × 3
[Key1Select] 00:00:00.1425704
[ExecuteKey1Select] 00:00:00.0296910
[ExecuteKey1Select] 00:00:00.0019183
[ExecuteKey1Select] 00:00:00.0016942
SQL 構築を事前に行っておくと、その分、SQL 実行も速くなっている。
また、1 回目のSQL 構築時間は 1回目の SQL 実行時間と比べてもかなり遅い。
(SQL 実行時間は環境依存なのであまり比較に意味はないかもしれないが・・・)

まとめ

上記から明らかなように、LINQ to Entities クエリの 1 回目は遅い。

対策としては、動作時間の長いプログラム(Web アプリやサービス)だと、起動時に全テーブルの SQL 構築処理を実行して事前キャッシュしておく、などが可能だが、1 回の実行で終わるようなプログラムの場合はどうしようもない。
そういうものに EF は使うべきではない、ということなんだろう・・・
今のところ、SQL 構築のオーバーヘッドは大きすぎると思われるので、パフォーマンスが求められる場合も EF は向いていない。

EF 5.0 以降を使用している場合、.NET 4.5 をインストールしておくことは割と重要。
複雑なクエリほど SQL 構築に時間がかかるため、自動コンパイルクエリが有効だと、2 回目以降のパフォーマンスが向上する。
CompiledQuery でクエリのキャッシュを手動管理することも可能だが、DbContext では使えないことに注意。(CompiledQuery は ObjectContext のみ対応)

クエリの初期化処理に「ビュー生成」があり、これは事前に作成しておくこと可能だが、上記の測定ではあまり効果が無かったため、省略している。
Entity Framework Power Tools を使えば、プリコンパイルビューが簡単に生成可能。

2015年3月25日

IQueryable はタイプセーフではない

Entity Framework (EF) 使いにとっては常識かもしれないが、IQueryable は残念ながらタイプセーフではない。
より正確に言うと、式木の構築はタイプセーフだが、IQueryable.Provider.Execute による式木の実行はタイプセーフでなくなる。

なぜなら、IQueryProvider.Execute の実装が、すべてのメソッドに対応しているわけではないため。

例えば EF の場合、IQueryProvider.Execute は式木から SQL を構築して DB に投げるが、式木内に SQL 変換が考慮されてないメソッドがあった場合、エラーを発生させるしかない。
特に自作メソッドについては対応しようがないため、すべてエラーとなる。

[EF のエラー発生サンプル]

// テーブル定義
public partial class Customer
{
    public string Id { get; set; }
    public string Name { get; set; }
}

// 実行処理 (クラス内)
public static void Sample()
{
    using( var context = new SampleEntities() ){
        var query =
            from item in context.Customer
            select item.Name.ToUpperInvariant();
        // ToUpperInvariant() は LINQ to Entities 未対応
        // ToUpper() は対応してる (EF 5.0 で確認)

        foreach( var item in query ) Console.WriteLine(item);
        // System.NotSupportedException 発生
    }
}

例外的に、Queryable.AsQueryable から生成できる EnumerableQuery は、ほぼタイプセーフになっている。
これの IQueryProvider.Execute の実装は、式木内の Queryable クラスのメソッドを Enumerable クラスの同名のメソッドに置換した後、コンパイルしてラムダ式を作成し、そのまま実行している。
つまり、LINQ to Objects に変換して実行するだけなので、自作メソッドでも何でも使用可能。

参考情報

LINQ to Entities で使用可能なメソッドについては、下記参照。
上記にないものについては、EntityFunctionsSqlFunctions (SQL Server 専用) を使用する。

LINQ to Entities で自作メソッドを SQL にマップする方法も一応ある。
LINQ to Entities が実際に実行できるかを確認したい場合は、LINQPad がオススメ。
というか競合してるアプリが他にない・・・

2015年3月21日

IQueryable の仕組みについて

Entity Framework (以下、EF)LINQ to Entities を実現するための IQueryable インターフェイスについて、今まであまり理解してなかったので MSDN とソースからお勉強。
理解してなくても使える EF は凄い。

IQueryable と IQueryProvider


public interface IQueryable : IEnumerable {
    Expression Expression { get; }
    Type ElementType { get; }
    IQueryProvider Provider { get; }
}
public interface IQueryable<out T> : IEnumerable<T>, IQueryable { }

public interface IQueryProvider{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);

    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

ソースの通りなのだが、とりあえず要点をまとめると
  • IQueryable.Expression は式木(式ツリー)を保持 (IQueryable の本体)
  • IQueryable.ProviderIQueryProvider を提供
  • IQueryable の通常版・ジェネリック版の違いは、継承元の IEnumerable がジェネリック版かどうか
  • IQueryProvider.CreateQuery(Expression) は引数の式木から IQueryable インスタンスを作成する
    (主に IQueryable の拡張メソッドで使用される)
  • IQueryProvider.Execute(Expression) は引数の式木を実行する
    (LINQ to Entities なら式木から SQL を構築して実行し、結果を返す)

IQueryable と IQueryProvider の実装方法は下記参照。

Queryable 拡張メソッド(IQueryable の標準クエリ演算子)

System.Linq.Queryable クラスの拡張メソッドの処理についてソースから説明。

[Queryable.Select の実装 (見易さのため一部整形)]

public static IQueryable<TResult> Select<TSource,TResult>(
    this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector
) {
    if (source == null) throw Error.ArgumentNull("source");
    if (selector == null) throw Error.ArgumentNull("selector");

    return source.Provider.CreateQuery<TResult>( 
        Expression.Call(
            null,
            ((MethodInfo)MethodBase.GetCurrentMethod())
                .MakeGenericMethod(typeof(TSource), typeof(TResult)), 
            new Expression[] { source.Expression, Expression.Quote(selector) }
        )
    );
}
[行毎の説明]
4-5 引数の null チェック
7 IQueryable インスタンスを新たに作成して return
※ IQueryable はイミュータブルなので、式木を変更するには新規作成が必要
8 IQueryProvider.CreateQuery の引数 : Expression.Call はメソッド呼び出しの式木を作成
9 Expression.Call の第 1 引数 : static メソッドを呼び出すので null を指定
10-11 Expression.Call の第 2 引数 : 自身 (Queryable.Select) を呼び出しメソッドとして指定
12 Expression.Call の第 3 引数 : 呼び出すメソッドの引数を式木で指定
1 個目の引数は IQueryable.Expression を使用し、2 個目の引数は selector 引数を使用
Expression.Quote は式木型引数を表すために使うものらしい

式木に触ったことが無い場合は意味不明かもしれないが、やってることは単純で、自身のメソッド呼び出しを式木に変換しているだけである。
Queryable クラスの他の拡張メソッドも、一部を除いてほぼ同じ実装。
(こういう実装の場合、CSS のクラスみたくメソッドの定義に対してカンマ区切りが使えれば理想かも)

また、式木 (Expression 型) は、ToString() で簡単に中身を見ることができる。

[Where と Select を使った式木の表示]

var source = new[]{ 0, 1, 2, 3, 4, 5 };
// 純粋な IEnumerable に対する AsQueryable() は EnumerableQuery を生成
var query =
    from n in source.AsQueryable()
    where n % 2 == 1
    select n * n;

Console.WriteLine(query.Expression);
Console.WriteLine(string.Join(",", query));
[結果]
System.Int32[].Where(n => ((n % 2) == 1)).Select(n => (n * n))
1,9,25

まとめ

  • IQueryable は式木を構築するためのインターフェース
  • Queryable 拡張メソッドは式木を簡単に構築するためのユーティリティ

Expression.Quote の存在に疑問を持った場合は下記が参考になる。