2017-10-09 275 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

相關問題