EFCore 中字符串过滤使用左模糊、右模糊、全模糊及性能分析
背景
我们在做数据库过滤的时候不可避免要进行字符串过滤,那么如果是一张大表的话,那么这个字符串是否会使用上索引?索引过滤的效果怎么样?以及标题中提到的左模糊、右模糊和全模糊这些都是什么意思,在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函数,并且适用上面的方式来进行处理了,这个在使用的时候需要注意区别。