繁体   English   中英

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

[英]Query generated by EF takes too much time to execute

我有一个由实体框架生成一个非常简单的查询, 有时,当我尝试运行此查询它几乎需要被执行超过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));

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


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

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 ,然后在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 在内部查询中,我们选择前40行。 注意,我们这里扫描整个表,我们只扫描前40行。 我们甚至不需要显式的ROW_NUMBER() 然后我们需要从找到的那些中选择最后10行40,因此外部查询在相反的方向上选择带有ORDER BY TOP(10) 这样就会以相反的顺序返回行40到31。 您可以在客户端上将它们重新排序为正确的顺序,或者再添加一个外部查询,只需通过SentDate DESC它们进行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 )上SentDate索引才能使其正常工作。 如果没有索引,查询将再次扫描整个表。

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

更新

执行计划比较

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

变种与TOP

变种与TOP

变体与ROW_NUMBER

变体与ROW_NUMBER

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

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

我猜测,当你要求第2页,第3页等时, WHERE row_number > 0会随时间变化...

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

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

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

更新 :检查评论! 原来DESC没有任何影响,如果您的数据从低到高,应该避免!

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

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

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

我不是很擅长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%肯定它会起作用。

你试过链接方法吗?

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

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

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

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

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

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

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

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

ALTER DATABASE YourDBName SET AUTO_UPDATE_STATISTICS_ASYNC ON

默认为OFF,因此当20%的数据发生更改时,SQL Server将停止 - 在运行查询之前等待Statistics更新。

叫我疯了,但看起来你在调用这段代码的时候已经有了自己的事情:

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

我认为这可以解释整个“有时它很慢”的一点。 可能运行正常,直到你在orderBy参数中有东西,然后它调用自己并创建编号为sub-select的行减慢它。

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM