You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

EF Core 3模拟PARTITION BY获取每组最新记录遇异常求助

解决EF Core 3.x中分组获取每组最新记录的问题

这确实是EF Core 3.x升级后常见的问题——3.0版本重写了查询翻译器,对GroupBy的翻译做了更严格的限制,原来EF Core 2.2中依赖客户端评估的分组取Top1写法,在3.x里因为默认禁用了客户端评估,就会抛出你遇到的InvalidOperationException

为什么原来的写法失效?

你最初的LINQ查询:

from lfv in dbo.ListingFlagValues 
group lfv by lfv.ListingId into groups 
select groups.OrderByDescending(x => x.Timestamp).FirstOrDefault();

在EF Core 2.2中,EF会先执行GROUP BY ListingId的SQL,然后在客户端对每个分组进行排序取第一条;但EF Core 3.x要求所有查询逻辑都能翻译成SQL,而这种“分组后取每组Top1”的逻辑无法直接映射到标准SQL的GROUP BY语法,所以翻译失败。

对应目标SQL的最优解法:使用窗口函数

既然你的目标效果等价于带MAX([Timestamp]) OVER (PARTITION BY)的SQL,EF Core 3.x及以上已经支持窗口函数的翻译,我们可以直接用RowNumber()窗口函数来实现,这也是性能最优的方案:

var latestFlagValues = db.ListingFlagValues
    // 先过滤FlagId=1的条件,和你的目标SQL对齐
    .Where(lfv => lfv.FlagId == 1)
    .Select(lfv => new 
    {
        lfv.ListingId,
        lfv.NewFlagValueId,
        lfv.Timestamp,
        // 按ListingId分组,倒序Timestamp,生成行号
        RowNumber = EF.Functions.RowNumber()
            .Over()
            .PartitionBy(lfv.ListingId)
            .OrderByDescending(lfv.Timestamp)
    })
    // 只取每组的第一行(最新记录)
    .Where(x => x.RowNumber == 1)
    // 映射成你需要的结果结构
    .Select(x => new 
    {
        x.ListingId,
        ValueId = x.NewFlagValueId
    })
    .ToList();

这个查询会直接翻译成和你目标SQL几乎一致的SQL语句,完全在数据库端执行,没有客户端评估的开销。

备选方案:子查询匹配最大Timestamp

如果你不想用窗口函数,也可以用子查询的方式实现,逻辑是检查每条记录的Timestamp是否是对应ListingId下的最大Timestamp:

var latestFlagValues = db.ListingFlagValues
    .Where(lfv => lfv.FlagId == 1)
    .Where(lfv => lfv.Timestamp == db.ListingFlagValues
        .Where(g => g.ListingId == lfv.ListingId && g.FlagId == 1)
        .Max(g => g.Timestamp))
    .Select(lfv => new 
    {
        lfv.ListingId,
        ValueId = lfv.NewFlagValueId
    })
    .ToList();

注意:如果同一ListingId下有多个记录的Timestamp完全相同,这个写法会返回所有符合的记录;而窗口函数的写法只会返回其中一条(取决于排序规则),你可以根据业务需求选择。

关于你尝试的第二种写法

你尝试的分组后嵌套SelectMany的写法,逻辑上是对的,但因为EF Core 3.x对GroupBy导航属性(GroupBy(x => x.Listing))的翻译支持有限,而且这种写法会先取出所有分组数据再过滤,性能不如窗口函数方案,所以更推荐用窗口函数实现。

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

火山引擎 最新活动