【问题标题】:Group by hour in IQueryable在 IQueryable 中按小时分组
【发布时间】:2019-11-24 01:42:08
【问题描述】:

在我的项目中,我在所有 x 秒内都从 SPS 收到了一些数据。每隔 y 分钟,我会将当前数据存档在数据库中,以便能够显示统计数据。

我收到的数据被放入模型中。类似这样的东西,但要复杂得多:

public class Data
{
    public DateTime ArchiveTime { get; set; }
    public float TempC { get; set; }
    public float CO2Percent { get; set; }
}

我有一个数据库存储库,它返回特定时间跨度内的所有条目。请参阅此代码:

// Context is my DbContext for a SQLite db and Data is the DbSet<Data> on that
IQueryable<Data> GetDataBetween(DateTime from, DateTime to) => Context.Data.Where(d => (d.ArchiveTime >= from && d.ArchiveTime <= to));

如您所见,这会返回 IQueryable,因此我想利用 linq to entity 功能。
我相信它被称为 linq to entity,但如果不是,我的意思是将表达式树转换为 sql 或其他内容的功能,而不仅仅是在 C# 中执行它。

由于数据库中每小时的条目数量无法确定,我希望每小时只获得一个条目(第一个),以便将其显示在图表中。

以下是一些日期时间的示例,可能更能说明我的意图:
注意:这些只是对象中包含的日期时间,我想要整个对象 - 而不仅仅是时间.

// say this is all the data I get between two times
2019-07-06 10:30:01 // I want
2019-07-06 10:40:09
2019-07-06 10:50:10
2019-07-06 11:00:13 // I want
2019-07-06 11:10:20
2019-07-06 11:20:22
2019-07-06 11:30:24
2019-07-06 11:40:32
2019-07-06 11:50:33
2019-07-06 12:00:35 // I want
2019-07-06 12:10:43
2019-07-06 12:20:45
2019-07-06 12:40:54
2019-07-06 12:50:56
2019-07-06 13:00:58 // I want
2019-07-06 13:11:06
2019-07-06 13:21:08
2019-07-06 13:31:09

我目前的做法是通过IEnumerableGroupBy。看这段代码:

var now = DateTime.Now;
IQueryable<Data> dataLastWeek = repos.GetDataBetween(now.AddDays(-7), now);

IEnumerable<Data> onePerHour = dataLastWeek.AsEnumerable()
    .GroupBy(d => new DateTime(d.ArchiveTime.Year, d.ArchiveTime.Month, d.ArchiveTime.Day, d.ArchiveTime.Hour, 0, 0))
    .Select(g => g.First());

这很好用,但由于它使用IEnumerable 并创建对象,我没有获得 linq 到实体的优势,我认为这种方式必须慢很多。

有没有办法重写这个查询以在 SQLite 数据库上使用IQueryable

编辑:我正在使用 .net core 3 preview6(最新预览版)版本的 EF Core。也许有一个新功能可以满足我的需求:)

【问题讨论】:

  • 我猜你在没有 AsEnumerable 的情况下尝试过它,但它不起作用,这可能是因为 LINQ 提供程序无法真正将 new DateTime(...)First(...) 的分组转换为 SQL。但是,我认为 dataLastWeek.GroupBy(d =&gt; new { d.ArchiveTime.Year, d.ArchiveTime.Month, d.ArchiveTime.Day, d.ArchiveTime.Hour }).Select(g =&gt; g.Min(x =&gt; x.ArchiveTime)); 应该可以工作,因为这可以在 SQL 中表示为 SELECT MIN(ArchiveTime) FROM data GROUP BY DATEPART(YEAR, ArchiveTime), DATEPART(MONTH, ArchiveTime), DATEPART(DAY, ArchiveTime), DATEPART(HOUR, ArchiveTime)
  • 是的,我确实尝试过(没有AsEnumerable),你说得对,这不起作用。此外,您建议的解决方案仅返回 DateTime,而不是 Data-object 本身:/
  • AsEnumerable() 在 EF Core 中不需要。不幸的是,对于当前的 EF Core 查询翻译,由于First()FirstOrDefault(),查询将使用客户端评估。
  • @IvanStoev 如果我不使用AsEnumerable(),它认为我想在IQueryable 上执行where,这会引发异常,因为我创建了一个新的DateTime,EF 将如何知道该怎么做。有什么办法可以删除第一个呢? @juharr 提到了OrderBy,但在这种情况下你会如何使用它呢? Take(1)?
  • @ckuri 我编辑了问题以添加一个注释,我想要完整的对象,而不仅仅是日期时间。您的解决方案似乎工作正常,但如前所述,只返回日期时间。

标签: c# datetime entity-framework-core linq-to-entities iqueryable


【解决方案1】:

GroupBy 的关键部分可以通过避免使用new DateTime(...) 并使用任一匿名类型来轻松翻译

.GroupBy(d => new { d.ArchiveTime.Date, d.ArchiveTime.Hour })

Date属性+AddHours

.GroupBy(d => d.ArchiveTime.Date.AddHours(d.ArchiveTime.Hour))

不幸的是,目前 (EF Core 2.2) 不会将嵌套的 First / FirstOrDefault / Take(1) 转换为 SQL 并使用客户端评估。对于First(),它是强制的,以模拟 LINQ to Objects 的抛出行为,但对于其他两种模式,它是由于缺乏正确的翻译造成的。

对于您的具体查询,我看到的唯一服务器端解决方案是根本不使用 GroupBy,而是关联自反加入,如下所示:

var onePerHour = dataLastWeek.Where(d => !dataLastWeek.Any(d2 =>
    d2.ArchiveTime.Date == d.ArchiveTime.Date &&
    d2.ArchiveTime.Hour == d.ArchiveTime.Hour &&
    d2.ArchiveTime < d.ArchiveTime));

【讨论】:

  • 这行得通,谢谢。但是,您只检查年份和时间 - 有什么理由吗?要获得我想要的行为(并且使用旧解决方案),您还需要检查月份和日期,否则您只能获得 24 个条目。
  • 非常快.. 我不直接将数据用于图表,而是从中创建元组。这些对 ef 来说是未知的,所以如果我在 IQueryable 上执行 select,它就会抛出。当我使用AsEnumerable 时,它可以工作,但速度很慢。使用ToList() 而不是AsEnumerable 可以将速度提高大约3 倍。那么在创建元组之前在onePerHour 上调用ToList() 是否正确?
  • (1) 抱歉,复制/粘贴错误,我的意思是Date (2) 不明白你的意思,ToList() 会将所有内容加载到内存中。
  • 我已经问过a follow up question。如果你有时间的话我会很感激的:)
猜你喜欢
  • 2021-07-29
  • 2011-01-07
  • 2021-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-09
  • 1970-01-01
相关资源
最近更新 更多