簡體   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