LINQ Group By 方法未生成预期 SQL

LINQ Group By method not generating the expected SQL

以下 LINQ 查询应该 return 每个用户的登录次数:

控制器:

var lst = _context.LoginHistory.GroupBy(l => l.UserName).Select(lg => new { user_name = lg.Key, cnt = lg.Count() });

 return View(lst.ToList());

但是 SQL Profiler of SQL Server 2012 正在 return 进行以下奇怪的查询:

SQL 分析器输出:

SELECT [l].[LoginHistory], [l].[Hit_Count], [l].[LastLogin], [l].[UserName]
FROM [LoginHistory] AS [l]
ORDER BY [l].[UserName]

型号:

public class LoginHistory
{
   public int LoginHistoryId { get; set; }
   public string UserName { get; set; }
   public int Hit_Count { get; set; }
   public DateTime LoginDate { get; set; }
}

注意

  1. 我不知道为什么连 Hit_Count 列都在探查器输出查询中,因为它在这里应该不起作用 - 我只是想显示每个用户的登录总数。此外,在 SQL Profiler 输出中,我期待类似于以下内容的内容 t-sql:
  2. 这是应用程序执行的唯一 LINQ qry,所以并不是我在 SQL Profiler
  3. 中错误地选择了错误的 SQL
  4. 视图中的结果也不正确[这实际上导致我进行了此post]
  5. 中显示的所有调查
  6. 这会不会是另一位用户指出的另一个 EF Core 1.1.1 错误 here

预期[或类似]SQL探查器输出:

SELECT username, COUNT(*)
FROM LoginHistory
GROUP BY username

这次它不是真正的错误(根据 EF Core 团队的说法),而是不完整的功能(因为在 EF6 中它按您预期的方式工作)。您可以在 EF Core Roadmap:

中看到它 "documented"

The things we think we need before we say EF Core is the recommended version of EF. Until we implement these features EF Core will be a valid option for many applications, especially on platforms such as UWP and .NET Core where EF6.x does not work, but for many applications the lack of these features will make EF6.x a better option.

然后

GroupBy translation will move translation of the LINQ GroupBy operator to the database, rather than in-memory.

所谓的客户端评估(EF Core 的一项功能,在以前的 EF 版本中不存在)是万恶之源。它允许 EF Core 在内存中 "process successfully" 许多查询,从而引入性能问题(尽管根据定义它们应该产生正确的结果)。

这就是为什么我建议始终打开 EF Core Logging 以监控查询的实际情况。例如,对于示例查询,您会看到以下警告:

The LINQ expression 'GroupBy([l].UserName, [l])' could not be translated and will be evaluated locally. To configure this warning use the DbContextOptionsBuilder.ConfigureWarnings API (event id 'RelationalEventId.QueryClientEvaluationWarning'). ConfigureWarnings can be used when overriding the DbContext.OnConfiguring method or using AddDbContext on the application service provider.

The LINQ expression 'GroupBy([l].UserName, [l])' could not be translated and will be evaluated locally. To configure this warning use the DbContextOptionsBuilder.ConfigureWarnings API (event id 'RelationalEventId.QueryClientEvaluationWarning'). ConfigureWarnings can be used when overriding the DbContext.OnConfiguring method or using AddDbContext on the application service provider.

The LINQ expression 'Count()' could not be translated and will be evaluated locally. To configure this warning use the DbContextOptionsBuilder.ConfigureWarnings API (event id 'RelationalEventId.QueryClientEvaluationWarning'). ConfigureWarnings can be used when overriding the DbContext.OnConfiguring method or using AddDbContext on the application service provider.

您还可以通过在 DbContext OnConfiguring 覆盖中添加以下内容来关闭客户端评估:

optionsBuilder.ConfigureWarnings(bulder => bulder.Throw(RelationalEventId.QueryClientEvaluationWarning));

但现在您将从该查询中简单地获取运行时异常。

如果这对您很重要,那么您可能属于 应用程序的类别,缺少这些功能将使 EF6.x 成为更好的选择

许多人在使用 SQL + LINQ + Entity Framework 时以及当他们想要 运行 像您这样的简单聚合函数时发现 Sql Profiler 不反映聚合并显示与通用 SELECT * FROM table.

非常相似的内容

虽然大多数使用 LINQ 和 EF 的应用程序也在使用数据库服务器,但其他应用程序正在使用或也在使用和映射来自其他数据源的数据,例如 XML、平面文件、Excel电子表格到应用程序的 entities/models/classes.

因此在 LINQ 中聚合数据时的正常操作模式是加载和映射资源数据,然后在应用程序中执行所需的功能。

这对某些人来说可能工作得很好,但在我的情况下,我的应用程序服务器资源和大量数据库资源有限,所以我选择将这些功能转移到我的 SQL 服务器上,然后在其中创建一个方法class 使用 ADO 并执行原始 SQL.

应用于您的特定模型,我们会有类似的东西,它可能会根据您的特定编码风格和任何适用的标准而有所不同。

public class LoginHistory {
    public int LoginHistoryId { get; set; }
    public string UserName { get; set; }
    public int Hit_Count { get; set; }
    public DateTime LoginDate { get; set; }

    public List<LoginHistory> GetList_LoginTotals() {
        List<LoginHistory> retValue = new List<LoginHistory>();

        StringBuilder sbQuery = new StringBuilder();
        sbQuery.AppendLine("SELECT username, COUNT(*) ");
        sbQuery.AppendLine("FROM LoginHistory ");
        sbQuery.AppendLine("GROUP BY username");

        using (SqlConnection conn = new SqlConnection(strConn)) {
            conn.Open();
            using (SqlCommand cmd = new SqlCommand(sbQuery.ToString(), conn)) {
                cmd.CommandType = CommandType.Text;
                using (SqlDataReader reader = cmd.ExecuteReader()) {
                    while (reader.Read()) {
                        var row = new LoginHistory {
                            UserName = reader.GetString(0)
                            , Hit_Count = reader.GetInt32(1)
                        };
                        retValue.Add(row);
                    }
                }
            }
            conn.Close();
        }
        return retValue;
    }
}

并且您的控制器代码可以更新为与此类似的内容:

var LoginList = new LoginHistory().GetList_LoginTotals(); 
return View(LoginList);

// or the one liner: return View(new LoginHistory().GetList_LoginTotals());