SQL LINQ SQL Performance

常用SQL語法與LINQ比對,以及效能比較

陳仲和 2019/01/02 20:20:50
11921

常用SQL語法與LINQ比對,以及效能比較


簡介

介紹常用的SQL語法在採用LINQ時的表現方式,以及如何避免不恰當的寫法照成DB效能不佳

作者

陳仲和


一、原由
SQL發展由美國ANSI採用作為關聯式資料庫的標準語言為1986年,距今已有33年之久,只要是開發程式會運用到資料庫的,就免不了對SQL語法要有一定的了解,而微軟提出LINQ來解決ORM與查詢語言的聯繫溝通也有10來年了,之前常常有聽人家說LINQ不好用,對DB效能不好,建議不要採用等等的論調出現,且還有在甲方看到DBA因為開發人員採用LINQ查詢DB,造成系統執行變慢而被飆罵的事情發生,故一直對此感到好奇,剛好此次參與的專案也是採用LINQ的方式製作報表,故除了有機會學習使用LINQ語言之外也興起比較的念頭,並將初步比對結果分享給大家。

二 、測試環境與資料
因主要是想比較兩者語法在執行上的差異,故在DB系統/Data/硬體設備上並無特別講究,只求測試時標準一致性即可。規格如下
DB           : MSSQL EXPRESS 2016
DATA       :  AdventureWork2014
Computer : Acer Aspire i5 8G RAM
OS           : Windows 10
 
測試時會用到的Table資料筆數如下
Person                          :19972
BusinessEntity               :20777
BusinessEntityAddress    :19614
Address                         :19614
Employee                           :290
EmployeeDepartmentHistory:296
Department                        :16
Shift                                   :3
 
、SQL與LINQ常用語法介紹與比較
1. Left JOIN
   第一個常會用的SQL語法為JOIN, 其中LEFT JOIN更是常被使用,在LINQ中 LEFT JOIN的條件寫法比較特別,要特別注意,範例如下
  1.1. SQL範例
string queryString = "SELECT a.[FirstName], a.[MiddleName],a.[LastName], d.City,d.AddressLine1,d.AddressLine2 "
                               + "FROM[Person].[Person] a "
                               + "join Person.BusinessEntity b on a.BusinessEntityID= b.BusinessEntityID "
                               + "join Person.BusinessEntityAddress c on a.BusinessEntityID = c.BusinessEntityID "
                               + "left join person.Address d on c.AddressID = d.AddressID";
  1.2. LINQ範例
var person = (from a in db.Person
                         join b in db.BusinessEntity on a.BusinessEntityID equals b.BusinessEntityID 
                         join c in db.BusinessEntityAddress on a.BusinessEntityID equals c.BusinessEntityID
                         join d in db.Address on new { AddressID = c.AddressID} equals new { AddressID = d.AddressID} into ps
                         from d in ps.DefaultIfEmpty()
                         select new {
                             a.FirstName,
                             a.MiddleName,
                             a.LastName,
                             d.City,
                             d.AddressLine1,
                             d.AddressLine2
                         })
                         ;

在此範例中因為欄位AddressLine1只是被拿來參照用,可以為Null,故採用Left Join的方式取得資料,在LINQ中可以看到是採用匿名類別方式產生屬性為AddressID的物件來做比較並將結果存放入ps變數中,之後再將可table d default可為empty的方式將AddressLine1與AddressLine2的方式取出,即可完成LINQ的寫法。

2.Case When..Else..End

在SQL裏,將欄位查詢時順便做比較而給予新的值的方式也是常用的寫法,範例如下

2.1. SQL範例

queryString = "select a.FirstName, a.LastName, e.DepartmentID, e.GroupName,"
                        + "case when d.StartTime >='07:00:00' and d.EndTime<= '15:00:00' then 'Day' Else 'Night' end as Shift, "
                        + "d.StartTime,d.EndTime "
                        + "from Person.Person a "
                        + "join HumanResources.Employee b on a.BusinessEntityID = b.BusinessEntityID "
                        + "join HumanResources.EmployeeDepartmentHistory c on b.BusinessEntityID = c.BusinessEntityID "
                        + "join HumanResources.Shift d on c.ShiftID = d.ShiftID "
                        + "join HumanResources.Department e on c.DepartmentID = e.DepartmentID ";

2.2. LINQ範例

var shift = (from e in db.Person
                         join f in db.Employee on e.BusinessEntityID equals f.BusinessEntityID
                         join g in db.EmployeeDepartmentHistory on f.BusinessEntityID equals g.BusinessEntityID
                         join h in db.Shift on g.ShiftID equals h.ShiftID
                         join i in db.Department on g.DepartmentID equals i.DepartmentID
                         select new
                         {
                             e.FirstName,
                             e.LastName,
                             i.DepartmentID,
                             i.GroupName,
                             Shift = h.StartTime >= new TimeSpan(7, 0, 0) && h.EndTime <= new TimeSpan(15, 0, 0) 
                             ? "Day" : "Night",
                             h.StartTime,
                             h.EndTime
                         }
                         );

Case When在LINQ上的寫法反而很簡單,就是大家常用的IF..ELSE寫法,只是要注意的是因為是物件導向寫法,比較時還要多處理物件型態的一致才有辦法做比較,在這個案例中是將上下班時間區分出早班與晚班的方式。

 3.SQL Function

在SQL執行時也會常常用到內建的Function做資料轉換與計算,LINQ也有提供相對應的寫法

3.1.SQL範例

queryString = "select a.FirstName, a.LastName,b.BirthDate, b.HireDate, "
                        + "DATEDIFF(year, b.BirthDate, getdate()) as yearsOld "
                        + "from Person.Person a "
                        + "join HumanResources.Employee b on a.BusinessEntityID = b.BusinessEntityID "
                        + "where b.HireDate >= '2009-01-01' ";

3.2. LINQ範例

var compareDate = DateTime.Parse("2009-01-01");
            var years = (from j in db.Person
                         join k in db.Employee on j.BusinessEntityID equals k.BusinessEntityID
                         where k.HireDate >= compareDate
                         select new
                         {
                             j.FirstName,
                             j.LastName,
                             k.BirthDate,
                             k.HireDate,
                             yearsOld = (int?)SqlFunctions.DateDiff("year", k.BirthDate, SqlFunctions.GetDate())
                         }
                         );

這裡用到的是計算兩個DateTime型別的差值的SQL Function DATEDIFF(),此範例是以現在日期的年減掉出生日的年來取得職員的歲數。首先,在LINQ裡首先要先using System.Data.Entity.SqlServer;然後就可以採用MSSQL裡內建的Function。此外本次範例還有加上Where條件,一樣是要注意比較欄位時的條件型態需要一致。

 

四、測試結果與結論

1. 利用SqlDataReader查詢結果

2.利用LINQ透過ORM查詢結果

在SQL執行放置Start Time與End Time是夾在command.ExecuteReader();執行的前後,而LINQ執行時是夾在.ToList()執行的前後,由上表可以看出至少目前比較常用的SQL語法,採用LINQ寫法上在執行時並沒有比較慢進而造成系統的延宕,故在此個人還是做個結論:

採用SQL或是LINQ作為關聯式資料庫的基本查詢理論上應差異不大,花時間辯證哪個比較好用不如發時間注意自己的查詢條件是否得當,尤其是開發人員應當對要查詢的來源資料要有一定暸解,並且對查詢的結果要自己大概預估一下會是如何的結果,在此分享幾個查詢關聯式資料庫時該注意的幾個觀念

1.查詢時是否有用到Index查詢?

2.是否有採用Where條件限制資料的查詢,尤其是查詢history或是Log table更要小心,避免Full table scan查詢。

3.Join Table時除了key值相等條件之外,是否還可以加上限制條件讓被Join的資料取樣可以愈少愈好?

4.查詢的資料如果常常新刪修的話,是否有定期做Index reorganize或是Index rebuild?

就上述幾點跟各位先進分享。

陳仲和
Matt
2019/05/29 01:22:11

手癢來分享一下幾個用法。

 

queryString 多行,可以使用 @"" Literals string 來處理,可以直接斷行並保持指令碼格式,這樣會比較好看。(註1)

單行則可以用使 $"" interpolated  string。(註2)

 

要留意一點,如果目標 Database 是 Oracle 時,使用 Literals string 來傳遞 sql 字串時,斷行及大量空白字串可能會在 db 端產生不正確的 sql script,造成程式錯誤。

 

另外,在無法或不想使用 EF 的場合,可以使用有官方支援的 3rd Extension, Dapper,底層同樣是 ADO.Net,高效好用。(註3)

 

 

 

組 sql 指令碼時,請一定要留意 sql injection 的問題。

 

 

 

#1. https://csharpindepth.com/Articles/Strings

 

#2. https://dapper-tutorial.net/dapper

 

#3. https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated