文章出處

應用場景

最近被應用程序中頁面加載慢的問題所折磨,看似容易的問題,其實并不容易(已經持續兩天時間了),經過“偵查”,發現了兩個“嫌疑犯”:

  1. EntityFramework 生成執行的 SQL
  2. 數據庫中索引創建

在《程序員眼中的 SQL Server-非聚集索引能給我們帶來什么?》這一篇博文中,我把懷疑對象放在了數據庫索引上,其實索引只是一方面的問題,最后通過仔細觀察 EntityFramework 生成執行的 SQL 代碼(EntityFramework 中如何查看執行 SQL?),主要查看的是獲取分頁列表的 SQL,最后發現,在分頁列表獲取的時候,執行的 SQL 是所有條件,并沒有分頁,也沒有 OrderBy,這是個奇怪的問題,因為結果是分頁的,但是在數據庫執行的 SQL 代碼,卻是獲取所有列表,我們來看一下,這是個什么情況???

上面所說的有點亂,我再重述下應用場景:我們使用 EntityFramework 做分頁查詢,一般一行代碼就可以搞定(Linq 的強大),比如:

var list = query.where(...).OrderByDescending(...).Skip(...).Take(...).ToList();

對,你沒看錯,就是這么簡單,這也是我們一般所使用的方法,在一般應用中可能不會出現什么問題,但是當數據量非常大的時候,而且你的代碼經過不斷的改寫,這時候可能就有些問題了,在我現在的應用項目中,主要是 OrderByDescending 這個排序沒有在 SQL 執行,因為這個原因,還導致后面的分頁沒有執行,但是運行的結果是分頁的,查看到的執行 SQL 是獲取所有 where 條件下的列表,也就是說分頁并不是在數據庫中執行的,而是獲取到內存中,然后再進行的分頁,這導致兩個問題,一個就是內存飆升(獲取列表數很大的情況),還有一個當然是頁面訪問慢。

問題分析

為了方面大家理解,我先貼一下現在應用程序中分頁部分的代碼:

        protected override IEnumerable<TAggregateRoot> FindAll(ISpecification<TAggregateRoot> specification, System.Linq.Expressions.Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder, int pageNumber, int pageSize)
        {
            if (pageNumber <= 0)
                throw new ArgumentOutOfRangeException("pageNumber", pageNumber, "The pageNumber is one-based and should be larger than zero.");
            if (pageSize <= 0)
                throw new ArgumentOutOfRangeException("pageSize", pageSize, "The pageSize is one-based and should be larger than zero.");

            var query = efContext.Context.Set<TAggregateRoot>()
                .Where(specification.GetExpression());
            int skip = (pageNumber - 1) * pageSize;
            int take = pageSize;

            if (sortPredicate != null)
            {
                switch (sortOrder)
                {
                    case SortOrder.Ascending:
                        return query.OrderBy(sortPredicate.Compile()).Skip(skip).Take(take).ToList();
                    case SortOrder.Descending:
                        return query.OrderByDescending(sortPredicate.Compile()).Skip(skip).Take(take).ToList();
                    default:
                        break;
                }
            }
            return query.Skip(skip).Take(take).ToList();
        }

你能看出什么問題嗎?看似沒什么問題,執行后就會出現上面我所描述的那個問題,我原來以為是 Where 和 OrderBy 順序的問題,現在看來還蠻可笑的,后來把中間查詢的代碼修改了下,進行測試:

var query = efContext.Context.Set<TAggregateRoot>()
                .Where(specification.GetExpression()).OrderBy(p=>p.ID).Skip(skip).Take(take);

OrderBy 沒有使用參數傳過來的 lambda 表達式,而是用聚合根的 ID 進行排序,通過查看生成的 SQL,是正確的分頁代碼,也就是說問題出在 sortPredicate 參數,或者說是 Expression<Func<TAggregateRoot, dynamic>> 的參數類型上面,EntityFramework(準確的說應該是是 Linq)中 OrderBy 方法的參數類型是 Expression<Func<TSource, TKey>>,TSource 表示數據源類型,TKey 表示返回值類型(注意委托類型為 Func),比如這個參數: p=>p.ID,就表示數據源類型為 TAggregateRoot,返回值類型為 int,執行排序就是 ID 字段。

因為我們數據源類型使用的是 TAggregateRoot,在排序字段類型指定方面,我們沒辦法具體的指定(比如 p=>p.Name 等),因為 TAggregateRoot 類型中只有一個 ID 屬性,所以我們必須通過表達式進行傳遞,也就是參數 sortPredicate,可以看到數據源類型為 TAggregateRoot,返回值類型(或者稱為排序字段類型)為 dynamic,這個表示動態編譯類型,沒怎么了解過,只是知道大體意思。數據源類型沒什么問題,因為我們 where 條件就是這樣用的,那就是排序字段類型的問題,關于這個問題,經過反復測試,也沒有好的方式解決,我就網上搜了下,對我有所幫助的資源有下面三個:

  1. EF orderby / thenby combo extension method
  2. Help me understand “LINQ to Entities only supports casting Entity Data Model primitive types”
  3. c# 擴展方法 奇思妙用 高級篇 九:OrderBy(string propertyName, bool desc)

還有一個被我關掉找不到了,大概意思是和第二個一樣的,都是用范型指定排序類型,第三個是園友寫的,很不錯,我原來以為看到希望了,但是發現和我的應用場景不太一樣,比如:var orderedQueryable = Queryable.OrderBy(repository, (dynamic)keySelector); 這段代碼中的 repository 就不知其意思,我最后采用的方式是用范型指定排序類型,經過測試是可以的,代碼如下:

        protected override IEnumerable<TAggregateRoot> DoFindAll<TOrderSort>(ISpecification<TAggregateRoot> specification, System.Linq.Expressions.Expression<Func<TAggregateRoot, TOrderSort>> sortPredicate, SortOrder sortOrder, int pageNumber, int pageSize)
        {
            if (pageNumber <= 0)
                throw new ArgumentOutOfRangeException("pageNumber", pageNumber, "The pageNumber is one-based and should be larger than zero.");
            if (pageSize <= 0)
                throw new ArgumentOutOfRangeException("pageSize", pageSize, "The pageSize is one-based and should be larger than zero.");

            var query = efContext.Context.Set<TAggregateRoot>()
                .Where(specification.GetExpression());
            int skip = (pageNumber - 1) * pageSize;
            int take = pageSize;

            if (sortPredicate != null)
            {
                switch (sortOrder)
                {
                    case SortOrder.Ascending:
                        return query.OrderBy(sortPredicate).Skip(skip).Take(take).ToList();
                    case SortOrder.Descending:
                        return query.OrderByDescending(sortPredicate).Skip(skip).Take(take).ToList();
                    default:
                        break;
                }
            }
            return query.Skip(skip).Take(take).ToList();
        }

代碼改過之后,通過 Sql Server Profiler 在數據庫中跟蹤 SQL 執行,終于發現了 Top 關鍵字(分頁產生的頁碼數量),然后又對數據庫中的索引進行了調整,頁面加載慢的問題終于得到了一定解決。

看似改一點代碼的問題,卻花了這么長時間,結果很簡單,過程卻很復雜,就記錄到這。


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()