為什麼說 LINQ 要勝過 SQL
如果你還沒有沉溺於 LINQ,就會想這有啥大驚小怪的。SQL 並沒有壞掉,為什麼還要對它進行修補呢? 為什麼我們還需要另外一種查詢語言呢?
流行的說法是 LINQ 同 C#(或者 VB)整合在了一起,故而消除了程式語言和資料庫之間配合上的鴻溝,同時為多個資料來源的組合提供了單一的查詢介面。雖然這些都是事實,但僅是故事的一部分。更重要的是:當要對資料庫進行查詢的時候,LINQ 在大多數情況下都比 SQL 更加有效。
同 SQL 相比, LINQ 更簡單、整潔而且高階。這樣子更像是拿 C# 同 C++ 做比較。真的,儘管有時候使用 C++ 仍然是最好的選擇(比如使用 SQL 的場景),但在大多數場景中,使用現代整潔的語言而不必為底層細節操作就是一項大勝利。
SQL 是一門非常古老的語言—發明於 1974 年。雖然經歷過了無數此擴充套件,但從來沒有被重新設計過。這就使得它有點混亂了—不像是 VB6 或者 Visual FoxPro。你也許已經慢慢變得習慣於此因而看不到任何錯漏的地方!
讓我們來看一個例子。你想要編寫一個簡單的查詢來獲取客戶資料,如下:
SELECT UPPER(Name) FROM Customer WHERE Name LIKE 'A%' ORDER BY Name
現在假設要將結果集裡的這些資料提供給一個網頁,並且我們想獲取第 21 到 30 行資料。所以我們需要一個子查詢:
SELECT UPPER(Name) FROM ( SELECT *, RN = row_number() OVER (ORDER BY Name) FROM Customer WHERE Name LIKE 'A%' ) A WHERE RN BETWEEN 21 AND 30 ORDER BY Name
而如果你需要支援版本(在 SQL Server 2005 之前的)更老的資料庫,情況會更糟糕:
SELECT TOP 10 UPPER (c1.Name) FROM Customer c1 WHERE c1.Name LIKE 'A%' AND c1.ID NOT IN ( SELECT TOP 20 c2.ID FROM Customer c2 WHERE c2.Name LIKE 'A%' ORDER BY c2.Name ) ORDER BY c1.Name
這樣做不僅複雜而混亂,而且也違背了 DRY 原則。如下是使用 LINQ 實現相同的查詢功能。顯然在簡單性上更勝一籌:
var query = from c in db.Customers where c.Name.StartsWith ("A") orderby c.Name select c.Name.ToUpper(); var thirdPage = query.Skip(20).Take(10);
只有當我們列舉到 thirdPage 時,查詢才會實際執行。在從 LINQ 到 SQL 或者 Entity Framework 的場景中,翻譯引擎會將(我們用兩個步驟組合而成的)查詢轉換成一個 SQL 語句,這個語句是針對其所連線的資料庫伺服器進行了優化的。
可組合性
您可能已經注意到 LINQ 的另一個更微妙(微妙但意義重大)的好處。我們選擇了組合中的兩個查詢步驟:
IQueryable<T> Paginate<T> (this IQueryable<T> query, int skip, int take) { return query.Skip(skip).Take(take); }
我們可以這樣做:
var query = ... var thirdPage = query.Paginate (20, 10);
更重要的是,在這裡我們可以進行任意的分頁查詢。換言之就是通過 LINQ 你可以把查詢分解成一部分,然後在你的應用程式中重用。
聯合
LINQ 另一好處就是你可以不用 JOIN 就能進行關係間查詢。例如,我們想要列出所有購物在 $1000 或者以上,並且居住在華盛頓的顧客。我們會假定讓購買專案化(也就是經典的採購/專案採購場景)並且把(沒有顧客記錄的)現金銷售也囊括進來。這就需要在四個表(Purchase, Customer, Address 以及 PurchaseItem)之間進行查詢。使用 LINQ,這樣的查詢不費吹灰之力:
from p in db.Purchases where p.Customer.Address.State == "WA" || p.Customer == null where p.PurchaseItems.Sum (pi => pi.SaleAmount) > 1000 select p
將此與同等功能的 SQL 相比較:
SELECT p.* FROM Purchase p LEFT OUTER JOIN Customer c INNER JOIN Address a ON c.AddressID = a.ID ON p.CustomerID = c.ID WHERE (a.State = 'WA' || p.CustomerID IS NULL) AND p.ID in ( SELECT PurchaseID FROM PurchaseItem GROUP BY PurchaseID HAVING SUM (SaleAmount) > 1000 )
對此例進一步擴充套件,假設我們想要將結果集按價格進行逆序排列,並在最終的投影中顯示銷售員的姓名以及所購買專案的數量。我們可以自然不重複地表達出這些附件的查詢條件:
from p in db.Purchases where p.Customer.Address.State == "WA" || p.Customer == null let purchaseValue = p.PurchaseItems.Sum (pi => pi.SaleAmount) where purchaseValue > 1000 orderby purchaseValue descending select new { p.Description, p.Customer.SalesPerson.Name, PurchaseItemCount = p.PurchaseItems.Count() }
下面是使用 SQL 實現相同的查詢:
SELECT p.Description, s.Name, (SELECT COUNT(*) FROM PurchaseItem pi WHERE p.ID = pi.PurchaseID) PurchaseItemCount FROM Purchase p LEFT OUTER JOIN Customer c INNER JOIN Address a ON c.AddressID = a.ID LEFT OUTER JOIN SalesPerson s ON c.SalesPersonID = s.ID ON p.CustomerID = c.ID WHERE (a.State = 'WA' OR p.CustomerID IS NULL) AND p.ID in ( SELECT PurchaseID FROM PurchaseItem GROUP BY PurchaseID HAVING SUM (SaleAmount) > 1000 ) ORDER BY (SELECT SUM (SaleAmount) FROM PurchaseItem pi WHERE p.ID = pi.PurchaseID) DESC
有意思的是可以將上述 SQL 查詢轉換回到 LINQ,所生成的查詢每一塊都會有傻瓜式重複。論壇裡常會貼出這樣的查詢(通常是非工作的版本)——這是用 SQL 進行思考而不是以 LINQ 進行思考的結果。這就像是是將 Fortran 程式轉換成 C# 6 時會抱怨 GOTO 的笨拙語法一樣。
資料修整
在查詢聯合中從多個表選擇資料 – 最終的結果會是一個扁平的以行為單位的元組。如果你使用了多年的 SQL,你可能認為這種事不會發生在你身上——它導致資料重複,從而使得結果集無法在客戶端很好地使用。所以當它發生時往往難以接受。與此相反,LINQ 讓你可以獲取到休整過的分層級的資料。這就避免了重複,讓結果集容易處理,而且在大多數情況下也會消除進行聯合操作的必要。例如,假設我們想要提取一組顧客,每一條記錄都帶上了它們的高價值交易。使用 LINQ,你可以這樣做:
from c in db.Customers where c.Address.State == "WA" select new { c.Name, c.CustomerNumber, HighValuePurchases = c.Purchases.Where (p => p.Price > 1000) }
HighValuePurchases,在這裡是一個集合。由於我們查詢的是一個相關屬性,就不需要進行聯合了。因此這是一個內聯合還是外聯合的細節問題就被很好的抽象掉了。在此例中,當翻譯成了 SQL,可能就是一個外聯合:LINQ 不會因為子集合返回的是零個元素就排除行。如果我們想要有一個可以翻譯成一個內聯合的東西,可以這樣做:
from c in db.Customers where c.Address.State == "WA" let HighValuePurchases = c.Purchases.Where (p => p.Price > 1000)where HighValuePurchases.Any()select new { c.Name, c.CustomerNumber, HighValuePurchases }
LINQ 還通過一組豐富的操作符對平面外聯合、自聯合、組查詢以及其它各種不同型別查詢進行了支援。
引數化
如果我們想要將之前的例子引數化會如何呢,如此”WA”狀態是不是就要來自於一個變數呢? 其實我們只要像下面這樣做就可以了:
string state = "WA"; var query = from c in db.Customers where c.Address.State == state ...
不會混淆 DbCommand 物件上面的引數,或者擔心 SQL 注入攻擊。 LINQ 的引數化是內聯、型別安全並且高度可讀的。它不僅解決了問題——而且解決得很不錯。
因為 LINQ 查詢時可以進行組合,所以我們可以有條件的新增謂詞。例如,我們寫出一個方法,如下:
IQueryable<Customer> GetCustomers (string state, decimal? minPurchase) { var query = Customers.AsQueryable(); if (state != null) query = query.Where (c => c.Address.State == state); if (minPurchase != null) query = query.Where (c => c.Purchases.Any (p => p.Price > minPurchase.Value)); return query; }
如果我們使用空的 state 以及 minPurchase 值呼叫了這個方法,那麼在我們列舉結果集的時候如下 SQL 就會被生成出來:
SELECT [t0].[ID], [t0].[Name], [t0].[AddressID] FROM [Customer] AS [t0]
不過,如果我們指定了 state 和 minPurchase 的值,LINQ 到 SQL 就不只是向查詢新增了謂詞,還會有必要的聯合語句:
SELECT [t0].[ID], [t0].[Name], [t0].[AddressID] FROM [Customer] AS [t0] LEFT OUTER JOIN [Address] AS [t1] ON [t1].[ID] = [t0].[AddressID] WHERE (EXISTS( SELECT NULL AS [EMPTY] FROM [Purchase] AS [t2] WHERE ([t2].[Price] > @p0) AND ([t2].[CustomerID] = [t0].[ID]) )) AND ([t1].[State] = @p1)
因為我們的方法返回了一個 IQueryable,查詢在列舉到之前並不會被實際地轉換成 SQL 並加以執行。這樣就給了呼叫進一步新增謂詞、分頁、自定義投影等等的機會。
靜態型別安全
在之前的查詢中,如果我們將 state 變數宣告成了一個整型數而不是一個字串,那麼查詢可能在編譯時就會報錯,而不用等到執行時。這個也同樣適用於把表名或者列名弄錯的情況。這在重構時有一個很實在的好處:如果你沒有完成手頭的工作,編譯器會給出提示。
客戶端處理
LINQ 讓你可以輕鬆地將查詢的一些部分轉移到客戶端上進行處理。對於負載負擔較大的資料庫伺服器,這樣做可實際提升效能。只要你所取資料沒有超過所需(換言之,你還是要在伺服器上做過濾),就可以經常性地通過把對結果集進行重新排序、轉換以及重組的壓力轉移到負載較少的應用伺服器上去。使用 LINQ,你需要做的就是 AsEnumerable() 轉移到查詢之中,而自那個點之後的所有事情都可以在本地執行。
什麼時候不用 LINQ 去查詢資料庫
儘管 LINQ 的功能強大,但是它並不能取代 SQL。它可以滿足 95% 以上的需求,不過你有時仍然需要SQL:
- 需要手動調整的查詢 (特殊是需要優化和進行鎖定提示的時候);
- 有些涉及到要 select 臨時表,然後又要對那些表進行查詢操作的查詢;
- 預知的更新以及批量插入操作。
還有就在用到觸發器時,你還是需要 SQL。 (儘管在使用 LINQ 的時候諸如此類的東西並非常常被需要,但在要使用儲存過程和函式的時候,SQL 是不可或缺的)。你可以通過在 SQL 中編寫表值函式來將 SQL 與 LINQ 結合在一起, 然後在更加複雜的 LINQ 查詢裡面呼叫這些函式。
瞭解兩門查詢語言並不是問題,因為無論如何你都會想要去學習 LINQ 的 — LINQ 在查詢本地集合以及 XML DOM 的時候非常實用。如果你使用的仍然是老舊的基於 XmlDocument 的 DOM,LINQ to XML 的 DOM 操作會是一種具有戲劇效果的進步。
還有就是相比於 SQL, LINQ 更易於掌握,所以如果你想寫個不錯的查詢,使用 LINQ 會比 SQL 更好達成。
將 LINQ 用於實戰
我幾乎是只用 LINQ 來做資料庫查詢,因為它更有效率。
對於應用程式的編寫而言,我的個人經驗是一個使用 LINQ 的資料訪問層(使用一個像 LINQ 到 SQL 或者 Entity Framework 的 API)可以將資料訪問的開發時間砍掉一半,而且可以讓維護工作更加的輕鬆。
相關文章
- 為什麼說Spark SQL遠遠超越了MPP SQLSparkSQL
- 為什麼要虛擬化,為什麼要容器,為什麼要Docker,為什麼要K8S?DockerK8S
- 為什麼說Java中要慎重使用繼承Java繼承
- 為什麼我們要學習DMAIC?—舉例說明AI
- 為什麼要code reviewView
- 為什麼要寫作
- 說說你做過讓你覺得最滿意的專案是什麼?為什麼?
- 為什麼要透過API介面來獲取資料API
- Python是什麼?為什麼要掌握python?Python
- 趣說開源|為什麼要參與到開源社群中?
- 為什麼說過早最佳化是萬惡之源?
- 為什麼說JPRG「反過來」影響了西方奇幻?
- 為什麼要學習 Julia
- 為什麼要指令重排序?排序
- 為什麼要財務自由
- 為什麼要學習 RustRust
- 為什麼要學習 Vim?
- 為什麼要學習Netty?Netty
- 為什麼說一個好的Java程式設計師,是無碼勝有碼?Java程式設計師
- 為什麼 JavaScript 的 this 要這麼用?JavaScript
- 效能優化小冊 - 非同步堆疊追蹤:為什麼 await 勝過 Promise優化非同步AIPromise
- 為什麼說DOM操作很慢
- Python優勢是什麼?為什麼要學習?Python
- Python到底是什麼?為什麼要學Python?Python
- 什麼是框架?為什麼說 Angular 是框架?框架Angular
- 說說我為什麼看好Spring Cloud AlibabaSpringCloud
- 為什麼要閱讀原始碼原始碼
- 序 為什麼要建立部落格
- 為什麼要分庫分表?
- 為什麼還要記密碼密碼
- [譯]為什麼要寫 super(props)
- 為什麼要貢獻開源
- 為什麼要加EventQueue.invokeLater
- 為什麼要特徵標準化特徵
- 我為什麼要學技術
- 為什麼要“東數西算”?
- redis為什麼要提供pipeline功能Redis
- 前端為什麼要工程化?前端
- 為什麼要學習Python?Python可以做什麼事情?Python