实体框架查询性能与原始 SQL 执行不同

Posted

技术标签:

【中文标题】实体框架查询性能与原始 SQL 执行不同【英文标题】:Entity Framework query performance differs extrem with raw SQL execution 【发布时间】:2016-05-28 11:43:30 【问题描述】:

我有一个关于实体框架查询执行性能的问题。

架构

我有一个这样的表结构:

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] 表中的数据进行分组并进行一些聚合。

我创建了一个实体框架查询(查询的详细信息可以在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 查询复制到同一台机器上的 SSMS 中,使用与实体框架相同的连接字符串连接。

结果是性能大大提高:

执行结果:92行 执行时间:517ms

我也做了一些循环运行测试,结果很奇怪。测试是这样的

for (int i = 0; i < 50; i++)

    DateTime begin = DateTime.UtcNow;

    [...query...]

    TimeSpan excecutionTimeSpan = DateTime.UtcNow - begin;
    Debug.WriteLine("0th 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 或模型创建延迟。之前在同一个模型实例上执行了一些其他查询,性能良好。

更新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]

所以你可以看到你描述的问题现在已经解决了,但是执行时间没有改变。

另外,正如您在架构和原始执行时间中看到的那样,我使用了优化结构和高度优化的索引器。

Update 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的回答有关):

我创建了一个跟踪调用树,也许它会有所帮助:

更新6(与@usr的回答有关):

我通过 SQL Server Profiler 创建了两个 showplan XML。

Fast run (SSMS).SQLPlan

Slow run (EF).SQLPlan

Update 7(与@VladimirBaranov 的cmets 相关):

我现在运行一些与您的 cmets 相关的测试用例。

首先,我通过使用一个新的计算列和一个匹配的 INDEXER 来消除订单操作的时间。这减少了与DATEADD(MINUTE, DATEDIFF(MINUTE, 0, [TimeStamp] ) / 15* 15, 0) 相关的性能滞后。详细了解如何以及为什么可以找到here。

结果如下:

纯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("0th 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("0th run: 1", i, excecutionTimeSpan.ToString());

第 0 次运行:00:00:00.8381200

第一次运行:00:00:00.6920736

第二次运行: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("0th run: 1", i, excecutionTimeSpan.ToString());

第 0 次使用 RECOMPILE 运行:00:00:00.8260932

第一次运行 RECOMPILE:00:00:00.9139730

使用 RECOMPILE 进行第二次运行:00:00:01.0680665

在 SSMS 中执行相同的 SQL 查询(没有重新编译):

00:00:01.105

在 SSMS 中执行相同的 SQL 查询(使用 RECOMPILE):

00:00:00.902

我希望这是您需要的所有值。

【问题讨论】:

如果您在 EF 中运行查询,然后在 SSMS 中运行,则大部分数据将在第二次缓存中。确保在测量执行速度时考虑到这一点(例如,如果您在 SSMS 版本之后再次运行 EF 版本,是否会回到 16 秒?) Slow in the Application, Fast in SSMS 将解释为什么您会看到这种性能差异。 @SteffenMangold,EF 运行的查询有自己的计划。您在 SSMS 中运行的“相同”查询将有自己的计划,这可能会有所不同。如果可能,这两个计划都将被 SQL Server 引擎缓存和重用。文章详细解释了如何处理它。我个人将OPTION(RECOMPILE) 用于此类查询,即具有Dynamic Search Conditions 的查询,例如:([Extent4].[ProjectID] = @p__linq__0) OR (@p__linq__0 IS NULL) @SteffenMangold,我说的是execution plan in the SQL Server engine。不知道EF执行计划是什么意思。 您真的阅读了链接 (sommarskog.se/query-plan-mysteries.html) 吗?那里给出了很多理由。不同的会话设置是此问题的主要原因。这样做:使用 SQL Profiler 和 XML Showplan 事件捕获慢版本和快版本的执行计划。将两个计划都作为文件发布在某处(一些文件托管商)。 【参考方案1】:

在这个答案中,我专注于最初的观察:EF 生成的查询很慢,但是在 SSMS 中运行相同的查询时速度很快。

对此行为的一种可能解释是Parameter sniffing。

SQL Server 在执行时使用了一个称为参数嗅探的过程 有参数的存储过程。当。。。的时候 过程被编译或重新编译,传入的值 参数被评估并用于创建执行计划。那 然后将值与执行计划一起存储在计划缓存中。在 随后的执行,使用相同的值和相同的计划。

因此,EF 会生成一个参数很少的查询。第一次运行此查询时,服务器会使用第一次运行时生效的参数值为此查询创建一个执行计划。这个计划通常很不错。但是,稍后您使用其他参数值运行相同的 EF 查询。对于新的参数值,以前生成的计划可能不是最优的,查询会变慢。服务端继续使用之前的计划,因为还是同一个查询,只是参数值不同。

如果此时您获取查询文本并尝试直接在 SSMS 中运行它,服务器将创建一个新的执行计划,因为从技术上讲,它与 EF 应用程序发出的查询不同。即使一个字符差异就足够了,会话设置的任何更改也足以让服务器将查询视为新查询。因此,服务器在其缓存中针对看似相同的查询有两个计划。第一个“慢”计划对于新的参数值很慢,因为它最初是为不同的参数值构建的。第二个“快”计划是针对当前参数值构建的,所以速度很快。

Erland Sommarskog 的文章Slow in the Application, Fast in SSMS 更详细地解释了这个和其他相关领域。

有几种方法可以丢弃缓存的计划并强制服务器重新生成它们。更改表或更改表索引应该这样做 - 它应该丢弃与该表相关的所有计划,包括“慢”和“快”。然后,您在 EF 应用程序中使用新的参数值运行查询并获得新的“快速”计划。您在 SSMS 中运行查询并获得具有新参数值的第二个“快速”计划。服务器仍然会生成两个计划,但现在两个计划都很快。

另一个变体是将OPTION(RECOMPILE) 添加到查询中。使用此选项,服务器不会将生成的计划存储在其缓存中。因此,每次查询运行时,服务器都会使用实际参数值来生成(它认为)对于给定参数值最佳的计划。缺点是计划生成的额外开销。

请注意,例如,由于统计数据过时,服务器仍然可以使用此选项选择“坏”计划。但是,至少,参数嗅探不会成为问题。


想知道如何在 EF 生成的查询中添加 OPTION (RECOMPILE) 提示的人可以看看这个答案:

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

【讨论】:

感谢@VladimirBaranov!我可以 100% 确认您暗示了参数嗅探。一个简单的测试:我搜索了我的 EF 查询的所有执行计划,然后删除了,就像描述的 here 一样。在此之后,我首先运行 EF 查询,它的速度与我预期的一样快(UserVoice。 我同意这是最好的答案,所以我很高兴在这里添加我的投票以及用户之声。祝大家编码愉快! @SteffenMangold,谢谢。我很高兴你发现这个答案很有用。我从未使用过 EF,所以我不知道将此选项添加到生成的查询中的正确 EF 样式方式是什么。至少,现在你应该更容易弄清楚发生了什么以及如果你看到这样的问题该怎么办。【参考方案2】:

我知道我来晚了,但由于我参与了相关查询的构建,我觉得有必要采取一些行动。

我看到的 Linq to Entities 查询的一般问题是我们构建它们的典型方式引入了不必要的参数,这可能会影响缓存的数据库查询计划(所谓的 Sql Server 参数嗅探问题)。

让我们按表达式查看您的查询组

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

由于minuteInterval 是一个变量(即非常数),它引入了一个参数。 DateTime.MinValue 相同(请注意,原始类型公开与 constant 类似的内容,但对于 DateTimedecimal 等,它们是 静态只读字段,这使得它们在表达式中的处理方式有很大的不同)。

但不管它在 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]

如您所见,我们成功消除了一些查询参数。那会有帮助吗?好吧,与任何数据库查询调整一样,它可能会也可能不会。你需要试试看。

【讨论】:

【参考方案3】:

数据库引擎根据每个查询的调用方式确定每个查询的计划。在您的 EF Linq 查询中,计划是以这样一种方式准备的,即每个输入参数都被视为未知数(因为您不知道会发生什么)。在您的实际查询中,您将所有参数作为查询的一部分,因此它将在与参数化的计划不同的计划下运行。我立即看到的受影响部分之一是

...(@p__linq__0 为空)..

这是 FALSE,因为 p_linq_0 = 20827 并且不是 NULL,所以你的 WHERE 的前半部分一开始就是 FALSE,不需要再查看了。在 LINQ 查询的情况下,数据库不知道会发生什么,所以无论如何都会评估所有内容。

您需要看看是否可以使用索引或其他技术来加快运行速度。

【讨论】:

感谢您的回答@x0007me,请参阅我关于上面的详细评论。 如果这会导致执行速度发生 30 倍的变化,我会感到惊讶。【参考方案4】:

EF 运行查询时,会将其包装起来并使用 sp_executesql 运行,这意味着执行计划将缓存在存储过程执行计划缓存中。由于原始 sql 语句与 SP 版本的执行计划构建方式存在差异(参数嗅探等),因此两者可能会有所不同。

在运行 EF(sp 包装)版本时,SQL Server 很可能使用更通用的执行计划,该计划涵盖比您实际传入的值更广泛的时间戳

也就是说,为了减少 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 查询中进行显式连接以消除 or ProductID is null 部分

【讨论】:

感谢@KristoferA。 1) 我已经解决了这个问题,但忘记将 INDEXER 复制到我的问题架构中(现在添加)2) 另请参阅 更新 2解决了这个 这些用于 FOREIGN KEYs 的标准 INDEXER 也存在。抱歉,我认为我不必提及它们。 实际上运行 EF 运行的相同查询 exec sp_executesql N'...query...' 通过 SSMS 仍然比通过 EF 快得多。以这种方式运行它不会绕过任何执行计划差异吗?

以上是关于实体框架查询性能与原始 SQL 执行不同的主要内容,如果未能解决你的问题,请参考以下文章

来自 sql 查询执行实体框架的匿名类型结果

实体框架查询性能

原始 SQL 查询和实体框架核心

在 Dapper 和实体框架中处理对象的方式有啥区别 - 使用原始 SQL 查询?

实体框架 DbContext 执行的日志查询

使用原始 SQL 查询绑定复杂模型实体?