EF 生成的查询执行时间过长

Query generated by EF takes too much time to execute

我有一个由 Entity-Framework 生成的非常简单的查询,有时 当我尝试 运行 这个query 执行完差不多30多秒,我超时了Exception.

SELECT TOP (10) 
[Extent1].[LinkID] AS [LinkID], 
[Extent1].[Title] AS [Title], 
[Extent1].[Url] AS [Url], 
[Extent1].[Description] AS [Description], 
[Extent1].[SentDate] AS [SentDate], 
[Extent1].[VisitCount] AS [VisitCount], 
[Extent1].[RssSourceId] AS [RssSourceId], 
[Extent1].[ReviewStatus] AS [ReviewStatus], 
[Extent1].[UserAccountId] AS [UserAccountId], 
[Extent1].[CreationDate] AS [CreationDate]
FROM ( SELECT [Extent1].[LinkID] AS [LinkID], [Extent1].[Title] AS [Title], [Extent1].[Url] AS [Url], [Extent1].[Description] AS [Description], [Extent1].[SentDate] AS [SentDate], [Extent1].[VisitCount] AS [VisitCount], [Extent1].[RssSourceId] AS [RssSourceId], [Extent1].[ReviewStatus] AS [ReviewStatus], [Extent1].[UserAccountId] AS [UserAccountId], [Extent1].[CreationDate] AS [CreationDate], row_number() OVER (ORDER BY [Extent1].[SentDate] DESC) AS [row_number]
    FROM [dbo].[Links] AS [Extent1]
)  AS [Extent1]
WHERE [Extent1].[row_number] > 0
ORDER BY [Extent1].[SentDate] DESC

生成查询的代码是:

public async Task<IQueryable<TEntity>> GetAsync(Expression<Func<TEntity, bool>> filter = null,
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null)
{
    return await Task.Run(() =>
    {
        IQueryable<TEntity> query = _dbSet;
        if (filter != null)
        {
            query = query.Where(filter);
        }

        if (orderBy != null)
        {
            query = orderBy(query);
        }

        return query;
    });
}

请注意,当我删除内部 Select 语句和 Where 子句并将其更改为以下内容时,查询会在不到一秒的时间内正常执行。

SELECT TOP (10) 
[Extent1].[LinkID] AS [LinkID], 
[Extent1].[Title] AS [Title], 
.
.
.
FROM [dbo].[Links] AS [Extent1]
ORDER BY [Extent1].[SentDate] DESC

任何建议都会有所帮助。

更新:

以上代码的用法如下:

var dbLinks = await _uow.LinkRespository.GetAsync(filter, orderBy);
var pagedLinks = new PagedList<Link>(dbLinks, pageNumber, PAGE_SIZE);
var vmLinks = Mapper.Map<IPagedList<LinkViewItemViewModel>>(pagedLinks);

和过滤器:

var result = await GetLinks(null, pageNo, a => a.OrderByDescending(x => x.SentDate));

您尝试过在方法中链接吗?

        IQueryable<TEntity> query = _dbSet;
        return query.Where(x => (filter != null ? filter : x)
                    .Where(x => (orderBy != null ? orderBy : x));

我想知道这是否会更改由 EF 创建的查询。

我猜 WHERE row_number > 0 会随着时间的推移而改变,因为您要求第 2 页、第 3 页等...

因此,我很好奇它是否有助于创建此索引:

CREATE INDEX idx_links_SentDate_desc ON [dbo].[Links] ([SentDate] DESC)

老实说,如果它有效,它几乎是一个创可贴,你可能需要经常重建这个索引,因为我猜它会随着时间的推移变得支离破碎......

更新:查看评论!事实证明 DESC 没有任何效果,如果您的数据从低到高应该避免!

我从没想过你根本就没有索引。经验教训 - 在进一步挖掘之前始终检查基础知识。


如果不需要分页,那么查询可以简化为

SELECT TOP (10) 
    [Extent1].[LinkID] AS [LinkID], 
    [Extent1].[Title] AS [Title], 
    ...
FROM [dbo].[Links] AS [Extent1]
ORDER BY [Extent1].[SentDate] DESC

正如您所验证的,它运行速度很快。

显然,您确实需要分页,让我们看看我们能做些什么。

你当前版本慢的原因,因为它首先扫描整个 table,计算每一行的行号,然后才returns 10 行。 我在这里错了。 SQL 服务器优化器非常聪明。 你的问题的根源在别的地方。请参阅下面的更新。


顺便说一句,正如其他人提到的,只有当 SentDate 列是唯一的时,此分页才能正常工作。如果它不是唯一的,则需要 ORDER BY SentDate 和另一个唯一的列,例如一些 ID 来解决歧义。

如果您不需要直接跳转到特定页面的能力,而是总是从第 1 页开始,然后转到下一页、下一页等等,那么描述这种分页的正确有效方法在这篇优秀的文章中:http://use-the-index-luke.com/blog/2013-07/pagination-done-the-postgresql-way 作者使用 PostgreSQL 进行说明,但该技术也适用于 MS SQL Server。它归结为记住显示页面最后一行的 ID,然后在 WHERE 子句中使用此 ID 和适当的支持索引来检索下一页而不扫描所有先前的行.

SQL Server 2008 没有内置的分页支持,因此我们必须使用变通方法。我将展示一个变体,它允许直接跳转到给定页面并且在第一页上运行速度很快,但在后面的页面上会越来越慢。

您的 C# 代码中将包含这些变量 (PageSizePageNumber)。我把它们放在这里是为了说明这一点。

DECLARE @VarPageSize int = 10; -- number of rows in each page
DECLARE @VarPageNumber int = 3; -- page numeration is zero-based

SELECT TOP (@VarPageSize)
    [Extent1].[LinkID] AS [LinkID]
    ,[Extent1].[Title] AS [Title]
    ,[Extent1].[Url] AS [Url]
    ,[Extent1].[Description] AS [Description]
    ,[Extent1].[SentDate] AS [SentDate]
    ,[Extent1].[VisitCount] AS [VisitCount]
    ,[Extent1].[RssSourceId] AS [RssSourceId]
    ,[Extent1].[ReviewStatus] AS [ReviewStatus]
    ,[Extent1].[UserAccountId] AS [UserAccountId]
    ,[Extent1].[CreationDate] AS [CreationDate]
FROM
    (
        SELECT TOP((@VarPageNumber + 1) * @VarPageSize)
            [Extent1].[LinkID] AS [LinkID]
            ,[Extent1].[Title] AS [Title]
            ,[Extent1].[Url] AS [Url]
            ,[Extent1].[Description] AS [Description]
            ,[Extent1].[SentDate] AS [SentDate]
            ,[Extent1].[VisitCount] AS [VisitCount]
            ,[Extent1].[RssSourceId] AS [RssSourceId]
            ,[Extent1].[ReviewStatus] AS [ReviewStatus]
            ,[Extent1].[UserAccountId] AS [UserAccountId]
            ,[Extent1].[CreationDate] AS [CreationDate]
        FROM [dbo].[Links] AS [Extent1]
        ORDER BY [Extent1].[SentDate] DESC
    ) AS [Extent1]
ORDER BY [Extent1].[SentDate] ASC
;

第一页是第 1 到 10 行,第二页是第 11 到 20 行,依此类推。 当我们尝试获取第四页时,让我们看看这个查询是如何工作的,即第 31 到 40 行。PageSize=10PageNumber=3。在内部查询中,我们 select 前 40 行。请注意,我们 扫描整个 table 此处,我们只扫描前 40 行。我们甚至不需要明确的 ROW_NUMBER()。然后我们需要从找到的 40 行中 select 最后 10 行,所以外部查询 selects TOP(10)ORDER BY 在相反的方向。因为这将 return 行 40 到 31 以相反的顺序。您可以在客户端将它们重新排序为正确的顺序,或者再添加一个外部查询,该查询只需按 SentDate DESC 再次对它们进行排序。像这样:

SELECT
    [Extent1].[LinkID] AS [LinkID]
    ,[Extent1].[Title] AS [Title]
    ,[Extent1].[Url] AS [Url]
    ,[Extent1].[Description] AS [Description]
    ,[Extent1].[SentDate] AS [SentDate]
    ,[Extent1].[VisitCount] AS [VisitCount]
    ,[Extent1].[RssSourceId] AS [RssSourceId]
    ,[Extent1].[ReviewStatus] AS [ReviewStatus]
    ,[Extent1].[UserAccountId] AS [UserAccountId]
    ,[Extent1].[CreationDate] AS [CreationDate]
FROM
    (
        SELECT TOP (@VarPageSize)
            [Extent1].[LinkID] AS [LinkID]
            ,[Extent1].[Title] AS [Title]
            ,[Extent1].[Url] AS [Url]
            ,[Extent1].[Description] AS [Description]
            ,[Extent1].[SentDate] AS [SentDate]
            ,[Extent1].[VisitCount] AS [VisitCount]
            ,[Extent1].[RssSourceId] AS [RssSourceId]
            ,[Extent1].[ReviewStatus] AS [ReviewStatus]
            ,[Extent1].[UserAccountId] AS [UserAccountId]
            ,[Extent1].[CreationDate] AS [CreationDate]
        FROM
            (
                SELECT TOP((@VarPageNumber + 1) * @VarPageSize)
                    [Extent1].[LinkID] AS [LinkID]
                    ,[Extent1].[Title] AS [Title]
                    ,[Extent1].[Url] AS [Url]
                    ,[Extent1].[Description] AS [Description]
                    ,[Extent1].[SentDate] AS [SentDate]
                    ,[Extent1].[VisitCount] AS [VisitCount]
                    ,[Extent1].[RssSourceId] AS [RssSourceId]
                    ,[Extent1].[ReviewStatus] AS [ReviewStatus]
                    ,[Extent1].[UserAccountId] AS [UserAccountId]
                    ,[Extent1].[CreationDate] AS [CreationDate]
                FROM [dbo].[Links] AS [Extent1]
                ORDER BY [Extent1].[SentDate] DESC
            ) AS [Extent1]
        ORDER BY [Extent1].[SentDate] ASC
    ) AS [Extent1]
ORDER BY [Extent1].[SentDate] DESC

仅当 SentDate 唯一时,此查询(作为原始查询)才能始终正确运行。如果它不是唯一的,请将唯一列添加到 ORDER BY。例如,如果 LinkID 是唯一的,则在最内层的查询中使用 ORDER BY SentDate DESC, LinkID DESC。在外部查询中反转顺序:ORDER BY SentDate ASC, LinkID ASC.

显然,如果你想跳转到第1000页,那么内部查询就必须读取10,000行,所以你越往前走,速度就越慢。

在任何情况下,您都需要在 SentDate(或 SentDate, LinkID)上有一个索引才能使其工作。如果没有索引,查询将再次扫描整个 table。

我不会在这里告诉您如何将此查询转换为 EF,因为我不知道。我从未使用过 EF。也许有办法。另外,显然,您可以强制它使用实际的 SQL,而不是尝试使用 C# 代码。

更新

执行计划比较

在我的数据库中,我有一个 table EventLogErrors 有 29,477,859 行,我在 SQL Server 2008 上比较了 EF 生成的 ROW_NUMBER 查询和我的建议这里有 TOP。我试图检索第四页 10 行长。在这两种情况下,优化器都足够聪明,只读取 40 行,正如您从执行计划中看到的那样。我使用一个主键列来为这个测试排序和分页。当我使用另一个索引列进行分页时,结果是相同的,即两个变体都只读取 40 行。不用说,两种变体 returned 结果都在几分之一秒内完成。

TOP

的变体

ROW_NUMBER

的变体

这意味着你的问题的根源在别处。您提到您的查询仅 有时 运行缓慢,我最初并没有真正注意它。有了这样的症状,我会做以下事情:

  • 检查执行计划。
  • 检查您是否有索引。
  • 检查索引没有严重碎片化并且统计信息没有过时。
  • SQL 服务器有一个叫做 Auto-Parameterization. Also, it has a feature called Parameter Sniffing. Also, it has a feature called Execution plan caching. When all three features work together it may result in using a non-optimal execution plan. There is an excellent article by Erland Sommarskog explaining it in detail: http://www.sommarskog.se/query-plan-mysteries.html 的功能本文解释了如何通过检查缓存的执行计划来确认问题是否真的在参数嗅探中,以及如何解决这个问题.

我曾 运行 遇到类似的问题,之前 EF 将决定装饰 SQL 它决定以非常低效的方式 运行。

无论如何,为您的问题提供一个可能的解决方案:

在我不喜欢 EF 使用我的代码生成 SQL 语句的情况下,我最终编写了一个存储过程,将其作为函数导入到我的 EDMX 中并使用它来检索我的数据。它使我能够控制如何制定 SQL,并且我确切地知道我需要利用什么索引来从中获得最佳性能。我想您知道如何编写存储过程并将其作为函数导入到 EF 中,因此我将省略这些细节。希望这对你有帮助。

我仍会继续查看此页面,看看是否有人针对您的问题提出了更好、更轻松的解决方案。

你的代码对我来说有点晦涩难懂,这是我第一次遇到这样的查询。正如您所说,有时执行时间太长,因此它告诉查询可以在某处以另一种方式解释,在某些情况下可能会忽略 EF performance considerations,因此 尝试重新排列查询 conditions/selections考虑在你的程序逻辑中延迟加载.

这看起来像是一个标准的分页查询。我猜你没有 SentDate 的索引。如果是这样,首先要尝试的是在 SentDate 上添加一个索引,看看这对性能有什么样的影响。假设您并不总是希望在 SentDate 上 sort/page 并且索引您可能希望 sort/page 的每一列不会发生,请查看 this other Whosebug question。在某些情况下,SQL 服务器的 "Gather Streams" 并行操作会溢出到 TempDb 中。发生这种情况时,性能就会下降。正如另一个答案所说,索引列可以帮助禁用并行性。检查您的查询计划,看看这是否可能是问题所在。

你是不是被SQL服务器的Statistic更新问题给坑了?

改变数据库 YourDBName 设置AUTO_UPDATE_STATISTICS_ASYNC开

默认关闭,因此您的 SQL 服务器将在 20% 的数据发生更改时停止 - 在 运行 查询之前等待统计信息更新。

有时内部 select 会导致执行计划出现问题,但这是从代码构建表达式树的最简单方法。通常,它不会对性能造成太大影响。

显然在这种情况下是这样。一种解决方法是使用您自己的 ExecuteStoreQuery 查询。像这样:

int takeNo = 20;
int skipNo = 100;

var results = db.ExecuteStoreQuery<Link>(
    "SELECT LinkID, Title, Url, Description, SentDate, VisitCount, RssSourceId, ReviewStatus, UserAccountId, CreationDate FROM Links", 
    null);

results = results.OrderBy(x=> x.SentDate).Skip(skipNo).Take(takeNo);

当然,这样做首先会失去使用 ORM 的很多好处,但在特殊情况下这可能是可以接受的。

叫我疯子,但看起来当调用这段代码时你已经得到了自己排序的东西:

if (orderBy != null)
{
    query = orderBy(query);
}

我认为这可以解释整个 "sometimes it's slow" 位。可能运行良好,直到 orderBy 参数中有内容,然后它会调用自身并创建编号为 sub-select 的行,这会减慢它的速度。

尝试注释掉代码的 query = orderBy(query) 部分,看看速度是否仍然会变慢。我打赌你不会。

此外,您可以使用 Dynamic LINQ 简化代码。它基本上允许您使用字段的字符串名称 (.orderby("somefield")) 进行特定排序,而不是尝试传入一个方法,我发现这要容易得多。我在 MVC 应用程序中使用它来处理用户在网格上单击的任何字段的排序。

我的 EF 不是很好,但可以给你提示。首先,您必须检查 [Extent1].[SentDate] 上是否有非聚集索引。其次,如果不存在,则创建,如果存在,则重新创建或重新安排它。

第三次像这样更改您的查询。因为你原来的 SQL 不是什么只是写的不必要的复杂,它的结果和我在这里展示的一样。尽量写得简单,工作起来会更快,维护起来也容易。

SELECT TOP (10) 
[Extent1].[LinkID] AS [LinkID], 
[Extent1].[Title] AS [Title], 
[Extent1].[Url] AS [Url], 
[Extent1].[Description] AS [Description], 
[Extent1].[SentDate] AS [SentDate], 
[Extent1].[VisitCount] AS [VisitCount], 
[Extent1].[RssSourceId] AS [RssSourceId], 
[Extent1].[ReviewStatus] AS [ReviewStatus], 
[Extent1].[UserAccountId] AS [UserAccountId], 
[Extent1].[CreationDate] AS [CreationDate]
FROM [dbo].[Links] AS [Extent1]
ORDER BY [Extent1].[SentDate] DESC

如果结果不同,或者像这样稍微修改一下。

select top 10 A.* from (
SELECT * from
[Extent1].[LinkID] AS [LinkID], 
[Extent1].[Title] AS [Title], 
[Extent1].[Url] AS [Url], 
[Extent1].[Description] AS [Description], 
[Extent1].[SentDate] AS [SentDate], 
[Extent1].[VisitCount] AS [VisitCount], 
[Extent1].[RssSourceId] AS [RssSourceId], 
[Extent1].[ReviewStatus] AS [ReviewStatus], 
[Extent1].[UserAccountId] AS [UserAccountId], 
[Extent1].[CreationDate] AS [CreationDate]
FROM [dbo].[Links] AS [Extent1] ) A
ORDER BY A.[SentDate] DESC 

我 99% 确定它会起作用。

尝试在 SentDate 上添加非聚集索引