linq效能剖析

來源:互聯網
上載者:User

標籤:style   ar   color   使用   sp   strong   on   檔案   資料   

Orcas(VS2008&Framework3.5)給我們帶來了很多令人興奮的新特性,尤其是LINQ的引進,可以說方便了一大批開發 人員和架構設計人員。過去,當我們使用O/RMapping的一些架構時,最擔心的應該是複雜的查詢和資料庫操作的效能問題,那麼LINQ在這個方面給我 們帶來了什麼呢?從LINQ查詢開始說起吧。

一:LINQ查詢:

一個項目中,對資料庫操作最多的應該就是查詢,特別是複雜一些的項目,查詢往往是從很多張表裡取一些資料,“東拼西湊”的呈現給使用者。如果不使用O/R Mapping呢,我們處理的方式無非是兩種:複雜的SQL語句或者是預存程序。

但是,在使用了O/R Mapping的情況下,為了保證取出的資料是強型別,不得不用多個方法來進行拼湊,麻煩且不說,效能損失是肯定的,那麼也就出現了O/R Mapping架構和未封裝的ADO.NET一起用的尷尬場面。那麼LINQ查詢怎麼處理的呢?首先我們先來看一些複雜的查詢文法。

設計兩張表,分別為表示部門(Department)和員工(Employee):

1、 模糊查詢:

模糊查詢在O/RMapping的架構中是比較難實現的,即使實現,效率也不是很高,LINQ提供了我們豐富的模糊查詢的方法,比如我們要在Employee表中查姓張的人,可以這樣:

DBDataClassesDataContext dbdata = new DBDataClassesDataContext(); 

 var query = from employee in dbdata.Employees whereemployee.EmployeeName.StartsWith("張")

                     select employee;  

在實際執行的時候,這句話被轉換成了:

  1. SELECT [t0].[EmployeeId], [t0].[DepId], [t0].[EmployeeName], 
  2. [t0].[EmployeeSalary] FROM [dbo].[Employee] AS
  3.  [t0] WHERE [t0].[EmployeeName] LIKE @p0 

這樣的SQL語句,這個裡面的@p0這個參數在執行的時候加上了“%”,也就是以@p0開頭的任何合格記錄都被取出來了。這樣的方法在LINQ查詢中還有Contains、EndsWith等。

2、巢狀查詢:

如果我們需要取出市場部的所有員工,在以往的O/R Mapping架構中,往往需要取兩次(我們不考慮存在映射關係的情況),一次是取出市場部的編號,再使用編號來取出所有員工。LINQ查詢給我們提供了更好的解決辦法,我們可以這樣操作:

  1. var query = from employee in dbdata.Employees where employee.DepId ==  
  2. (from department in dbdata.Departments where department.DepName == "市場部" 
  3.  select department ).Single().DepId  
  4. select employee;  

這裡我們使用了巢狀查詢來完成,這句話在執行的時候,被轉換成了SQL的巢狀查詢:

  1. SELECT [t0].[EmployeeId], [t0].[DepId], [t0].[EmployeeName],
  2.  [t0].[EmployeeSalary] FROM [dbo].[Employee] AS [t0] WHERE [t0].[DepId] = 
  3. (SELECT [t1].[DepId] FROM [dbo].[Department] AS
  4.  [t1] WHERE [t1].[DepName] = @p0) 

當然,在這裡,我們不必那麼麻煩,只需要使用已經存在的實體關聯,就可以輕鬆的完成:

  1. var query = from employee in dbdata.Employees where
  2.  employee.Department.DepName == "市場部" 
  3. select employee;  

不過,這和剛才的查詢卻有本質的差別,我們來看看這裡,LINQ查詢轉換成什麼樣的SQL語句了:

  1. SELECT [t0].[EmployeeId], [t0].[DepId], [t0].[EmployeeName], 
  2. [t0].[EmployeeSalary] FROM [dbo].[Employee] AS 
  3. [t0] LEFT OUTER JOIN [dbo].[Department] AS [t1] ON [t1].[DepId] = 
  4. [t0].[DepId] WHERE [t1].[DepName] = @p0 

這裡,轉換出來的並不是嵌套的SQL語句,而被轉換成了左串連查詢,可以看出,巢狀查詢是我們在特殊的情況下,可以使用的一個利器。

3、投影

如果說剛才的查詢,還只是很簡單的查詢,那麼結合匿名類來實現投影查詢,則是LINQ查詢提供的一個利器,這種方法非常靈活,同時也滿足了我們絕大部分的查詢需求。下面我們來看一個例子:我們需要查詢出部門工資的總和,怎麼做呢?

  1. var query = from department in dbdata.Departments  
  2. select  new{ depid = department.DepId, depname=department.DepName, 
  3. depsalary = 
  4. department.Employees.Sum(e => e.EmployeeSalary) };  

這句查詢語句中,在new關鍵字後面產生了一個匿名類,這個類有三個屬性,分別是 depid,depname和depsalary,其中,depsalary是經過計算獲得,這句話被轉換成:

  1. SELECT [t0].[DepId] AS [depid], [t0].[DepName] AS [depname], 
  2. (SELECT SUM([t1].[EmployeeSalary]) FROM [dbo].[Employee] AS [t1] 
  3. WHERE [t1].[DepId] = 
  4. [t0].[DepId] ) AS [depsalay] FROM [dbo].[Department] AS [t0] 

好優雅的代碼,實在是太方便了。

4、使用LINQ查詢的擴充方法

LINQ查詢提供了很多擴充方法,方便我們做各種查詢,我們來看幾個典型的擴充方法:

a) Average、Max

  1. Decimal x = dbdata.Employees.Average(e => e.EmployeeSalary);  
  2. Decimal y = dbdata.Employees.Max(e => e.EmployeeSalary);  

這是計算出員工的平均工資與最大工資,你不需要編寫任何的SQL語句,Orcas中提供的LINQ到SQL對象關係映射器會處理擷取,跟蹤,和更新映射到你的資料庫資料定義和預存程序的對象。

你只要使用任何LINQ查詢擴充方法對結果進行過濾和構形即可,LINQ到SQL會執行擷取資料所需的SQL代碼(注意,上面的 Average和Max 擴充方法很明顯地不會從資料表中返回所有的資料行,它們會使用TSQL的彙總函式來計算資料庫中的值,然後只返回一個標量值)。

b) Where、OrderBy

有時候,我們只對某張表做簡單的查詢和排序,那麼,這個時候不必寫冗長的LINQ查詢語句,直接使用LINQ擴充方法即可,如:

  1. var query = dbdata.Employees.Where(e => e.EmployeeSalary > 2000).
  2. OrderBy(e => e.EmployeeName); 

這裡使用了Lambda文法,這句話被轉換成以下的SQL語句:

  1. SELECT [t0].[EmployeeId], [t0].[DepId], [t0].[EmployeeName], 
  2. [t0].[EmployeeSalary] FROM [dbo].[Employee] AS [t0] WHERE
  3.  [t0].[EmployeeSalary] > @p0 ORDER BY [t0].[EmployeeName] 

以上是通過利用由Lambda提供的對錶達式樹支援,以及IQueryable介面來實現的,代碼乾淨整潔。

C)Take、Skip

對於大批量的資料處理,一直是開發人員的比較頭疼的事情,微軟在.NET1.1到2.0中的Gridview等控制項,對大批量資料的處理上一直都不是很理想,LINQ查詢對於大批量資料的處理,可以很好的解決這個方面的問題。

  1. var query = dbdata.Employees.Skip(10).Take(10); 

這句話表示跳過該表的10條記錄,再取10條,也就是取第11至20條記錄,轉換成SQL語句如下:

  1. SELECT [t1].[EmployeeId], [t1].[DepId], [t1].[EmployeeName], 
  2. [t1].[EmployeeSalary] FROM 
  3. (SELECT ROW_NUMBER() OVER (ORDER BY [t0].[EmployeeId],
  4.  [t0].[DepId], [t0].[EmployeeName], [t0].[EmployeeSalary]) AS
  5.  [ROW_NUMBER], [t0].[EmployeeId], 
  6. [t0].[DepId], [t0].[EmployeeName], 
  7. [t0].[EmployeeSalary] FROM [dbo].[Employee] AS [t0]) AS 
  8. [t1] WHERE [t1].[ROW_NUMBER] BETWEEN
  9.  @p0 + 1 AND @p0 + @p1 ORDER BY [t1].[ROW_NUMBER] 

從以上這句SQL語句來看,最消耗效能分頁操作完全交給了資料庫操作,其處理的機制不再像Gridview控制項的分頁,是將資料全部取出,然後再進行分頁顯示,因此效率上要高了很多。

D)ToList和ToArray

在預設情況下,查詢結果的資料類型是IEnumerable類型,可能很多開發人員並不習慣這個類型,而更加喜歡集合或者是數組,那麼沒關係,可以使用ToList或者是ToArray來將查詢結果轉換成集合或者數組

在這裡,我們需要知道的是:使用查詢語句查詢結果的時候,實際上並沒有真正操作資料庫,這裡是運用的消極式載入的機制,如果不希望使用消極式載入,而是需要立刻知道查詢的結果時,使用ToList或者是ToArray便可以做到。這是非常有用的機制。

比如我們需要顯示兩個部門的員工時,部門可以先取出放置在List中,然後再依次取出各個部門的員工,這時訪問的效率要高一些,因為不需要每次都訪問資料庫去取出部門。

二:資料操作的效能分析

當我們會熟練的使用以上的查詢方法對資料庫裡的內容做各種各樣的查詢以後,就應當要瞭解這些資料庫操作的機制,及時調整各種資料動作陳述式,以較高的效率運行。那麼,下面我們來看看LINQ的資料庫操作,看看她都做了些什麼工作。

與NHibernate來比較,LINQ在O/R Mapping的效能與可控性上確實優於NHibernate,首先,Linq預設的資料對應採用的是Attribute來實現,這是.NET特有的語 法,在編譯時間就已經決定了資料對象的各種屬性,而NHibernate等大多數O/RMapping工具仍然採用XML對應檔來描述資料對象的屬性,從 外部檔案上讀取資料對象的屬性,顯然運行時效率要有所損失。

其次,在獲得資料的方式上也有所差別,LINQ中強大的SQL分析機制,可以分析出各種資料操作的SQL語句,並且進行最佳化,其效率的提升也是顯而易見的。

當然,作為一個O/R Mapping的工具來說,其效率一定達不到直接使用SQL語句訪問資料庫的效率,也就是我們通常所說的SqlDataReader/SqlDataAdapter訪問資料庫,但是,Linq的表現卻給了我們非常大的驚喜。

我做了一個測試,使用SqlDataReader和LINQ做相同的大批量資料查詢時,落後竟然不到10%,而NHibernate的查詢效率,卻 低了很多,幾乎慢了1倍。對於如此強大的資料對應功能,這樣的效率是我們可以接受的。但是很可惜的一點是,LINQ查詢目前只能支援對SQLServer 的支援(但可以支援XML、Entity等)。

在資料查詢上,我們通過對LINQ查詢產生的SQL語句進行分析,便可以最佳化查詢,這是非常方便的,但是,針對資料更新的效率問題,我們不得不談談LINQ的資料更新機制,一般情況下,資料更新我們會這麼做:

  1. var query = from emp in dbdata.Employees where emp.DepId=="1001" select emp;  
  2.   Employee employee = query.First();  
  3.   employee.EmployeeName = "李四";  
  4.   dbdata.SubmitChanges();  

對於以上這段代碼,我們可以看出,其功能是從Employee表中取出部門代碼為1001的所有員工,然後我們取出第一條資料(這裡為了簡便,我們僅僅取出第一條,其實可以用Where取出滿足條件的記錄),然後把名字修改成“李四”,再更新到資料庫中。

這段代碼,LINQ查詢都幹了些什麼呢?通過查詢從資料庫中取出若干條記錄,放在記憶體中,並且都標記為new(未改變)狀態,當修改了員工姓名的時候,被修改的對象被標記為Dirty(已改變)。

在SubmitChanges的時候,再為記憶體中對象狀態為Dirty的記錄自動產生SQL語句並執行,也就是說,我們要完成一次資料的更新,至少要完成一次查詢和一次更新。

由於採用了延時載入(Layze Load)的技術,在以上語句中實際從資料庫中取出的記錄只有1條,更新的時候也只更新這一條,因此效率仍然是非常高的,我在測試的過程中發現,從 250000條資料中隨機抽取一條進行更新,實際的效率和從10條資料中隨機抽取一條進行更新幾乎沒有差別,因為比較更新狀態是在記憶體中進行,因此效率是 比較高的。下面我們再看看實際的更新產生了什麼樣的SQL語句:

  1. UPDATE [dbo].[Employee] SET [EmployeeName] = @p4 WHERE
  2.  ([EmployeeId] = @p0) AND ([DepId] = @p1) AND  
  3. ([EmployeeName] = @p2) AND ([EmployeeSalary] = @p3)  

原來,我們只修改了EmployeeName的欄位,產生的SQL語句卻也僅僅是更新了Employee欄位。那麼,我們再看看後面的條件,為什麼 會包含除了主鍵以外的其他條件呢?原來,這也是LINQ查詢自動產生SQL語句的嚴謹所在,這是為了防止並發情況下,多個事務針對同一條記錄更新時發生錯 誤,假如A事務更新了該記錄,則B事務更新會失敗。

我們不禁要問,假如要更新主鍵欄位怎麼辦?會不會錯誤的更新到多條記錄呢?答案是肯定的,肯定會錯誤的更新到其他記錄,因此,LINQ中規定了主鍵 欄位是不允許更新的,如果確實要更新,那麼就刪除掉該記錄,重新插入新紀錄。這麼嚴謹的SQL語句,會給我們帶來一些麻煩,我們來看下面一個應用情境:

如果我們在表中設有一個欄位用於計數器,使用SQL語句是這樣的:

  1. Update CountTable set CountColumn=CountColumn+1 where [email protected] 

但使用LINQ查詢產生的Sql語句卻是:

  1. UPDATE [dbo].[CountTable] SET [CountColumn] = @p2 WHERE 
  2. ([CountId] = @p0) AND ([CountColumn] = @p1) 

@p2這個參數是計算好後傳入的,@p1這個參數是CountColumn原來的值。也就是說,CountColumn+1這個值不是由資料庫運算 出來的,這樣一來,當並發數很高的時候,我們往往會更新失敗。我做了個測試,使用多線程類比多使用者的情況下進行計數統計,資料庫中統計的值比使用靜態變數 儲存的值要小,這也就是說資料庫更新是存在失敗的情況。

另外,這樣每次的更新,需要完成的操作有尋找和更新兩個步驟,因此對於效率也有比較大的影響。

linq效能剖析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.