為什么需要動態SQL


為什么需要動態SQL

在使用EF或者寫SQL語句時,查詢條件往往是這樣一種非常常見的邏輯:如果客戶填了查詢信息,則查詢該條件;如果客戶沒填,則返回所有數據。

我常常看到很多人解決這類問題時使用了錯誤的靜態SQL的解決辦法,使得數據庫無法利用索引,導致性能急劇下降。

介紹數據

這次我將使用我的某客戶的真實數據來演示(已確認不涉及信息安全😎),有一個訂單表FoodOrder,結構如下:

我在IdFoodMenuIdOrderUserId上創建了非聚集索引,在OrderTime上創建了聚集索引。該表有51652條數據。

靜態SQL

在這種邏輯中如果想用一條SQL語句搞定所有查詢,那么代碼可能長這個樣子:

set statistics io on 

declare @userId int = 506
declare @menuId int = 3176

select * from FoodOrder where 
	(@userId is null or OrderUserId = @userId) AND 
	(@menuId is null or FoodMenuId = @menuId)

這種寫法雖然方便,但基於其SQL過於“復雜”,甚至還使用了IS NULLOR,導致語句完全無法使用索引,運行SET STATISTICS IO ON后,顯示信息如下:

(3 行受影響)
Table 'FoodOrder'. Scan count 1, logical reads 337, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.

顯示其進行了一次表掃描,並進行了337次邏輯讀,輸出數據只有3行。

然后看看實際的執行計划:

如圖,顯示了一個極其簡單的執行計划,確實進行了一次表掃描,讀取了51652行數據,並且完全沒有走索引。

動態SQL

而動態SQL,就是將查詢條件中的判斷語句,提前在代碼中判斷完成,而放到數據庫(如SQL Server)中執行時就是簡單的、可利用索引的SQL語句了,在這個例子中,判斷@userId@menuId是否為null的代碼,可能會長這個樣子(如果是Dapper):

var sql = new StringBuilder();
sql.Append("SELECT * FROM FoodOrder WHERE 1=1 ");

if (userId != null) 
{
    sql.AppendLine("AND OrderUserId = @userId");
}

if (menuId != null)
{
    sql.AppendLine("AND FoodMenuId = @menuId");
}
// ...

如果是EF,代碼可能是這個樣子:

IQueryable<FoodOrder> query = db.FoodOrders;

if (userId != null)
{
    query = query.Where(x => x.OrderUserId == userId);
}

if (menuId != null)
{
    query = query.Where(x => x.FoodMenuId = menuId);
}
// ...

這樣一來,最終在數據中執行的SQL語句就比較簡單了,如果客戶確實傳了userIdmenuId兩個參數,SQL就應該長這個樣子:

select * from FoodOrder where 
	OrderUserId = @userId AND 
	FoodMenuId = @menuId

運行的set statistics io on結果如下:

(3 行受影響)
Table 'FoodOrder'. Scan count 2, logical reads 11, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0.

顯然僅進行了11次邏輯讀(相比靜態SQL337次),然后執行計划如下:

顯示進行了兩次Index Seek,顯然是走了索引,顯示查詢開銷只占5%,而之前的開銷占95%,性能區別高達20倍以上。

總結

據說上次博客園出現性能問題,就是因為EF Core 3.0有這個bug,會生成多余的IS NOT NULL(鏈接:EF Core 3.0 Preview 9 的2個小坑),這個bug已經確認最新的EF Core 3.1中解決。

就像文中所說的動態SQL,我認為理解數據庫、對寫出高性能的應用程序至關重要——這顯而易見,但其實又很容易忽略。忽略的原因不僅是因為新手,很多老手有時因為“互聯網”思維和“設計模式”等原因,也會有意忽略數據庫的理解。

現在很多“互聯網”應用思維認為,數據庫就是一個倉庫,它應該只負責其最“擅長”的增刪改查功能即可,其它的應該都交由緩存來解決。有句話說得好,就是命名和緩存失效,是編程界最困難的兩個問題。緩存有緩存的問題,不好好理解數據庫,就必須花大量時間好好理解緩存。設計一個正確的緩存往往又比花大量時間設計數據庫要復雜得多。

另外現在流行的“領域驅動設計”(DDD)也主張應用應該先從業務邏輯開始抽象,數據庫和性能往往成為他們首先忽略的對象,最后可能也得加個“緩存”來解決,導致原來簡單的系統急劇膨脹,復雜不堪。這種過度設計、人雲亦雲的做法值得深思。

喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】

DotNet騷操作


免責聲明!

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



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