EF性能優化-有人說EF性能低,我想說:EF確實不如ADO.NET


十年河東,十年河西,莫欺少年窮。

EF就如同那個少年,ADO.NET則是一位壯年。畢竟ADO.NET出生在EF之前,而EF所走的路屬於應用ADO.NET。

也就是說:你所寫的LINQ查詢,最后還是要轉化為ADO.NET的SQL語句,轉化過程中無形降低了EF的執行效率。

但是,使用EF的一個好處就是系統便於維護,減少了系統開發時間,降低了生成成本。

OK,上述只是做個簡單的對比,那么在實際編碼過程中,我們應當怎樣提升EF的性能呢?

工欲善其事,必先利其器。

我們使用EF和在很大程度提高了開發速度,不過隨之帶來的是很多性能低下的寫法和生成不太高效的sql。

雖然我們可以使用SQL Server Profiler來監控執行的sql,不過個人覺得實屬麻煩,每次需要打開、過濾、清除、關閉。

在這里強烈推薦一個插件MiniProfiler。實時監控頁面請求對應執行的sql語句、執行時間。簡單、方便、針對性強。

如圖:

關於MiniProfiler的使用,大家可參考:MiniProfiler工具介紹(監控加載用時,EF生成的SQL語句)--EF,迷你監控器,哈哈哈

1、EF使用SqlQuery

上述已經說的很明白了,EF效率低於ADO.NET是因為LINQ-TO-SQL的過程消耗了時間。而使用SqlQuery則可以直接寫SQL語句。

當然,如果你想得到更快的執行速度,你也可以在數據庫上寫存儲過程PROC

關於SqlQuery的用法,在此不作解釋。

2、EF使用AsNoTracking(),無跟蹤查詢技術(查詢出來的數據不可以修改,如果你做了修改,你會發現修改並不成功)

2.1、測試修改:

 var student = context.Student.AsNoTracking().Where(A => A.Id == 2).FirstOrDefault() ;
                    student.StuName = "毛毛";
                    context.SaveChanges();

上述代碼嘗試修改數據,程序運行完以后,我們會發現數據庫Id為2的學生的姓名並沒有修改,因此,采用無跟蹤查詢技術得到的數據是不可以進行修改的。

2.2、性能測試:

代碼測試如下:

public ActionResult Index()
        {  
            var profiler = MiniProfiler.Current;
            using (profiler.Step("高性能查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var a = context.Student.AsNoTracking().Where(A => A.StuName.Contains("")).ToList();
                    
                }
            }
            using (profiler.Step("查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var b = context.Student.Where(A => A.StuName.Contains("")).ToList();

                }
            }
            return View();
        }
View Code

性能對比如下:

 

注意:(因為我使用的是本地數據庫,所以效率差別不是很大,如果是遠程數據庫且數據量比較大,性能會提升很多,有測試證明:其性能可提升4~5倍)

  • AsNoTracking干什么的呢?無跟蹤查詢而已,也就是說查詢出來的對象不能直接做修改。所以,我們在做數據集合查詢顯示,而又不需要對集合修改並更新到數據庫的時候,一定不要忘記加上AsNoTracking。
  • 如果查詢過程做了select映射就不需要加AsNoTracking。如:db.Students.Where(t=>t.Name.Contains("張三")).select(t=>new (t.Name,t.Age)).ToList();

3、性能提升之AsNonUnicode

代碼測試如下:

public ActionResult Index()
        {  
            var profiler = MiniProfiler.Current;
           
            using (profiler.Step("查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var b = context.Student.Where(A => A.StuName=="趙剛").ToList();

                }
            }
            using (profiler.Step("高性能查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var a = context.Student.AsNoTracking().Where(A => A.StuName == DbFunctions.AsNonUnicode("趙剛")).ToList();

                }
            }
            return View();
        }
View Code

性能對比如下:

從上圖可以看出,生成了兩條基本相同的SQL語句,唯獨不相同的地方是:不加AsNonUnicode SQL中會有 N,加了AsNonUnicode后,SQL中沒有N 

使用 N 前綴(查詢過程中需要把數據庫默認格式轉化為Unicode 格式來查詢,因此:性能被拉低)

在服務器上執行的代碼中(例如在存儲過程和觸發器中)顯示的 Unicode 字符串常量必須以大寫字母 N 為前綴。即使所引用的列已定義為 Unicode 類型,也應如此。

不使用 N 前綴

如果不使用 N 前綴,字符串將轉換為數據庫的默認代碼格式。這可能導致不識別某些字符。

因此,關於 AsNonUnicode 的的使用,還要結合具體情況。 

4、多字段組合排序(字符串)先按照學號排序,再按姓名排序(請將排序OrderBy放在構造LINQ的最后)

錯誤代碼如下:

            using (profiler.Step("查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var b1 = context.Student.Where(A => A.StuName.StartsWith("")).OrderBy(A => A.StuNum).OrderBy(A => A.StuName).ToList();

                }
            }

正確代碼如下:

            using (profiler.Step("高性能查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var b2 = context.Student.Where(A => A.StuName.StartsWith("")).OrderBy(A => A.StuNum).ThenBy(A => A.StuName).ToList();

                }
            }

由上圖得到的結果分析可知:錯誤代碼連續使用兩個OrderBy,導致后面的OrderBy覆蓋了前面的OrderBy,也就是說:錯誤代碼是按照姓名排列的。

因此,涉及連續排序時,要用ThenBy。

5、foreach循環的陷進 

5.1、關於延遲加載

請看上圖紅框。為什么StudentId有值,而Studet為null?因為使用code first,需要設置導航屬性為virtual,才會加載延遲加載數據。

加了virtual后,我們就可以使用延遲加載了。但是,如果用上述的ForEach循環,會產生嚴重的性能問題。

如下:

我們通過 MiniProfiler工具 監控下生成的SQL語句,如下

生成了101條SQL語句,是不是很嚇人。

 那我們應當怎么正確的使用懶加載呢?

解決方案:使用Include顯示連接查詢(注意:需要手動導入using System.Data.Entity 不然Include只能傳表名字符串)。

加上了Include后,懶加載就變成了顯示加載,也就是說帶有Virtual的懶加載字段信息會被一次加載出來,因此:使用 Include 后,只會生成一條SQL語句!

 

再看MiniProfiler的監控(瞬間101條sql變成了1條,這其中的性能可想而知。)

因此,性能會大大滴提升哦。

6、AutoMapper的使用

所謂AutoMapper即:自動映射,關於AutoMapper的使用,大家可參考我的博客:AutoMapper自動映射

下面結合數據庫來看如下示例:

數據表關系:

create table Dept
(
Id int identity(1,1) not null,
deptNum varchar(20) not null primary key,
deptName nvarchar(20) default('計算機科學與工程系'),
)


create table Student
(
Id int identity(1,1) not null,
StuNum varchar(20) primary key,
deptNum varchar(20) FOREIGN KEY (deptNum) REFERENCES Dept (deptNum), 
StuName nvarchar(10),--
StuSex nvarchar(2) default(''),
AddTime datetime default(getdate()),
)

很簡單。系表和學生表,有個外鍵deptNum,

EF中生成的DTO如下:

namespace BingFa.Entity
{
    using System;
    using System.Collections.Generic;
    
    public partial class Student
    {
        public int Id { get; set; }
        public string StuNum { get; set; }
        public string deptNum { get; set; }
        public string StuName { get; set; }
        public string StuSex { get; set; }
        public Nullable<System.DateTime> AddTime { get; set; }
    
        public virtual Dept Dept { get; set; }
    }
}

namespace BingFa.Entity
{
    using System;
    using System.Collections.Generic;
    
    public partial class Dept
    {
        public Dept()
        {
            this.Student = new HashSet<Student>();
        }
    
        public int Id { get; set; }
        public string deptNum { get; set; }
        public string deptName { get; set; }
    
        public virtual ICollection<Student> Student { get; set; }
    }
}

Model層

    public class StudentModel
    {
        public int Id { get; set; }
        public string StuNum { get; set; }
        public string deptNum { get; set; }
        public string StuName { get; set; }
        public string StuSex { get; set; }
        public Nullable<System.DateTime> AddTime { get; set; }
        public string deptName { get; set; }
    }

測試代碼如下:

由上述代碼得知,我們需要根據導航屬性獲取系名。

同理,如果你有很多導航屬性,你亦可以多寫幾次 ForMember(......) ,但是這樣做會陷入延遲加載的陷阱

針對上述的寫法,我們的監測如下:

可以看出竟然生成了兩條SQL語句,如果你用了N個導航屬性,那么就會生成N+1個SQL語句,這顯然是不能接受的,怎么辦呢?

同上述,ForEach的陷阱一樣,我們可以派上Include,如下:

加上了AsNoTracking無跟蹤查詢技術,這個是用來提升查詢性能。同時加上了Include,用於顯示加載,從而避免了懶加載生成SQL的問題。

監測如下:

由此可知,僅僅生成了一條SQL語句,SQL查詢性能也提升了很多,因此在使用AutoMapper時,切記別陷入這種陷阱。

其實,說白了,其實都是懶加載惹的禍,用不好的話,懶加載會讓你很累的哦。

7、count(*)被你用壞了嗎(Any的用法)

要求:查詢是否存在名字為“張三2”的學生。(你的代碼會怎樣寫呢?)

用第一種?第二種?第三種?呵呵,我以前就是使用的第一種,然后有人說“你count被你用壞了”,后來我想了想了怎么就被我用壞了呢?直到對比了這三個語句的性能后我知道了。

看到監控后,瞬間驚呆了,count(*)的性能竟然最低,Any的性能最高。性能之差竟有三百多倍,count確實被我用壞了。(我想,不止被我一個人用壞了吧。)

我們看到上面的Any干嘛的?官方解釋是:

我反復閱讀這個中文解釋,一直無法理解。甚至早有人也提出過同樣的疑問《實在看不懂MSDN關於 Any 的解釋

所以我個人理解也是“確定集合中是否有元素滿足某一條件”。我們來看看any其他用法:

要求:查詢教過“張三”或“李四”的老師

實現代碼:

兩種方式,以前我會習慣寫第一種。當然我們看看生成過的sql和執行效率之后,看法改變了。

效率之差竟有近六倍。

我們再對比下count:

得出奇怪的結論:

  1. 在導航屬性里面使用count和使用any性能區別不大,反而FirstOrDefault() != null的方式性能最差。
  2. 在直接屬性判斷里面any和FirstOrDefault() != null性能區別不大,count性能要差的多。
  3. 所以,不管是直接屬性還是導航屬性我們都用any來判斷是否存在是最穩當的。

8、動態創建LINQ子查詢

查詢姓 張 李 王 的男人

LINQ 如下:

var Query = from P in persons1
                            where (P.Name.Contains("") || P.Name.Contains("") || P.Name.Contains(""))&&P.Sex==""
                            select new PersonModel
                            {
                                Name = P.Name,
                                Sex = P.Sex,
                                Age = P.Age,
                                Money = P.Money
                            };

現在需求變更如下:查詢姓 張 李 王 的男人 並且 年齡要大於20歲

LINQ 變更如下:

var Query = from P in persons1
                            where (P.Name.Contains("") || P.Name.Contains("") || P.Name.Contains(""))&&P.Sex==""&&P.Age>20
                            select new PersonModel
                            {
                                Name = P.Name,
                                Sex = P.Sex,
                                Age = P.Age,
                                Money = P.Money
                            };

好了,如果您認為上述構建WHERE子句的方式就是動態構建的話,那么本篇博客就沒有什么意義了!

那么什么樣的方式才是真正的動態構建呢?

OK,咱們進入正題:

在此我提出一個簡單需求如下:

我相信我的需求提出后,你用上述方式就寫不出來了,我的需求如下:

請根據數組中包含的姓氏進行查詢:

數組如下:

string[] xingList = new string[] { "", "", "", "", "", "", "", "", "", "" };

在這里,有人可能會立馬想到:分割數組,然后用十個 || 進行查詢就行了!

我要強調的是:如果數組是動態的呢?長度不定,包含的姓氏不確定呢?

呵呵,想必寫不出來了吧!

還好,LINQ也有自己的一套代碼可以實現(如果LINQ實現不了,那么早就沒人用LINQ了):

由於代碼比較多,在此大家可參考:LINQ 如何動態創建 Where 子查詢

代碼如下:

public BaseResponse<IList<MessageModel>> GetMessageList(string Tags, string Alias, int pageSize, int pageIndex)
        {
            BaseResponse<IList<MessageModel>> response = new BaseResponse<IList<MessageModel>>();
            var msg = base.unitOfWork.GetRepository<MSG_Message>().dbSet.Where(A=>!A.IsDeleted);//
            var Query = from M in msg
                        select new MessageModel
                        {
                            CreatedTime = M.CreatedTime,
                            MessageContent = M.MessageContent,
                            MessageID = M.MessageID,
                            MessageTitle = M.MessageTitle,
                            MessageType = M.MessageType,
                            Tags=M.Tags,
                            Alias=M.Alias
                        };
            ParameterExpression c = Expression.Parameter(typeof(MessageModel), "c");
            Expression condition = Expression.Constant(false);
            if (!string.IsNullOrEmpty(Tags))
            {
                string[] TagsAry = new string[] { };
                TagsAry = Tags.Split(',');
               
                foreach (string s in TagsAry)
                {
                    Expression con = Expression.Call(
                        Expression.Property(c, typeof(MessageModel).GetProperty("Tags")),
                        typeof(string).GetMethod("Contains", new Type[] { typeof(string) }),
                        Expression.Constant(s));
                    condition = Expression.Or(con, condition);
                }

              
            }
            if (!string.IsNullOrEmpty(Alias))
            {
                Expression con_Alias = Expression.Call(
                     Expression.Property(c, typeof(MessageModel).GetProperty("Alias")),
                     typeof(string).GetMethod("Contains", new Type[] { typeof(string) }),
                     Expression.Constant(Alias));
                condition = Expression.Or(con_Alias, condition);
                //
            }
            Expression<Func<MessageModel, bool>> end =
    Expression.Lambda<Func<MessageModel, bool>>(condition, new ParameterExpression[] { c });

            Query = Query.Where(end);
            //
            response.RecordsCount = Query.Count();
            //
            List<MessageModel> AllList = new List<MessageModel>();
            List<MessageModel> AllList_R = new List<MessageModel>();
            AllList_R = Query.ToList();
            AllList = AllList_R.Where(A => A.Alias.Contains(Alias)).ToList();//加載所有Alias的 
            for (int i = 0; i < AllList_R.Count; i++)
            {
                string[] TagsAry = new string[] { };
                if (!string.IsNullOrEmpty(AllList_R[i].Tags))
                {
                    TagsAry = AllList_R[i].Tags.Split(',');
                    bool bol = true;
                    foreach (var Cm in TagsAry)
                    {
                        if (!Tags.Contains(Cm))
                        {
                            bol = false;
                            break;
                        }
                    }
                    if (bol)
                    {
                        AllList.Add(AllList_R[i]);
                    }
                }
            }
            AllList = AllList.OrderByDescending(A => A.CreatedTime).ToList();
            if (pageIndex > 0 && pageSize > 0)
            {
                AllList = AllList.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
                response.PagesCount = GetPagesCount(pageSize, response.RecordsCount);

            }
            response.Data = AllList;
            return response;

        }
View Code

需要指出的是:

Expression.Or(con, condition);  邏輯或運算
Expression.And(con, condition); 邏輯與運算

代碼分析:

生成的LINQ子查詢類似於:c=>c.Tags.Contains(s) || c=>c.Alias.Contains(Alias)....

9、真分頁與假分頁(了解 IQueryable,IEnumerable的區別)

 大家都知道分頁是非常常用的功能,但是在使用EF寫分頁語句的時候,稍有不慎,真分頁便會成為假分頁:

上述兩個看似類似的LINQ語句,實際執行起來效率差了很多。其原因是ToList使用的位置,當你ToList()時,EF會將linq轉化為SQL,然后執行。

第一個LINQ我們可理解為:先把數據全部都查詢出來,然后分頁

第二個LINQ我們可理解為:只查詢分頁所需的N條數據。如果你有100萬條數據,第一種方法會全部查詢出來,第二種方法僅僅會查詢分頁所需的10條數據,其性能對比可想而知。

10、批量刪除和修改

不知道你是否研究過EF的插入刪除和修改操作,當你批量操作數據的時候,通過SQL Server Profiler可以明顯看到產生了大量的Insert,Update語句,效率非常低;因為他插入一條數據,會對應生成一條Insert語句,當你的list中有10萬條數據時,就會生成10萬條插入語句!不過還好咱們有對策:Entity Framework Extendeds ,EF擴展類完美解決批量操作問題:

要使用AddRange,一次性插入10萬條數據。

11、EF使用存儲過程

在此貼出我的存儲過程(我這個存儲過程也是處理並發的存儲過程),關於並發處理大家可參考:C# 數據庫並發的解決方案(通用版、EF版)

create proc LockProc --樂觀鎖控制並發
(
@ProductId int, 
@IsSuccess bit=0 output
)
as
declare @count as int
declare @flag as TimeStamp
declare @rowcount As int 
begin tran
select @count=ProductCount,@flag=VersionNum from Inventory where ProductId=@ProductId
 
update Inventory set ProductCount=@count-1 where VersionNum=@flag and ProductId=@ProductId
insert into InventoryLog values('插入一條數據,用於計算是否發生並發',GETDATE())
set @rowcount=@@ROWCOUNT
if @rowcount>0
set @IsSuccess=1
else
set @IsSuccess=0
commit tran

EF執行存儲過程的方法如下:

#region 通用並發處理模式 存儲過程實現
        /// <summary>
        /// 存儲過程實現
        /// </summary>
        public void SubMitOrder_2()
        {
            int productId = 1;
            bool bol = LockForPorcduce(productId);
            //1.5  模擬耗時
            Thread.Sleep(500); //消耗半秒鍾
            int retry = 10;
            while (!bol && retry > 0)
            {
                retry--;
                LockForPorcduce(productId);
            }
        }


        private bool LockForPorcduce(int ProductId)
        {
            using (BingFaTestEntities context = new BingFaTestEntities())
            {
                SqlParameter[] parameters = {
                    new SqlParameter("@ProductId", SqlDbType.Int),
                    new SqlParameter("@IsSuccess", SqlDbType.Bit)
                    };
                parameters[0].Value = ProductId;
                parameters[1].Direction = ParameterDirection.Output;
                var data = context.Database.ExecuteSqlCommand("exec LockProc @ProductId,@IsSuccess output", parameters);
                string n2 = parameters[1].Value.ToString();
                if (n2 == "True")
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
        #endregion
View Code

12、EF Contains、StartsWith、EndsWith

請看如下代碼:

        public ActionResult Index()
        {
            var profiler = MiniProfiler.Current;

            using (profiler.Step("查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var data = context.Student.Where(A => A.StuName.StartsWith("")).ToList();
                }
                return View();
            }
        }
View Code

生成了按照Unicode字符集進行的模糊查詢,生成的SQL帶N

如何優化呢?首先我們按照本篇博客第三條:3、性能提升之AsNonUnicode 我們按照數據庫默認編碼查詢來提升效率。

        public ActionResult Index()
        {
            var profiler = MiniProfiler.Current;

            using (profiler.Step("查詢Student的數據"))
            {
                using (BingFaTestEntities context = new BingFaTestEntities())
                {
                    var data = context.Student.Where(A => A.StuName.StartsWith(DbFunctions.AsNonUnicode(""))).ToList();
                }
                return View();
            }
        
View Code

根據生成的SQL語句,可以看出查詢沒有帶N,執行時間為32.4秒,效率增加一倍。

除了上述優化之外,還要看公司項目的具體要求,如果要求進行雙向匹配,那么你只能老老實實的采用Contains,如果公司只要求單項匹配,你可以采用StartsWith、EndsWith

當然,要想模糊查詢相率高些,單項匹配當然最好,具體還要看項目需求哦

13、EF預熱

使用過EF的都知道針對所有表的第一次查詢都很慢,而同一個查詢查詢過一次后就會變得很快了。

假設場景:當我們的查詢編譯發布部署到服務器上時,第一個訪問網站的的人會感覺到頁面加載的十分緩慢,這就帶來了很不好的用戶體驗。

解決方案:在網站初始化時將數據表遍歷一遍

在Global文件的Application_Start方法中添加如下代碼(代碼如下(Entity Framework的版本至少是6.0才支持)):

using (var dbcontext = new BingFaTestEntities())
{
var objectContext = ((IObjectContextAdapter)dbcontext).ObjectContext;
var mappingCollection = (StorageMappingItemCollection)objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);
mappingCollection.GenerateViews(new List<EdmSchemaError>());
}

我們做個測試:

12.1、第一次運行程序,不進行EF預熱的:

12.2、同樣重新運行程序,進行EF預熱的:

執行速度:

由上圖可以,在進行了EF預熱后,加載時間為856.9毫秒,而不進行EF預熱加載用時1511.5毫秒,由此可知,加上預熱代碼后,第一次加載速度幾乎快了一倍。

@陳卧龍的博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM