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

EF Core LINQ查询无法转换:用户考试统计查询重构求助

问题重构:EF Core用户考试统计查询优化

需求说明

需统计每个用户的考试次数、单场考试最低分、最高分及平均分,目标生成的SQL如下:

with
agg as (
    select
        s.UserId,
        count(*) cnt,
        min(s.sump) minPoints,
        max(s.sump) maxPoints,
        avg(s.sump) avgPoints
    from (
        select
            e.UserId,
            sum(a.Points) sump
        from Examinations e 
        join UserAnswers ua on ua.ExamId = e.id
        join Answers a on a.Id = ua.AnswerId
        group by e.UserId, e.Id
    ) s
    group by s.UserId
)
select
    u.UserId,
    u.Name,
    u.Surname,
    u.Patronimic,
    agg.cnt,
    agg.minPoints,
    agg.maxPoints,
    agg.avgPoints
from AppUsers u
join agg on u.UserId = agg.UserId

当前问题

编写的LINQ代码逻辑嵌套复杂,EF Core无法将其翻译为SQL,报错「could not be translated」。原代码如下:

_db.AppUsers
.GroupJoin(
    _db.Examinations
    .Include(x => x.UserAnswers)
    .Where(x => x.FinishDateTime != null && x.ExamPassed != null),
    user => user.UserId,
    exam => exam.UserId,
    (user, exam) => new
    {
        user.UserId,
        user.Name,
        user.Surname,
        user.Patronimic,
        aggregates = exam
            .Select(g => new
            {
                g.UserId,
                UserPoints = g.UserAnswers
                    .Join(
                        _db.Answers,
                        uAnswer => uAnswer.AnswerId,
                        answer => answer.Id,
                        (uAnswer, answer) => new { answer.Points }
                    ).Sum(x => x.Points)
            })
            .GroupBy(g => g.UserId)
            .Select(x => new
            {
                min = x.Min(a => a.UserPoints),
                max = x.Max(a => a.UserPoints),
                avg = x.Average(a => a.UserPoints),
                cnt = x.Count()
            })
            .FirstOrDefault()
    }
)
.Select(a => new UserExamsTotalModel
{
    UserId = a.UserId,
    UserName = a.Name,
    UserSurname = a.Surname,
    UserPatronimic = a.Patronimic,
    UserTookeTestsTotal = a.aggregates != null ? a.aggregates.cnt : 0,
    MinPointsInTest = a.aggregates != null ? a.aggregates.min : 0,
    MaxPointsInTest = a.aggregates != null ? a.aggregates.max : 0,
    AvgPointsInTest = a.aggregates != null ? a.aggregates.avg : 0
})
.ToListAsync();

实体模型参考

public class ExaminationModel
{
    public int Id { get; set; }
    public int IdentityUserId { get; set; }
    public int TestId { get; set; }
    public DateTime StartDateTime { get; set; }
    public DateTime? FinishDateTime { get; set; }
    public bool? ExamPassed { get; set; }
}
public class UserAnswer
{
    public int Id { get; set; }
    public int ExamId { get; set; }
    public int QuestionId { get; set; }
    public int AnswerId { get; set; }
}
public class AnswerModel
{
    public int Id { get; set; }
    public string Text { get; set; } = string.Empty;
    public int QuestionId { get; set; }
    public int Points { get; set; }
}
public class UserModel
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Surname { get; set; } = string.Empty;
    public string? Patronimic { get; set; }
    public DateTime BirthDate { get; set; }
    public string Email { get; set; } = string.Empty;
}

重构后的查询方案

按照目标SQL的CTE结构拆分查询,让EF Core能正确翻译:

步骤1:计算每场考试的用户得分

关联三张表,按用户ID和考试ID分组计算单场总分:

var examScores = _db.Examinations
    .Where(e => e.FinishDateTime != null && e.ExamPassed != null)
    .Join(_db.UserAnswers, e => e.Id, ua => ua.ExamId, (e, ua) => new { e.UserId, e.Id, ua.AnswerId })
    .Join(_db.Answers, x => x.AnswerId, a => a.Id, (x, a) => new { x.UserId, x.Id, a.Points })
    .GroupBy(x => new { x.UserId, x.Id })
    .Select(g => new
    {
        g.Key.UserId,
        ExamScore = g.Sum(x => x.Points)
    });

步骤2:聚合用户的考试统计数据

基于单场得分,按用户ID聚合统计核心指标:

var userStats = examScores
    .GroupBy(s => s.UserId)
    .Select(g => new
    {
        UserId = g.Key,
        ExamCount = g.Count(),
        MinScore = g.Min(s => s.ExamScore),
        MaxScore = g.Max(s => s.ExamScore),
        AvgScore = g.Average(s => s.ExamScore)
    });

步骤3:关联用户表获取完整信息

将统计数据与用户表关联,映射到目标模型:

var result = _db.AppUsers
    .Join(userStats, u => u.UserId, stats => stats.UserId, (u, stats) => new UserExamsTotalModel
    {
        UserId = u.UserId,
        UserName = u.Name,
        UserSurname = u.Surname,
        UserPatronimic = u.Patronimic,
        UserTookeTestsTotal = stats.ExamCount,
        MinPointsInTest = stats.MinScore,
        MaxPointsInTest = stats.MaxScore,
        AvgPointsInTest = stats.AvgScore
    })
    .ToListAsync();

完整链式调用代码

如果需要合并为单链式查询:

var result = _db.AppUsers
    .Join(
        _db.Examinations
            .Where(e => e.FinishDateTime != null && e.ExamPassed != null)
            .Join(_db.UserAnswers, e => e.Id, ua => ua.ExamId, (e, ua) => new { e.UserId, e.Id, ua.AnswerId })
            .Join(_db.Answers, x => x.AnswerId, a => a.Id, (x, a) => new { x.UserId, x.Id, a.Points })
            .GroupBy(x => new { x.UserId, x.Id })
            .Select(g => new { g.Key.UserId, ExamScore = g.Sum(x => x.Points) })
            .GroupBy(s => s.UserId)
            .Select(g => new
            {
                UserId = g.Key,
                ExamCount = g.Count(),
                MinScore = g.Min(s => s.ExamScore),
                MaxScore = g.Max(s => s.ExamScore),
                AvgScore = g.Average(s => s.ExamScore)
            }),
        u => u.UserId,
        stats => stats.UserId,
        (u, stats) => new UserExamsTotalModel
        {
            UserId = u.UserId,
            UserName = u.Name,
            UserSurname = u.Surname,
            UserPatronimic = u.Patronimic,
            UserTookeTestsTotal = stats.ExamCount,
            MinPointsInTest = stats.MinScore,
            MaxPointsInTest = stats.MaxScore,
            AvgPointsInTest = stats.AvgScore
        }
    )
    .ToListAsync();

关键优化点

  • 移除Include,改用直接Join关联表,降低EF Core翻译难度
  • 拆分嵌套查询为分步聚合,逻辑与目标SQL的CTE结构对齐
  • 简化中间对象结构,避免不必要的嵌套层级

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 07:10:27