EF Core多表同条件Count计数LINQ查询优化方案
问题描述
当前需要对13张数据表(4张已实现+9张待新增)按指定ImportId做匹配,异步统计符合条件的记录总数,初始串行实现代码如下:
var countA=await _importContext.table1.CountAsync(ssc => ssc.ImportId == importId) var countB=await _importContext.table2.CountAsync(ssc => ssc.ImportId == importId) var countC=await _importContext.table3.CountAsync(ssc => ssc.ImportId == importId) var countD=await _importContext.table4.CountAsync(ssc => ssc.ImportId == importId)
曾尝试将所有计数逻辑封装到匿名对象中统一执行,代码如下,但测试发现耗时比逐个await的串行写法更高:
var result = new { countA = context.table1.Count(), countB = context.table2.Count(), // 其余表计数逻辑 };
核心诉求:精简重复代码、消除冗余,同时优化查询性能。
问题根因
两种写法性能差的核心原因非常明确:
- 逐个await的写法是串行执行:13个查询依次发送到数据库,总耗时是所有查询耗时的累加,数据库连接无法复用,代码冗余度高。
- 匿名对象写法性能更差的原因:
- 使用同步
Count()方法而非异步API,会阻塞调用线程,额外增加线程调度开销 - EF Core不会自动将多个独立的Count操作合并为批量查询,本质还是串行执行,叠加同步阻塞的开销,耗时自然更高
- 若直接在匿名对象中写
CountAsync()不做await,只会得到Task类型对象,无法直接拿到计数结果,执行逻辑依然是串行。
- 使用同步
优化方案
第一步:代码精简,消除重复逻辑
所有需要计数的表实体都包含ImportId字段,先提取公共接口统一约束:
// 按ImportId实际类型调整属性类型,支持int/long/Guid等 public interface IImportTracked { Guid ImportId { get; set; } }
让所有13张表的实体类实现该接口,再在DbContext中添加通用计数方法,避免重复写相同的lambda判断:
public async Task<int> GetImportCountAsync<T>(Guid importId) where T : class, IImportTracked { return await Set<T>().CountAsync(e => e.ImportId == importId); }
第二步:性能优化,二选一即可
方案1:并行执行(改造成本低,收益明显)
EF Core的DbContext是线程不安全的,禁止同一个上下文实例并发执行查询,因此需要借助IDbContextFactory为每个并行查询创建独立的上下文实例,再通过Task.WhenAll实现并行执行,总耗时约等于最慢的单条查询耗时,远低于串行累加的耗时:
// 先在服务注册时配置DbContext工厂:services.AddDbContextFactory<ImportContext>(配置 => 配置.UseSqlServer(连接字符串)); // 注入IDbContextFactory<ImportContext> _contextFactory 即可使用 // 构造所有计数任务 var countTasks = new List<Task<int>> { _contextFactory.CreateDbContextAsync().AsTask().ContinueWith(async ctxTask => { await using var ctx = await ctxTask; return await ctx.GetImportCountAsync<Table1>(importId); }).Unwrap(), _contextFactory.CreateDbContextAsync().AsTask().ContinueWith(async ctxTask => { await using var ctx = await ctxTask; return await ctx.GetImportCountAsync<Table2>(importId); }).Unwrap(), // 剩余11张表按相同格式追加即可,代码重复度极低 }; // 并行执行所有查询 var countResults = await Task.WhenAll(countTasks); // 按任务添加顺序取值即可 var countA = countResults[0]; var countB = countResults[1]; // ...
若数据库负载较高,可通过
SemaphoreSlim限制最大并行查询数,避免打满数据库连接池。
方案2:单查询批量返回(性能最优)
将所有计数逻辑拼成一条SQL,仅需一次数据库往返即可拿到所有结果,性能比多连接并行更高,维护成本也很低:
首先定义接收结果的DTO:
public class ImportCountSummary { public int CountA { get; set; } public int CountB { get; set; } public int CountC { get; set; } public int CountD { get; set; } // 追加剩余9张表对应的计数属性 }
执行单条原生SQL查询:
var querySql = @" SELECT (SELECT COUNT(*) FROM table1 WHERE ImportId = {0}) AS CountA, (SELECT COUNT(*) FROM table2 WHERE ImportId = {0}) AS CountB, (SELECT COUNT(*) FROM table3 WHERE ImportId = {0}) AS CountC, (SELECT COUNT(*) FROM table4 WHERE ImportId = {0}) AS CountD -- 按格式追加剩余9张表的计数子查询 "; var summary = await _importContext.Database .SqlQueryRaw<ImportCountSummary>(querySql, importId) .FirstAsync();
拿到的summary对象就包含所有表的计数结果,全程只有一次数据库交互,是性能最优的实现方式。
内容的提问来源于stack exchange,提问作者Ms1
相关产品推荐
相关产品推荐

