為什么需要動態SQL
在使用EF
或者寫SQL
語句時,查詢條件往往是這樣一種非常常見的邏輯:如果客戶填了查詢信息,則查詢該條件;如果客戶沒填,則返回所有數據。
我常常看到很多人解決這類問題時使用了錯誤的靜態SQL
的解決辦法,使得數據庫無法利用索引,導致性能急劇下降。
介紹數據
這次我將使用我的某客戶的真實數據來演示(已確認不涉及信息安全😎),有一個訂單表FoodOrder
,結構如下:
我在Id
、FoodMenuId
、OrderUserId
上創建了非聚集索引,在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 NULL
和OR
,導致語句完全無法使用索引,運行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
語句就比較簡單了,如果客戶確實傳了userId
和menuId
兩個參數,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
次邏輯讀(相比靜態SQL
的337
次),然后執行計划如下:
顯示進行了兩次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騷操作】