2017-10-09 281 views
0

比方说,我有一个代表在某个时间值的EF实体类:LINQ - 过滤,分组和获得最小值和最大值

public class Point 
{ 
    public DateTime DT {get; set;} 
    public decimal Value {get; set;} 
} 

我也代表某个时间段的一类:

public class Period 
{ 
    public DateTime Begin {get; set;} 
    public DateTime End {get; set;} 
} 

然后我有Period的数组,可以包含一些特定的时间段,让我们说,它看起来像(Period对象总是按升序排列数组中):

var periodSlices = new Period [] 
{ 
    new Period { Begin = new DateTime(2016, 10, 1), End = new DateTime(2016, 10, 15)}, 
    new Period { Begin = new DateTime(2016, 10, 16), End = new DateTime(2016, 10, 20)}, 
    new Period { Begin = new DateTime(2016, 10, 21), End = new DateTime(2016, 12, 30)} 
}; 

现在,使用LINQ to SQL如何写在每个的periodSlices这将有最古老的(分)滤除和组Point的查询和最新的(最大)值,所以在这个例子场景中的结果应该有一组3个最小和最大点(当然如果有的话)。

所以我需要的结果就像IQueryable<Period, IEnumerable<Point>>

现在我做这种方式,但性能不是最大:

using (var context = new EfDbContext()) 
{ 
    var periodBegin = periodSlices[0].Begin; 
    var periodEnd = periodSlices[periodSlices.Length - 1].End; 

    var dbPoints = context.Points.Where(p => p.DT >= periodBegin && p.DT <= periodEnd).ToArray(); 

    foreach (var slice in periodSlices) 
    { 
     var points = dbPoints.Where(p => p.DT >= slice.Begin && p.DT <= slice.End); 

     if (points.Any()) 
     { 
      var latestValue = points.MaxBy(u => u.DT).Value; 
      var earliestValue = points.MinBy(u => u.DT).Value; 
     } 
    } 
} 

性能是至关重要的(速度越快越好,因为我需要过滤掉和组〜点100K)。

+0

如果在你的集合项目很多,你可以使用Parallel.ForEach,它可以提高速度 – Ferus7

+1

查询这个复杂的是不理想的EF,因为它是不可能的框架,以生成优化的查询此复杂。你可以做两件事:1)创建一个你可以用EF调用的存储过程。 2)_Maybe_创建一个视图来查询最小和最大值,但我认为如果你正在寻找3组数据,你很可能需要查询它3次,使它不理想。 – krillgar

+0

在你的例子中,你为什么重复'periodSlices'?无论“切片”的值如何,您只需运行相同的代码3次。 – Rotem

回答

2

如果你想得到每个时间片的最早(最小)和最新(最大)点,我首先要看的是让数据库做更多的事情。

当您调用.ToArray()时,它会将所有选定的点带入内存。这是毫无意义的,因为你只需要每片2片。所以,如果你没有服用点,如:

foreach (var slice in periodSlices) 
{ 
    var q = context 
       .Points 
       .Where(p => p.DT >= slice.Begin && p.DT <= slice.End) 
       .OrderBy(x => x.DT); 
    var min = q.FirstOrDefault(); 
    var max = q.LastOrDefault(); 
} 

威力更好地工作

我说可能因为这要看是什么指标有在数据库上多少点在每个切片。最终要获得非常好的性能,您可能必须在日期时间上添加索引,或者更改结构,以便预先存储最小值和最大值,或者在存储过程中执行此操作。

+0

我已经尝试过类似的方法,性能很不错 – pitersmx

+1

另外就我所知,LINO to SQL中不支持'LastOrDefault'。因此,我需要的数据进行两次排序:升序,并得到FirstOrDefault,然后下降,并得到FirstOrDefault – pitersmx

+0

@pitersmx是的好点 – mikelegg

4

这里是一个SQL查询的解决方案:

var baseQueries = periodSlices 
    .Select(slice => db.Points 
     .Select(p => new { Period = new Period { Begin = slice.Begin, End = slice.End }, p.DT }) 
     .Where(p => p.DT >= p.Period.Begin && p.DT <= p.Period.End) 
    ); 

var unionQuery = baseQueries 
    .Aggregate(Queryable.Concat); 

var periodQuery = unionQuery 
    .GroupBy(p => p.Period) 
    .Select(g => new 
    { 
     Period = g.Key, 
     MinDT = g.Min(p => p.DT), 
     MaxDT = g.Max(p => p.DT), 
    }); 

var finalQuery = 
    from p in periodQuery 
    join pMin in db.Points on p.MinDT equals pMin.DT 
    join pMax in db.Points on p.MaxDT equals pMax.DT 
    select new 
    { 
     Period = p.Period, 
     EarliestPoint = pMin, 
     LatestPoint = pMax, 
    }; 

我已经分开了LINQ查询部分为独立的变量,只是可读性。为了得到结果,只有最终的查询应执行:

var result = finalQuery.ToList(); 

基本上,我们建立一个UNION ALL查询每个切片,然后确定最小和最大的日期来回各个时期,并最终得到了相应的数值为这些日期。我使用join代替分组内的“典型”OrderBy(Descending) + FirstOrDefault(),因为后者会生成可怕的SQL。

现在,主要问题。我不能说这是否会比原来的方法快 - 这取决于DT列是否索引和periodSlices计数,因为每个切片查询,这3片看起来像这样

增加了另一个 UNION ALL SELECT从源表
SELECT 
    [GroupBy1].[K1] AS [C1], 
    [GroupBy1].[K2] AS [C2], 
    [GroupBy1].[K3] AS [C3], 
    [Extent4].[DT] AS [DT], 
    [Extent4].[Value] AS [Value], 
    [Extent5].[DT] AS [DT1], 
    [Extent5].[Value] AS [Value1] 
    FROM (SELECT 
     [UnionAll2].[C1] AS [K1], 
     [UnionAll2].[C2] AS [K2], 
     [UnionAll2].[C3] AS [K3], 
     MIN([UnionAll2].[DT]) AS [A1], 
     MAX([UnionAll2].[DT]) AS [A2] 
     FROM (SELECT 
      1 AS [C1], 
      @p__linq__0 AS [C2], 
      @p__linq__1 AS [C3], 
      [Extent1].[DT] AS [DT] 
      FROM [dbo].[Point] AS [Extent1] 
      WHERE ([Extent1].[DT] >= @p__linq__0) AND ([Extent1].[DT] <= @p__linq__1) 
     UNION ALL 
      SELECT 
      1 AS [C1], 
      @p__linq__2 AS [C2], 
      @p__linq__3 AS [C3], 
      [Extent2].[DT] AS [DT] 
      FROM [dbo].[Point] AS [Extent2] 
      WHERE ([Extent2].[DT] >= @p__linq__2) AND ([Extent2].[DT] <= @p__linq__3) 
     UNION ALL 
      SELECT 
      1 AS [C1], 
      @p__linq__4 AS [C2], 
      @p__linq__5 AS [C3], 
      [Extent3].[DT] AS [DT] 
      FROM [dbo].[Point] AS [Extent3] 
      WHERE ([Extent3].[DT] >= @p__linq__4) AND ([Extent3].[DT] <= @p__linq__5)) AS [UnionAll2] 
     GROUP BY [UnionAll2].[C1], [UnionAll2].[C2], [UnionAll2].[C3]) AS [GroupBy1] 
    INNER JOIN [dbo].[Point] AS [Extent4] ON [GroupBy1].[A1] = [Extent4].[DT] 
    INNER JOIN [dbo].[Point] AS [Extent5] ON [GroupBy1].[A2] = [Extent5].[DT] 
+0

谢谢,我会尽快尝试。现在,'DT'列没有索引,我会为它创建一个索引,但我担心它会影响'Points'表的'写'性能。 – pitersmx

相关问题