背景
我們在做數據庫過濾的時候不可避免要進行字符串過濾,那么如果是一張大表的話,那么這個字符串是否會使用上索引?索引過濾的效果怎么樣?以及標題中提到的左模糊、右模糊和全模糊這些都是什么意思,在EFCore中到底該怎么用,帶着這些問題我們進入今天文章的主題。
過程分析
1 右模糊
所謂的右模糊就是查詢的字符串左邊部分是精確值,右邊部分可以省略,具體在EFCore中使用StartsWith函數,我們來看看EFCore中一段簡單的代碼。
public Page<GetRepairContractTempOutput> GetRepairContractsByCode(string code, PageRequest pageRequest) { var query = _repairContractRepository.GetAll() .Where(r => r.Code.StartsWith(code)); var totalCount = query.Count(); var pagedResults = query.ProjectTo<GetRepairContractTempOutput>(_mapper.ConfigurationProvider).PageAndOrderBy(pageRequest).ToList(); return new Page<GetRepairContractTempOutput>(pageRequest, totalCount, pagedResults); }
在上面的代碼中我們查詢數據庫實體RepairContract中查詢Code字段以傳入的code參數開始的記錄並分頁返回前20行,我們這里輸入code='RC100012018',我們來看一下生成的SQL長成啥樣子
SELECT TOP(20) [r].[Code], [r].[CreateTime], [r].[Id] FROM [RepairContract] AS [r] WHERE ([r].[Code] LIKE 'RC100012018' + N'%' AND (LEFT([r].[Code], LEN('RC100012018')) = 'RC100012018')) OR ('RC100012018' = N'') ORDER BY [r].[CreateTime] DESC
我們再來看看這段SQL的執行計划,我們在表RepairContract上面的Code建立了一個唯一性索引,通過看下面的執行計划走的是Index Seek 那么是正確使用了索引的
圖一 右模糊執行計划
2 左模糊
所謂的左模糊就是查詢的字符串右邊部分是精確值,左邊部分可以省略,這個和剛才的右模糊剛好相反,具體在EFCore中使用EndsWith函數,我們來看看EFCore中一段簡單的代碼
public Page<GetRepairContractTempOutput> GetRepairContractsByCode(string code, PageRequest pageRequest) { var query = _repairContractRepository.GetAll() .Where(r => r.Code.EndsWith(code)); var totalCount = query.Count(); var pagedResults = query.ProjectTo<GetRepairContractTempOutput>(_mapper.ConfigurationProvider).PageAndOrderBy(pageRequest).ToList(); return new Page<GetRepairContractTempOutput>(pageRequest, totalCount, pagedResults); }
同樣我們輸入一個code='2018070136 ' 這時我們看一下生成的SQL,我們發現使用的是RIGHT函數來進行計算的
SELECT TOP(20) [r].[Code], [r].[CreateTime], [r].[Id] FROM [RepairContract] AS [r] WHERE (RIGHT([r].[Code], LEN('2018070136 ')) = '2018070136 ') OR ('2018070136 ' = N'') ORDER BY [r].[CreateTime] DESC
同樣的我們也來看這段SQL的執行計划,這次我們發現並沒有使用上我們創建的Code索引,這次走的是Index Scan 索引全掃描,所以左模糊使用EndsWith函數是無效的,走的是全表掃描。
圖二 左模糊執行計划
3 全模糊
所謂全模糊就是我們在輸入code的時候沒有任何限制,可以只輸入中間的一部分,我們在代碼中使用的是Contains函數
我們首先來看EFCore的代碼
public Page<GetRepairContractTempOutput> GetRepairContractsByCode(string code, PageRequest pageRequest) { var query = _repairContractRepository.GetAll() .Where(r => r.Code.Contains(code)); var totalCount = query.Count(); var pagedResults = query.ProjectTo<GetRepairContractTempOutput>(_mapper.ConfigurationProvider).PageAndOrderBy(pageRequest).ToList(); return new Page<GetRepairContractTempOutput>(pageRequest, totalCount, pagedResults); }
這次我們來看看生成的SQL是什么樣的,這次SQL SERVER是使用CHARINDEX函數來進行過濾的,這幾種在使用的時候需要注意區別
SELECT TOP(20) [r].[Code], [r].[CreateTime], [r].[Id] FROM [RepairContract] AS [r] WHERE (CHARINDEX('2018070136 ', [r].[Code]) > 0) OR ('2018070136 ' = N'') ORDER BY [r].[CreateTime] DESC
同時我們也來看看這種情況下生成的執行計划是什么樣的?我們發現使用Contains函數也沒有用上索引
圖三 使用全模糊執行計划
總結
在上面的幾種情況下,我們發現只有右模糊能夠正確使用索引,其它情況下都是走的全表掃描,如果在真實線上環境下,真的需要左模糊那該怎么辦呢?難道就沒有辦法了嗎?在實際的情況下我們是這樣進行處理的,我們在數據庫中添加了一個CodeReverse的計算型字段,每次新增一條記錄的時候CodeReverse字段存儲的是Code的反轉字段,這樣當我們需要使用左模糊的時候我們使用CodeReverse來進行匹配,這樣我們就能夠完美解決左模糊和右模糊的問題了,具體步驟如下:
1 DbContext中OnModelCreating中添加下面的內容
modelBuilder.Entity<RepairContract>(eb => { eb.Property(e => e.CodeReverse).HasComputedColumnSql($"Reverse({nameof(RepairContract.Code)}) PERSISTED"); });
2 查詢時邏輯
/// <summary> /// 對委托書編號進行過濾 /// </summary> /// <param name="query"></param> /// <param name="code"></param> /// <returns></returns> public static IQueryable<RepairContract> FilterCode( this IQueryable<RepairContract> query, string code) { if (string.IsNullOrWhiteSpace(code)) return query; var codeReverse = new string(code.Reverse().ToArray()); // 字母開頭的, 右模糊, 非字母開頭的左模糊 return Regex.IsMatch(code, @"^\d") ? query.Where(r => r.CodeReverse.StartsWith(codeReverse)) : query.Where(r => r.Code.StartsWith(code)); }
這里使用代碼的時候需要注意,我們RepairContract中完整地Code='RC100012018070136',所以我們通過判斷輸入的編號是否是字母開頭從而決定是使用左模糊還是右模糊,這個在使用的時候需要根據自己的需要來進行靈活變通,對於一般數據量的表如果查詢的時候使用Contains函數也是可以的,對於大表的話就需要使用StartsWith函數,並且適用上面的方式來進行處理了,這個在使用的時候需要注意區別。