You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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(),
    // 其余表计数逻辑
};

核心诉求:精简重复代码、消除冗余,同时优化查询性能。


问题根因

两种写法性能差的核心原因非常明确:

  1. 逐个await的写法是串行执行:13个查询依次发送到数据库,总耗时是所有查询耗时的累加,数据库连接无法复用,代码冗余度高。
  2. 匿名对象写法性能更差的原因:
    • 使用同步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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:12:32