EF生成的查询需要太多时间才能执行

时间:2015-03-19 07:16:14

标签: c# sql sql-server entity-framework sql-server-2008-r2

我有一个非常简单的查询,它是由Entity-Framework生成的, 有时 当我尝试运行此查询时执行它几乎需要超过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子句并将其更改为以下时,Query会在不到一秒的时间内执行。

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));

11 个答案:

答案 0 :(得分:9)

我从未想过你根本就没有索引。获得的经验教训 - 在进一步挖掘之前,请务必检查基础知识。


如果您不需要分页,则可以将查询简化为

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

它运行得很快,因为你已经验证了。

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

您当前版本速度慢的原因,因为它首先扫描整个表,计算每行的行数,然后返回10行。 我错了。 SQL Server优化器相当聪明。 问题的根源是其他地方。请参阅下面的更新。


正如其他人所提到的,BTW只有在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,然后在ID子句中使用此WHERE并使用适当的支持索引来检索下一页而不扫描所有先前的行

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。在内部查询中,我们选择前40行。请注意,我们扫描整个表格,我们只扫描前40行。我们甚至不需要明确的ROW_NUMBER()。然后我们需要从找到的那些中选择最后10行40,因此外部查询在相反的方向上选择TOP(10) ORDER BY。这样就会以相反的顺序返回行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)上设置索引才能使其正常运行。如果没有索引,查询将再次扫描整个表。

我在这里没有告诉你如何将这个查询翻译成EF,因为我不知道。我从未使用EF。可能有办法。另外,显然,您可以强制它使用实际的SQL,而不是尝试使用C#代码。

更新

执行计划比较

在我的数据库中,我有一个包含29,477,859行的表EventLogErrors,我在SQL Server 2008上将EF生成的查询与ROW_NUMBER进行了比较,并在此处使用TOP进行了比较。我试图检索第10页10行长。在这两种情况下,优化器都足够智能,只能读取40行,正如您可以从执行计划中看到的那样。我使用主键列进行此测试的排序和分页。当我使用另一个索引列进行分页时,结果是相同的,即两个变体只读取40行。毋庸置疑,两种变体都会在几分之一秒内返回。

变体TOP

Variant with TOP

变体ROW_NUMBER

Variant with ROW_NUMBER

这一切意味着问题的根源在于其他地方。您提到您的查询运行缓慢只有有时,我最初并没有真正关注它。有了这样的症状我会做以下事情:

  • 检查执行计划。
  • 检查您是否有索引。
  • 检查索引是否碎片严重,统计信息不会过时。
  • SQL Server有一个名为Auto-Parameterization的功能。此外,它还有一个名为Parameter Sniffing的功能。此外,它还有一个名为Execution plan caching的功能。当所有三个功能协同工作时,可能会导致使用非最佳执行计划。 Erland Sommarskog有一篇很好的文章详细解释了它:http://www.sommarskog.se/query-plan-mysteries.html本文解释了如何通过检查缓存的执行计划以及解决问题可以采取的措施来确认问题确实存在于参数嗅探中。

答案 1 :(得分:5)

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

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

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

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

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

答案 2 :(得分:2)

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

显然,在这种情况下确实如此。一种解决方法是使用您自己的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的许多好处,但在特殊情况下这可能是可以接受的。

答案 3 :(得分:1)

这看起来像标准的分页查询。我猜你在SentDate上没有索引。如果是这样,首先要尝试的是在SentDate上添加一个索引,看看它对性能产生了什么样的影响。假设您并不总是希望对SentDate进行排序/分页,并且索引每个您可能想要排序/分页的列不会发生,请查看this other stackoverflow question。在某些情况下,SQL Server&#34; Gather Streams&#34;并行操作可以溢出到TempDb中。当发生这种情况时,性能会进入厕所。正如另一个答案所说,索引列可能会有所帮助,因为可以禁用并行性。查看您的查询计划,看看是否可能出现问题。

答案 4 :(得分:1)

我在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%肯定它会起作用。

答案 5 :(得分:0)

您是否尝试过链接方法?

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

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

答案 6 :(得分:0)

在EF决定装饰它决定以非常不同的方式运行的SQL之前,我遇到过类似的问题。

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

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

我仍会继续查看此页面,看看是否有人为您的问题找到了更好,更少痛苦的解决方案。

答案 7 :(得分:0)

你的代码对我来说有点模糊,这是我第一次遇到这样的查询。正如您所说,有时执行需要太长时间,因此它告诉查询可以在某处以其他方式解释,可能在某些情况下忽略EF performance considerations,所以尝试重新排列查询条件/选择< / strong>和考虑在程序逻辑中延迟加载

答案 8 :(得分:0)

你是不是被SQL服务器中的统计更新问题所困扰?

ALTER DATABASE YourDBName SET AUTO_UPDATE_STATISTICS_ASYNC ON

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

答案 9 :(得分:0)

叫我疯了,但是当你调用这段代码时,看起来你已经有了自己的命令:

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

我认为这可以解释整个&#34;有时它很慢&#34;位。可能运行正常,直到你在orderBy参数中有一些东西,然后它自己调用并创建那个编号为sub-select的行,这会减慢它的速度。

尝试评论代码的query = orderBy(query)部分,看看你是否仍然放慢速度。我打赌你没赢。

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

答案 10 :(得分:0)

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