在IQueryable中按小时分组

时间:2019-07-15 10:57:14

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

在我的项目中,我全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实体功能。
我相信它被称为linq到实体,但如果不是这样,我的意思是将表达式树转换为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一起使用?

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

1 个答案:

答案 0 :(得分:2)

通过避免使用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));