2013-05-27 21 views
9

Ich habe eine Entitätssammlung von Readings. Jede Reading ist mit einer Entität namens Meter verknüpft. (Und jede Meter hält mehrere Messwerte). Jede Reading enthält ein Feld für Meter-ID (int) und ein Feld für die Zeit.Effiziente LINQ to Entities Abfrage

hier etwas vereinfacht Code, um es zu zeigen:

public class Reading 
{ 
    int Id; 
    int meterId; 
    DateTime time; 
} 

public class Meter 
{ 
    int id; 
    ICollection<Readings> readings;  
} 

einen bestimmten Zeitraum und eine Liste des meterid s Gegeben, was der effizienteste Weg sein würde, für jeden Meter die erste und letzte Lesung zu bekommen dieser Zeitraum?

Ich bin in der Lage, durch alle Meter zu durchlaufen und für jeden Meter erste und letzte Lesung für den Zeitraum obatin, aber ich irrte, wenn es eine effizientere Art und Weise, dies zu acheive.

Und eine Bonus Frage: gleiche Frage, aber mit mehreren Zeitabschnitten, um Daten zu erhalten, statt nur einer Periode.

+0

Haben Sie versucht, 'Queryable.First()' und 'Queryable.Last (')? –

+0

Ja, meine Lösung ist für jeden Meter und Zeitraum, First() und Last() zu nehmen - aber das berücksichtigt nicht die Tatsache, dass ich für alle Meter den gleichen Zeitraum betrachte. vielleicht wäre eine Art Gruppierung hier effizienter? –

Antwort

3

Ich bin nicht ganz sicher, wie Sie diese Daten wollen, aber man konnte es in eine anonyme Art projizieren:

var metersFirstAndLastReading = meters.Select(m => new 
    { 
     Meter = m, 
     FirstReading = m.readings.OrderBy(r => r.time).First(), 
     LastReading = m.readings.OrderBy(r => r.time).Last() 
    }); 

Anschließend können Sie Ihre Ergebnisliste wie folgt lesen (in diesem Beispiel nur als Illustration zu verstehen ist):

foreach(var currentReading in metersFirstAndLastReading) 
{ 
    string printReadings = String.Format("Meter id {0}, First = {1}, Last = {2}", 
           currentReading.Meter.id.ToString(), 
           currentReading.FirstReading.time.ToString(), 
           currentReading.LastReading.time.ToString()); 

    // Do something... 
} 

Eine andere Möglichkeit wäre, Eigenschaften in Meter schaffen, welche die ersten und letzten Messwerte dynamisch zurück:

public class Meter 
{ 
    public int id; 
    public List<Reading> readings; 

    public Reading FirstReading 
    { 
     get 
     { 
      return readings.OrderBy(r => r.time).First(); 
     } 
    } 

    public Reading LastReading 
    { 
     get 
     { 
      return readings.OrderBy(r => r.time).Last(); 
     } 
    } 
} 

EDIT: Ich habe die Frage ein wenig missverstanden.

Hier ist die Implementierung ist es, die ersten und letzten Ablesungen für einen Zähler einschließlich einen Datumsbereich (unter der Annahme meterIdList eine ICollection<int> von IDs und begin und end wird den angegebenen Datumsbereich)

var metersFirstAndLastReading = meters 
    .Where(m => meterIdList.Contains(m.id)) 
    .Select(m => new 
    { 
     Meter = m, 
     FirstReading = m.readings 
         .Where(r => r.time >= begin && r.time <= end) 
         .OrderBy(r => r.time) 
         .FirstOrDefault(), 
     LastReading = m.readings 
         .Where(r => r.time >= begin && r.time <= end) 
         .OrderByDescending(r => r.time) 
         .FirstOrDefault() 
    }); 

um zu bestimmen, Sie können jetzt keine Eigenschaften mehr verwenden (da Sie Parameter angeben müssen), so dass die Methoden problemlos als Alternative funktionieren:

public class Meter 
{ 
    public int id; 
    public List<Reading> readings; 

    public Reading GetFirstReading(DateTime begin, DateTime end) 
    { 
     var filteredReadings = readings.Where(r => r.time >= begin && r.time <= end); 

     if(!HasReadings(begin, end)) 
     { 
      throw new ArgumentOutOfRangeException("No readings available during this period"); 
     } 

     return filteredReadings.OrderBy(r => r.time).First(); 
    } 

    public Reading GetLastReading(DateTime begin, DateTime end) 
    { 
     var filteredReadings = readings.Where(r => r.time >= begin && r.time <= end); 

     if(!HasReadings(begin, end)) 
     { 
      throw new ArgumentOutOfRangeException("No readings available during this period"); 
     } 

     return filteredReadings.OrderBy(r => r.time).Last(); 
    } 

    public bool HasReadings(DateTime begin, DateTime end) 
    { 
     return readings.Any(r => r.time >= begin && r.time <= end); 
    } 
} 
+0

Sie müssen where-Klauseln hinzufügen, um den Zeitraum zu berücksichtigen – jammykam

+0

'Lesen gehört zu einem' Meter', so dass Sie keine Filterung durchführen müssen. – davenewza

+0

@davenewza, danke für Ihre Antwort. Filterung nach Zeit ist erforderlich. Aber ich bin mir nicht sicher, ob dies die Leistung steigert. es geht immer noch durch jeden Meter und fragt nach dem ersten und letzten, oder? –

0

Erstellen Sie eine neue Klasse, die als Rückgabetyp Result genannt, die wie diese

public class Result 
{ 
    public int MeterId; 
    public Readings Start; 
    public Readings Last; 
} 

ich, indem sie eine Liste von Zählern und bevölkert einige Daten, Ihre Abfrage sollte so ziemlich das gleiche sein, obwohl

emuliert Ihre Situation aussieht
var reads = Meters.Where(x => x.readings != null) 
        .Select(x => new Result 
          { 
           MeterId = x.id, 
           Start = x.readings.Select(readings => readings).OrderBy(readings=>readings.time).FirstOrDefault(), 
           Last = x.readings.Select(readings=>readings).OrderByDescending(readings=>readings.time).FirstOrDefault() 
          }); 
+0

Oder OP könnte anonyme Typen verwenden. – davenewza

+0

Ja, ich bevorzuge nur getippt, aber anon ist auch gut. Ich denke, dass die Verwendung eines Rückgabetyps ein wenig Klarheit bringt. – James

0
public IEnumerable<Reading> GetFirstAndLastInPeriod 
    (IEnumerable<Reading> readings, DateTime begin, DateTime end) 
{ 
    return 
     from reading in readings 
     let span = readings.Where(item => item.time >= begin && item.time <= end) 
     where reading.time == span.Max(item => item.time) 
      || reading.time == span.Min(item => item.time) 
     select reading;    
} 
0
meters.Where(mt=>desiredMeters.Contains(mt)).Select(mt=> 
    new{ 
    mt.Id, 
    First = mt.Readings.Where(<is in period>).OrderBy(rd=>rd.Time).FirstOrDefault(), 
    Last = mt.Readings.Where(<is in period>).OrderBy(rd=>rd.Time).LastOrDefault() 
    }); 

Wenn Sie viele Messwerte pro Meter haben, wird dies nicht gut durchführen, und Sie sollten Lesungen betrachten von SortedList Klasse sein .

0

meine Lösung wird wieder genau, was u (Liste aller Meters enthält Lesungen innerhalb der gegebenen Zeitperiode) wollen

public IList<Reading[]> GetFirstAndLastReadings(List<Meter> meterList, DateTime start, DateTime end) 
    {  
     IList<Reading[]> fAndlReadingsList = new List<Reading[]>(); 

      meterList.ForEach(x => x.readings.ForEach(y => 
      { 
       var readingList = new List<Reading>(); 
       if (y.time >= startTime && y.time <= endTime) 
       { 
         readingList.Add(y); 
         fAndlReadingsList.Add(new Reading[] { readingList.OrderBy(reading => reading.time).First(), readingList.OrderBy(reading => reading.time).Last() }); 
       } 
      })); 

     return fAndlReadingsList; 
    } 
1

Ich habe eine sehr ähnliche Datenmodell, in dem dieser Code verwendet wird, um die ältesten Messwerte zu erhalten Ich habe es gerade geändert, um auch die neuesten aufzunehmen.

Ich benutze Abfragesyntax, so etwas zu tun:

var query = from reading in db.Readings 
      group reading by reading.meterId 
      into readingsPerMeter 
      let oldestReadingPerMeter = readingsPerMeter.Min(g => g.time) 
      let newestReadingPerMeter = readingsPerMeter.Max(g => g.time) 
      from reading in readingsPerMeter 
      where reading.time == oldestReadingPerMeter || reading.time == newestReadingPerMeter 
      select reading; //returns IQueryable<Reading> 

, die für jeden Meter in einen nur den neuesten und ältesten Messwerte zur Folge hätte.

Der Grund, warum ich denke, dass dies effizient ist, ist, weil es einen Blick auf die DB, um alle Messwerte für jeden Meter zu erhalten, statt mehrere Nachschlagewerke für jeden Meter. Wir haben ~ 40000 Meter mit ~ 30mil Lesungen. Ich habe gerade den Lookup auf unseren Daten getestet es dauerte etwa 10s

Die vorgeformte SQL ist ein Crossjoin zwischen zwei Subselects für jedes der Min und Max Daten.

UPDATE:

Da dies abfragbaren sollten Sie in der Lage sein, eine Zeit nach liefern, wie folgt aus:

query.Where(r=>r.time > someTime1 && r.time < someTime2) 

Oder es in die ursprüngliche Anfrage gestellt, ich genauso wie es wie folgt getrennt. Die Abfrage wird noch nicht ausgeführt, da wir noch keine Aktion ausgeführt haben, mit der die Daten abgerufen werden.

+0

@Thanks, bitte beachten Sie, dass: a. Das filtert nicht nach der Zeit. b. Es gibt entweder den ersten oder den letzten Messwert und nicht beide zurück. aber ich denke, ich habe die Idee. –

+0

Aktualisiert, um den Periodenteil hinzuzufügen, nimmt es die neueste und älteste Lesung innerhalb der Periode für jeden Meter. – FRoZeN

+0

dies scheint die Leistung zu verbessern. Aber für jeden Meter gibt es nur eine Lesung, nicht zwei. Irgendeine Idee, wie man die erste Anzeige zuletzt bekommt? Danke –

0

Ich habe einige sehr gute Leads bekommen, danke an alle Antwortenden. Hier ist die Lösung, die für mich gearbeitet:

 /// <summary> 
     /// Fills the result data with meter readings matching the filters. 
     /// only take first and last reading for each meter in period. 
     /// </summary> 
     /// <param name="intervals">time intervals</param> 
     /// <param name="meterIds">list of meter ids.</param> 
     /// <param name="result">foreach meter id , a list of relevant meter readings</param> 
     private void AddFirstLastReadings(List<RangeFilter<DateTime>> intervals, List<int> meterIds, Dictionary<int, List<MeterReading>> result) 
     { 
      foreach (RangeFilter<DateTime> interval in intervals) 
      { 
       var metersFirstAndLastReading = m_context.Meter.Where(m => meterIds.Contains(m.Id)).Select(m => new 
       { 
        MeterId = m.Id, 
        FirstReading = m.MeterReading 
            .Where(r => r.TimeStampLocal >= interval.FromVal && r.TimeStampLocal < interval.ToVal) 
            .OrderBy(r => r.TimeStampLocal) 
            .FirstOrDefault(), 
        LastReading = m.MeterReading 
            .Where(r => r.TimeStampLocal >= interval.FromVal && r.TimeStampLocal < interval.ToVal) 
            .OrderByDescending(r => r.TimeStampLocal) 
            .FirstOrDefault() 
       }); 

       foreach (var firstLast in metersFirstAndLastReading) 
       { 
        MeterReading firstReading = firstLast.FirstReading; 
        MeterReading lastReading = firstLast.LastReading; 

        if (firstReading != null) 
        { 
         result[firstLast.MeterId].Add(firstReading); 
        } 

        if (lastReading != null && lastReading != firstReading) 
        { 
         result[firstLast.MeterId].Add(lastReading); 
        } 

       } 

      } 
     } 


    } 
Verwandte Themen