web-dev-qa-db-ja.com

Entity Frameworkのクエリパフォーマンスは、生のSQL実行でextremと異なります

Entity Frameworkのクエリ実行パフォーマンスについて質問があります。

スキーマ

私はこのようなテーブル構造を持っています:

_CREATE TABLE [dbo].[DataLogger]
(
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [ProjectID] [bigint] NULL,
    CONSTRAINT [PrimaryKey1] PRIMARY KEY CLUSTERED ( [ID] ASC )
)

CREATE TABLE [dbo].[DCDistributionBox]
(
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [DataLoggerID] [bigint] NOT NULL,
    CONSTRAINT [PrimaryKey2] PRIMARY KEY CLUSTERED ( [ID] ASC )
)

ALTER TABLE [dbo].[DCDistributionBox]
    ADD CONSTRAINT [FK_DCDistributionBox_DataLogger] 
    FOREIGN KEY([DataLoggerID]) REFERENCES [dbo].[DataLogger] ([ID])

CREATE TABLE [dbo].[DCString] 
(
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [DCDistributionBoxID] [bigint] NOT NULL,
    [CurrentMPP] [decimal](18, 2) NULL,
    CONSTRAINT [PrimaryKey3] PRIMARY KEY CLUSTERED ( [ID] ASC )
)

ALTER TABLE [dbo].[DCString]
    ADD CONSTRAINT [FK_DCString_DCDistributionBox] 
    FOREIGN KEY([DCDistributionBoxID]) REFERENCES [dbo].[DCDistributionBox] ([ID])

CREATE TABLE [dbo].[StringData]
(
    [DCStringID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,
    [DCCurrent] [decimal](18, 2) NULL,
    CONSTRAINT [PrimaryKey4] PRIMARY KEY CLUSTERED ( [TimeStamp] DESC, [DCStringID] ASC)
)

CREATE NONCLUSTERED INDEX [TimeStamp_DCCurrent-NonClusteredIndex] 
ON [dbo].[StringData] ([DCStringID] ASC, [TimeStamp] ASC)
INCLUDE ([DCCurrent])
_

外部キーの標準インデックスも存在します(スペース上の理由でそれらをすべてリストしたくありません)。

_[StringData]_テーブルには、次のストレージ統計があります。

  • データスペース:26,901.86 MB
  • 行数:131,827,749
  • パーティション化:true
  • パーティション数:62

使用法

ここで、_[StringData]_テーブルのデータをグループ化し、いくつかの集計を行います。

Entity Frameworkクエリを作成しました(クエリの詳細情報は here にあります):

_var compareData = model.StringDatas
    .AsNoTracking()
    .Where(p => p.DCString.DCDistributionBox.DataLogger.ProjectID == projectID && p.TimeStamp >= fromDate && p.TimeStamp < tillDate)
    .Select(d => new
    {
        TimeStamp = d.TimeStamp,
        DCCurrentMpp = d.DCCurrent / d.DCString.CurrentMPP
    })
    .GroupBy(d => DbFunctions.AddMinutes(DateTime.MinValue, DbFunctions.DiffMinutes(DateTime.MinValue, d.TimeStamp) / minuteInterval * minuteInterval))
    .Select(d => new
    {
        TimeStamp = d.Key,
        DCCurrentMppMin = d.Min(v => v.DCCurrentMpp),
        DCCurrentMppMax = d.Max(v => v.DCCurrentMpp),
        DCCurrentMppAvg = d.Average(v => v.DCCurrentMpp),
        DCCurrentMppStDev = DbFunctions.StandardDeviationP(d.Select(v => v.DCCurrentMpp))
    })
    .ToList();
_

実行時間は非常に長い!?

  • 実行結果:92行
  • 実行時間:〜16000ms

試行回数

Entity Frameworkが生成したSQLクエリを調べたところ、次のようになりました。

_DECLARE @p__linq__4 DATETIME = 0;
DECLARE @p__linq__3 DATETIME = 0;
DECLARE @p__linq__5 INT = 15;
DECLARE @p__linq__6 INT = 15;
DECLARE @p__linq__0 BIGINT = 20827;
DECLARE @p__linq__1 DATETIME = '06.02.2016 00:00:00';
DECLARE @p__linq__2 DATETIME = '07.02.2016 00:00:00';

SELECT 
1 AS [C1], 
[GroupBy1].[K1] AS [C2], 
[GroupBy1].[A1] AS [C3], 
[GroupBy1].[A2] AS [C4], 
[GroupBy1].[A3] AS [C5], 
[GroupBy1].[A4] AS [C6]
FROM ( SELECT 
    [Project1].[K1] AS [K1], 
    MIN([Project1].[A1]) AS [A1], 
    MAX([Project1].[A2]) AS [A2], 
    AVG([Project1].[A3]) AS [A3], 
    STDEVP([Project1].[A4]) AS [A4]
    FROM ( SELECT 
        DATEADD (minute, ((DATEDIFF (minute, @p__linq__4, [Project1].[TimeStamp])) / @p__linq__5) * @p__linq__6, @p__linq__3) AS [K1], 
        [Project1].[C1] AS [A1], 
        [Project1].[C1] AS [A2], 
        [Project1].[C1] AS [A3], 
        [Project1].[C1] AS [A4]
        FROM ( SELECT 
            [Extent1].[TimeStamp] AS [TimeStamp], 
            [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
            FROM    [dbo].[StringData] AS [Extent1]
            INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
            INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
            INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
            WHERE (([Extent4].[ProjectID] = @p__linq__0) OR (([Extent4].[ProjectID] IS NULL) AND (@p__linq__0 IS NULL))) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
        )  AS [Project1]
    )  AS [Project1]
    GROUP BY [K1]
)  AS [GroupBy1]
_

このSQLクエリを、Entity Frameworkと同じ接続文字列で接続された同じマシン上のSSMSにコピーしました。

その結果、パフォーマンスが大幅に向上します。

  • 実行結果:92行
  • 実行時間:517ms

また、ループ実行テストも行っていますが、結果がおかしいです。テストは次のようになります

_for (int i = 0; i < 50; i++)
{
    DateTime begin = DateTime.UtcNow;

    [...query...]

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}
_

結果は非常に異なり、ランダムに見えます(?):

_0th run: 00:00:11.0618580
1th run: 00:00:11.3339467
2th run: 00:00:10.0000676
3th run: 00:00:10.1508140
4th run: 00:00:09.2041939
5th run: 00:00:07.6710321
6th run: 00:00:10.3386312
7th run: 00:00:17.3422765
8th run: 00:00:13.8620557
9th run: 00:00:14.9041528
10th run: 00:00:12.7772906
11th run: 00:00:17.0170235
12th run: 00:00:14.7773750
_

質問

Entity Frameworkクエリの実行が非常に遅いのはなぜですか?結果の行数は非常に少なく、生のSQLクエリは非常に高速なパフォーマンスを示します。

更新1

MetaContextやModelの作成が遅れないように注意します。他のいくつかのクエリは、直前に同じModelインスタンスで実行され、パフォーマンスが向上しています。

更新2(@ x0007meの回答に関連):

ヒントをありがとうございますが、これは次のようにモデル設定を変更することで解消できます。

_modelContext.Configuration.UseDatabaseNullSemantics = true;
_

EFで生成されたSQLは次のとおりです。

_SELECT 
1 AS [C1], 
[GroupBy1].[K1] AS [C2], 
[GroupBy1].[A1] AS [C3], 
[GroupBy1].[A2] AS [C4], 
[GroupBy1].[A3] AS [C5], 
[GroupBy1].[A4] AS [C6]
FROM ( SELECT 
    [Project1].[K1] AS [K1], 
    MIN([Project1].[A1]) AS [A1], 
    MAX([Project1].[A2]) AS [A2], 
    AVG([Project1].[A3]) AS [A3], 
    STDEVP([Project1].[A4]) AS [A4]
    FROM ( SELECT 
        DATEADD (minute, ((DATEDIFF (minute, @p__linq__4, [Project1].[TimeStamp])) / @p__linq__5) * @p__linq__6, @p__linq__3) AS [K1], 
        [Project1].[C1] AS [A1], 
        [Project1].[C1] AS [A2], 
        [Project1].[C1] AS [A3], 
        [Project1].[C1] AS [A4]
        FROM ( SELECT 
            [Extent1].[TimeStamp] AS [TimeStamp], 
            [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
            FROM    [dbo].[StringData] AS [Extent1]
            INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
            INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
            INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
            WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
        )  AS [Project1]
    )  AS [Project1]
    GROUP BY [K1]
)  AS [GroupBy1]
_

これで、説明した問題が解決されたことがわかりますが、実行時間は変わりません。

また、スキーマと生の実行時間でわかるように、高度に最適化されたインデクサーを使用して最適化された構造を使用しました。

更新3(@Vladimir Baranovの回答に関連):

これがクエリプランのキャッシュに関連している理由がわかりません。 MSDNでは、EF6がクエリプランのキャッシュを利用することが明確に記述されているためです。

巨大な実行時間の差がクエリプランのキャッシュ(疑似コード)に関連していないことを示す簡単なテスト証明:

_using(var modelContext = new ModelContext())
{
    modelContext.Query(); //1th run activates caching

    modelContext.Query(); //2th used cached plan
}
_

その結果、両方のクエリが同じ実行時間で実行されます。

更新4(@bubiの回答に関連):

説明したとおり、EFによって生成されたクエリを実行しようとしました。

_int result = model.Database.ExecuteSqlCommand(@"SELECT 
    1 AS [C1], 
    [GroupBy1].[K1] AS [C2], 
    [GroupBy1].[A1] AS [C3], 
    [GroupBy1].[A2] AS [C4], 
    [GroupBy1].[A3] AS [C5], 
    [GroupBy1].[A4] AS [C6]
    FROM ( SELECT 
        [Project1].[K1] AS [K1], 
        MIN([Project1].[A1]) AS [A1], 
        MAX([Project1].[A2]) AS [A2], 
        AVG([Project1].[A3]) AS [A3], 
        STDEVP([Project1].[A4]) AS [A4]
        FROM ( SELECT 
            DATEADD (minute, ((DATEDIFF (minute, 0, [Project1].[TimeStamp])) / @p__linq__5) * @p__linq__6, 0) AS [K1], 
            [Project1].[C1] AS [A1], 
            [Project1].[C1] AS [A2], 
            [Project1].[C1] AS [A3], 
            [Project1].[C1] AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp] AS [TimeStamp], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringData] AS [Extent1]
                INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
            )  AS [Project1]
        )  AS [Project1]
        GROUP BY [K1]
    )  AS [GroupBy1]",
    new SqlParameter("p__linq__0", 20827),
    new SqlParameter("p__linq__1", fromDate),
    new SqlParameter("p__linq__2", tillDate),
    new SqlParameter("p__linq__5", 15),
    new SqlParameter("p__linq__6", 15));
_
  • 実行結果:92
  • 実行時間:〜16000ms

通常のEFクエリと同じくらい正確でした!?

更新5(@vittoreの回答に関連):

トレースされたコールツリーを作成します。

call tree trace

更新6(@usrの回答に関連):

SQL Serverプロファイラを使用して2つのプラン表示XMLを作成しました。

高速実行(SSMS).SQLPlan

スローラン(EF).SQLPlan

更新7(@VladimirBaranovのコメントに関連):

コメントに関連するテストケースをさらに実行します。

まず、新しい計算列と一致するINDEXERを使用して、時間のかかる注文操作を排除します。これにより、DATEADD(MINUTE, DATEDIFF(MINUTE, 0, [TimeStamp] ) / 15* 15, 0)に関連するパフォーマンスラグが減少します。 ここ を見つける方法と理由の詳細。

結果は次のようになります。

純粋なEntityFrameworkクエリ:

_for (int i = 0; i < 3; i++)
{
    DateTime begin = DateTime.UtcNow;
    var result = model.StringDatas
        .AsNoTracking()
        .Where(p => p.DCString.DCDistributionBox.DataLogger.ProjectID == projectID && p.TimeStamp15Minutes >= fromDate && p.TimeStamp15Minutes < tillDate)
        .Select(d => new
        {
            TimeStamp = d.TimeStamp15Minutes,
            DCCurrentMpp = d.DCCurrent / d.DCString.CurrentMPP
        })
        .GroupBy(d => d.TimeStamp)
        .Select(d => new
        {
            TimeStamp = d.Key,
            DCCurrentMppMin = d.Min(v => v.DCCurrentMpp),
            DCCurrentMppMax = d.Max(v => v.DCCurrentMpp),
            DCCurrentMppAvg = d.Average(v => v.DCCurrentMpp),
            DCCurrentMppStDev = DbFunctions.StandardDeviationP(d.Select(v => v.DCCurrentMpp))
        })
        .ToList();

        TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
        Debug.WriteLine("{0}th run pure EF: {1}", i, excecutionTimeSpan.ToString());
}
_

0回目の純粋なEF:00:00:12.6460624

1回目の実行の純粋なEF:00:00:11.0258393

2回目の実行の純粋なEF:00:00:08.4171044

EFで生成されたSQLをSQLクエリとして使用します。

_for (int i = 0; i < 3; i++)
{
    DateTime begin = DateTime.UtcNow;
    int result = model.Database.ExecuteSqlCommand(@"SELECT 
        1 AS [C1], 
        [GroupBy1].[K1] AS [TimeStamp15Minutes], 
        [GroupBy1].[A1] AS [C2], 
        [GroupBy1].[A2] AS [C3], 
        [GroupBy1].[A3] AS [C4], 
        [GroupBy1].[A4] AS [C5]
        FROM ( SELECT 
            [Project1].[TimeStamp15Minutes] AS [K1], 
            MIN([Project1].[C1]) AS [A1], 
            MAX([Project1].[C1]) AS [A2], 
            AVG([Project1].[C1]) AS [A3], 
            STDEVP([Project1].[C1]) AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp15Minutes] AS [TimeStamp15Minutes], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringData] AS [Extent1]
                INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp15Minutes] >= @p__linq__1) AND ([Extent1].[TimeStamp15Minutes] < @p__linq__2)
            )  AS [Project1]
            GROUP BY [Project1].[TimeStamp15Minutes]
        )  AS [GroupBy1];",
    new SqlParameter("p__linq__0", 20827),
    new SqlParameter("p__linq__1", fromDate),
    new SqlParameter("p__linq__2", tillDate));

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}
_

0回目の実行:00:00:00.8381200

1回目の実行:00:00:00.6920736

2回目の実行:00:00:00.7081006

およびOPTION(RECOMPILE)を使用:

_for (int i = 0; i < 3; i++)
{
    DateTime begin = DateTime.UtcNow;
    int result = model.Database.ExecuteSqlCommand(@"SELECT 
        1 AS [C1], 
        [GroupBy1].[K1] AS [TimeStamp15Minutes], 
        [GroupBy1].[A1] AS [C2], 
        [GroupBy1].[A2] AS [C3], 
        [GroupBy1].[A3] AS [C4], 
        [GroupBy1].[A4] AS [C5]
        FROM ( SELECT 
            [Project1].[TimeStamp15Minutes] AS [K1], 
            MIN([Project1].[C1]) AS [A1], 
            MAX([Project1].[C1]) AS [A2], 
            AVG([Project1].[C1]) AS [A3], 
            STDEVP([Project1].[C1]) AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp15Minutes] AS [TimeStamp15Minutes], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringData] AS [Extent1]
                INNER JOIN [dbo].[DCString] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBox] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLogger] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp15Minutes] >= @p__linq__1) AND ([Extent1].[TimeStamp15Minutes] < @p__linq__2)
            )  AS [Project1]
            GROUP BY [Project1].[TimeStamp15Minutes]
        )  AS [GroupBy1]
        OPTION(RECOMPILE);",
    new SqlParameter("p__linq__0", 20827),
    new SqlParameter("p__linq__1", fromDate),
    new SqlParameter("p__linq__2", tillDate));

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("{0}th run: {1}", i, excecutionTimeSpan.ToString());
}
_

RECOMPILEによる0回目の実行:00:00:00.8260932

RECOMPILEを使用した1回目の実行:00:00:00.9139730

RECOMPILEを使用した2回目の実行:00:00:01.0680665

SSMSで実行された同じSQLクエリ(RECOMPILEなし):

00:00:01.105

SSMSで実行された同じSQLクエリ(RECOMPILEを使用):

00:00:00.902

これがあなたが必要とするすべての値であることを願っています。

23
Steffen Mangold

この回答では、元の観察に焦点を当てています。EFによって生成されるクエリは低速ですが、同じクエリをSSMSで実行すると高速です。

この動作の1つの考えられる説明は パラメータスニッフィング です。

SQL Serverは、パラメーターを持つストアドプロシージャを実行するときに、パラメータースニッフィングと呼ばれるプロセスを使用します。プロシージャがコンパイルまたは再コンパイルされると、パラメーターに渡された値が評価され、実行プランの作成に使用されます。その後、その値は実行プランと共にプランキャッシュに格納されます。以降の実行では、その同じ値(および同じ計画)が使用されます。

したがって、EFはパラメーターがほとんどないクエリを生成します。このクエリを初めて実行すると、サーバーは、最初の実行で有効だったパラメーターの値を使用して、このクエリの実行プランを作成します。その計画は通常かなり良いです。ただし、後で、パラメーターの他の値を使用して同じEFクエリを実行します。パラメータの新しい値に対して、以前に生成されたプランが最適ではなく、クエリが遅くなる可能性があります。サーバーは以前のプランを引き続き使用します。これは同じクエリであるため、パラメーターの値が異なるだけです。

この時点でクエリテキストを取得してSSMSで直接実行しようとすると、技術的にはEFアプリケーションによって発行されるクエリとは異なるため、サーバーは新しい実行プランを作成します。 1文字の違いでも十分ですが、サーバーがクエリを新しいクエリとして処理するには、セッション設定の変更でも十分です。その結果、サーバーには、キャッシュ内で一見同じクエリに対する2つの計画があります。最初の「遅い」計画は、もともと別のパラメーター値用に作成されたものであるため、パラメーターの新しい値については遅いです。 2番目の「高速」プランは、現在のパラメーター値に対して作成されるため、高速です。

Erland Sommarskogによる記事 アプリケーションで低速、SSMSで高速 は、これと他の関連領域についてさらに詳しく説明しています。

キャッシュされたプランを破棄してサーバーに強制的に再生成させる方法はいくつかあります。テーブルを変更するか、テーブルインデックスを変更すると、この処理が行われます。この処理は、このテーブルに関連するすべての計画(「低速」と「高速」の両方)を破棄します。次に、パラメーターの新しい値を使用してEFアプリケーションでクエリを実行し、新しい "高速"プランを取得します。 SSMSでクエリを実行し、パラメーターの新しい値を使用して2番目の「高速」プランを取得します。サーバーはまだ2つのプランを生成しますが、どちらのプランも高速です。

別のバリアントは、クエリにOPTION(RECOMPILE)を追加することです。このオプションを使用すると、サーバーは生成されたプランをキャッシュに保存しません。したがって、クエリが実行されるたびに、サーバーは実際のパラメーター値を使用して、指定されたパラメーター値に最適である(と考える)計画を生成します。欠点は、プラン生成のオーバーヘッドが増えることです。

たとえば、古い統計のために、サーバーはこのオプションで「悪い」計画を選択する可能性があります。しかし、少なくとも、パラメータのスニッフィングは問題にはなりません。


EFによって生成されたクエリにOPTION (RECOMPILE)ヒントを追加する方法を知りたい人は、この答えを見てください:

https://stackoverflow.com/a/26762756/4116017

14

私はここで少し遅れていることを知っていますが、問題のクエリの作成に参加したので、何か行動を起こす必要があると感じています。

Linq to Entitiesクエリで見られる一般的な問題は、それらを構築する一般的な方法で不要なパラメーターが導入され、キャッシュされたデータベースクエリプランに影響する可能性があることです(いわゆるSQL Serverパラメータースニッフィング問題)。

クエリグループを式で見てみましょう

d => DbFunctions.AddMinutes(DateTime.MinValue, DbFunctions.DiffMinutes(DateTime.MinValue, d.TimeStamp) / minuteInterval * minuteInterval)

minuteIntervalは変数(つまり、非定数)であるため、パラメーターが導入されます。 DateTime.MinValueでも同じです(プリミティブ型は定数 sと同様のものを公開しますが、DateTimedecimalなどではstatic readonly fields式内での扱い方に大きな違いがあります)。

ただし、CLRシステムでの表現方法に関係なく、DateTime.MinValueは論理的には定数です。 minuteIntervalはどうですか、それはあなたの使用法に依存します。

この問題を解決するための私の試みは、その式に関連するすべてのパラメーターを排除することです。コンパイラで生成された式ではそれができないため、System.Linq.Expressionsを使用して手動で作成する必要があります。後者は直感的ではありませんが、幸い、ハイブリッドアプローチを使用できます。

まず、式パラメーターを置き換えることができるヘルパーメソッドが必要です。

public static class ExpressionUtils
{
    public static Expression ReplaceParemeter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }

    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == Source ? Target : base.VisitParameter(node);
        }
    }
}

これで必要なものがすべて揃いました。カスタムメソッド内にロジックをカプセル化します。

public static class QueryableUtils
{
    public static IQueryable<IGrouping<DateTime, T>> GroupBy<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> dateSelector, int minuteInterval)
    {
        Expression<Func<DateTime, DateTime, int, DateTime>> expr = (date, baseDate, interval) =>
            DbFunctions.AddMinutes(baseDate, DbFunctions.DiffMinutes(baseDate, date) / interval).Value;
        var selector = Expression.Lambda<Func<T, DateTime>>(
            expr.Body
            .ReplaceParemeter(expr.Parameters[0], dateSelector.Body)
            .ReplaceParemeter(expr.Parameters[1], Expression.Constant(DateTime.MinValue))
            .ReplaceParemeter(expr.Parameters[2], Expression.Constant(minuteInterval))
            , dateSelector.Parameters[0]
        );
        return source.GroupBy(selector);
    }
}

最後に、

.GroupBy(d => DbFunctions.AddMinutes(DateTime.MinValue, DbFunctions.DiffMinutes(DateTime.MinValue, d.TimeStamp) / minuteInterval * minuteInterval))

.GroupBy(d => d.TimeStamp, minuteInterval * minuteInterval)

生成されたSQLクエリは次のようになります(minuteInterval = 15の場合):

SELECT 
    1 AS [C1], 
    [GroupBy1].[K1] AS [C2], 
    [GroupBy1].[A1] AS [C3], 
    [GroupBy1].[A2] AS [C4], 
    [GroupBy1].[A3] AS [C5], 
    [GroupBy1].[A4] AS [C6]
    FROM ( SELECT 
        [Project1].[K1] AS [K1], 
        MIN([Project1].[A1]) AS [A1], 
        MAX([Project1].[A2]) AS [A2], 
        AVG([Project1].[A3]) AS [A3], 
        STDEVP([Project1].[A4]) AS [A4]
        FROM ( SELECT 
            DATEADD (minute, (DATEDIFF (minute, convert(datetime2, '0001-01-01 00:00:00.0000000', 121), [Project1].[TimeStamp])) / 225, convert(datetime2, '0001-01-01 00:00:00.0000000', 121)) AS [K1], 
            [Project1].[C1] AS [A1], 
            [Project1].[C1] AS [A2], 
            [Project1].[C1] AS [A3], 
            [Project1].[C1] AS [A4]
            FROM ( SELECT 
                [Extent1].[TimeStamp] AS [TimeStamp], 
                [Extent1].[DCCurrent] / [Extent2].[CurrentMPP] AS [C1]
                FROM    [dbo].[StringDatas] AS [Extent1]
                INNER JOIN [dbo].[DCStrings] AS [Extent2] ON [Extent1].[DCStringID] = [Extent2].[ID]
                INNER JOIN [dbo].[DCDistributionBoxes] AS [Extent3] ON [Extent2].[DCDistributionBoxID] = [Extent3].[ID]
                INNER JOIN [dbo].[DataLoggers] AS [Extent4] ON [Extent3].[DataLoggerID] = [Extent4].[ID]
                WHERE ([Extent4].[ProjectID] = @p__linq__0) AND ([Extent1].[TimeStamp] >= @p__linq__1) AND ([Extent1].[TimeStamp] < @p__linq__2)
            )  AS [Project1]
        )  AS [Project1]
        GROUP BY [K1]
    )  AS [GroupBy1]

ご覧のとおり、一部のクエリパラメータは正常に削除されました。それは役に立ちますか?まあ、他のデータベースクエリチューニングと同様に、そうでない場合もあります。あなたは試してみる必要があります。

2
Ivan Stoev

DBエンジンは、呼び出された方法に基づいて各クエリのプランを決定します。 EF Linqクエリの場合、計画は各入力パラメーターが不明として扱われるように準備されます(何が入ってくるのか分からないため)。実際のクエリでは、すべてのパラメータがクエリの一部として含まれているため、パラメータ化されたプランとは異なるプランで実行されます。私がすぐに目にする影響を受けた作品の一つは

...(@ p__linq__0 IS NULL)..

これは、p_linq_0 = 20827であるためFALSEであり、NULLではないため、WHEREの前半は最初はFALSEであり、これ以上調べる必要はありません。 LINQクエリの場合、DBは何が入ってくるのかわからないので、とにかくすべてを評価します。

これをより高速に実行するために、インデックスまたはその他の手法を使用できるかどうかを確認する必要があります。

1
Milan

EFがクエリを実行すると、クエリがラップされ、sp_executesqlで実行されます。つまり、実行プランはストアドプロシージャの実行プランキャッシュにキャッシュされます。生のsqlステートメントとSPバージョンの実行プランがどのように構築されているかは、パラメータスニッフィングなど)が異なるため、2つは異なる場合があります。

EF(sp wrapped)バージョンを実行する場合、SQLサーバーは、実際に渡される値よりも幅広いタイムスタンプをカバーする、より一般的な実行プランを使用している可能性があります。

とはいえ、SQLサーバーがハッシュ結合などで「おもしろい」ことを試みる可能性を減らすために、私が最初に行うことは次のとおりです。

1)where句および結合で使用される列にインデックスを付けます

create index ix_DataLogger_ProjectID on DataLogger (ProjectID);
create index ix_DCDistributionBox_DataLoggerID on DCDistributionBox (DataLoggerID);
create index ix_DCString_DCDistributionBoxID on DCString (DCDistributionBoxID);

2)Linqクエリで明示的な結合を実行して、またはProductIDがnullの部分を削除します

1
KristoferA