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

Entity Framework中实现PARTITION BY查询取关联表最新记录

低版本Entity Framework实现取关联表最新单条记录方案

你当前用不了EF Core 5.0才支持的Filtered Include(即Include内加Where过滤的特性),最稳妥、性能最好的实现方式是拆分查询+手动绑定导航属性,兼容EF6、EF Core 3.1及更早版本,完全规避多Include产生的笛卡尔积冗余问题。


实现步骤

核心思路是放弃用EF自动加载两个历史全表,分三次独立查询,再把查询到的最新记录手动绑定到对应实体的导航属性上,EF的上下文跟踪会自动处理关联关系。

1. 查询主表基础数据

先加载Sub主表、SubsHistories关联、App基础信息,不加载两个历史表的全量数据:

// 加载主数据
var subs = await _context.Subs
    .Include(s => s.SubsHistories)
    .Include(s => s.App)
    .ToListAsync();

// 提取所有涉及的AppId,缩小后续查询范围,避免全表扫描
var appIds = subs
    .Where(s => s.App != null)
    .Select(s => s.App.Id)
    .Distinct()
    .ToList();

2. 批量查询所有App的最新历史记录

分别查询OwnerHistories、SubmitterHistories中每个App对应的最新单条记录,两种写法可选:

写法1:用LINQ分组查询(EF可自动翻译为SQL)

低版本EF可正常翻译分组取第一条的逻辑,生成的SQL和你手写的ROW_NUMBER分区查询逻辑等价:

// 查询最新Owner历史
var latestOwnerHistories = await _context.OwnerHistories
    .Where(oh => appIds.Contains(oh.AppId))
    .GroupBy(oh => oh.AppId)
    .Select(g => g.OrderByDescending(oh => oh.ODate).FirstOrDefault())
    .ToListAsync();
var ownerMap = latestOwnerHistories.ToDictionary(oh => oh.AppId, oh => oh);

// 查询最新Submitter历史
var latestSubmitterHistories = await _context.SubmitterHistories
    .Where(sh => appIds.Contains(sh.AppId))
    .GroupBy(sh => sh.AppId)
    .Select(g => g.OrderByDescending(sh => sh.SDate).FirstOrDefault())
    .ToListAsync();
var submitterMap = latestSubmitterHistories.ToDictionary(sh => sh.AppId, sh => sh);

写法2:直接执行手写原生SQL(兼容性最高)

如果你的EF版本LINQ翻译有问题,直接用你写好的原生SQL查询,完全可控:

// 查询最新Owner历史
var latestOwnerHistories = await _context.OwnerHistories.FromSqlInterpolated($@"
    SELECT * FROM (
        SELECT *, ROW_NUMBER() OVER(PARTITION BY AppId ORDER BY ODate DESC) rn
        FROM OwnerHistories
        WHERE AppId IN ({appIds})
    ) t WHERE rn = 1
").ToListAsync();
var ownerMap = latestOwnerHistories.ToDictionary(oh => oh.AppId, oh => oh);

// 查询最新Submitter历史
var latestSubmitterHistories = await _context.SubmitterHistories.FromSqlInterpolated($@"
    SELECT * FROM (
        SELECT *, ROW_NUMBER() OVER(PARTITION BY AppId ORDER BY SDate DESC) rn
        FROM SubmitterHistories
        WHERE AppId IN ({appIds})
    ) t WHERE rn = 1
").ToListAsync();
var submitterMap = latestSubmitterHistories.ToDictionary(sh => sh.AppId, sh => sh);

3. 手动绑定导航属性

把查询到的最新记录绑定到对应App的导航属性上,替代原来Include加载的全量集合:

foreach (var sub in subs)
{
    if (sub.App == null) continue;

    // 绑定最新Owner记录
    sub.App.OwnerHistories = ownerMap.TryGetValue(sub.App.Id, out var owner)
        ? new List<OwnerHistory> { owner }
        : new List<OwnerHistory>();

    // 绑定最新Submitter记录
    sub.App.SubmitterHistories = submitterMap.TryGetValue(sub.App.Id, out var submitter)
        ? new List<SubmitterHistory> { submitter }
        : new List<SubmitterHistory>();
}

方案优势

  • 兼容性极强:支持所有EF版本,不需要依赖高版本特性
  • 性能远高于多Include写法:总共只执行3次查询,完全避免多一对多关联JOIN产生的笛卡尔积冗余,返回数据量和你手写的原生SQL完全一致
  • 逻辑可控:不会出现EF自动生成SQL不符合预期的问题,历史表数据量越大,性能提升越明显

如果最终返回给业务层不需要直接返回EF实体,也可以在查询完所有数据后直接映射为DTO,省去手动给导航属性赋值的步骤,性能还能再提升。


内容的提问来源于stack exchange,提问作者user989988

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 15:00:46