【问题标题】:Efficient LINQ to Entities query高效的 LINQ to Entities 查询
【发布时间】:2013-05-27 06:02:50
【问题描述】:

我有一个Readings 的实体集合。 每个Reading 都链接到一个名为Meter 的实体。 (并且每个Meter 都有多个读数)。 每个Reading 都包含一个仪表ID (int) 字段和一个时间字段。

这里有一些简化的代码来演示它:

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

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

给定一个特定时期和meterids 列表, 获得每个仪表的最有效方法是什么 那个时间段的第一次和最后一次阅读?

我能够遍历所有仪表,并为每个仪表获取 该期间的第一次和最后一次阅读, 但如果有更有效的方法来实现这一点,我正在徘徊。

还有一个额外问题:同一个问题,但需要多个时间段来获取数据, 而不是只有一个时期。

【问题讨论】:

  • 你试过Queryable.First()Queryable.Last()吗?
  • 是的,我的解决方案是每个仪表和周期都采用 First() 和 Last() - 但这并没有考虑到所有仪表我都在同一时间查看的事实时期。也许某种分组在这里会更有效?

标签: c# .net linq entity-framework linq-to-entities


【解决方案1】:

我不完全确定您想要这些数据的方式,但您可以将其投影为匿名类型:

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

然后您可以像这样阅读您的结果列表(此示例仅作为说明):

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...
}

另一种选择是在 Meter 中创建属性,动态返回第一个和最后一个读数:

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();
        }
    }
}

编辑:我有点误解了这个问题。

这是确定仪表的第一个和最后一个读数的实现包括一个日期范围(假设meterIdList是ID的ICollection&lt;int&gt;beginend是指定日期范围)

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()
    });

您现在将无法使用属性(因为您需要提供参数),因此方法可以作为替代方案正常工作:

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);
    }
}

【讨论】:

  • 您需要添加'where'子句以考虑时间段
  • Readings 属于Meter,因此您无需执行任何过滤。
  • @davenewza,感谢您的回复。需要按时间过滤。但无论如何,我不确定这是否会提高性能。它仍然通过每一米,并要求第一个和最后一个,对吧?
  • @davenewza ,我最终将您的解决方案与meterFirstAndLastReading一起使用(在“误解”短语之后的第二个。它确实有帮助。如果您愿意纠正两件事,我会很高兴标记它作为答案。1. Last() 无效。相反,您需要按降序排序并选择 First() (或者更好的是 FirstOrDefault() )。2. 缺少按仪表 ID 过滤。我将添加对我的问题有效的代码。干杯:-)
  • @omer:使用meterIdList 更新了代码并修复了OrderByDescending 部分(哎呀!)。是否要使用 First()FirstByDefault() 取决于数据的性质。您肯定希望您的代码在最肯定需要数据时抛出异常 - 这取决于您:)
【解决方案2】:

我有一个非常相似的数据模型,此代码用于获取最旧的读数,我只是将其更改为也包括最新的。

我使用查询语法来做这样的事情:

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> 

这将导致每个仪表只有最新和最旧的读数。

我认为这是有效的原因是因为它对数据库进行一次查找以获取每个仪表的所有读数,而不是对每个仪表进行多次查找。我们有约 40000 米,读数约 3000 万。我刚刚测试了对我们数据的查找大约需要 10 秒

执行的 sql 是针对每个最小和最大日期的两个子选择之间的交叉连接。

更新:

由于这是可查询的,您应该能够在之后提供一个句点,如下所示:

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

或者把它放到原始查询中,我就是喜欢这样分开。查询尚未执行,因为我们尚未执行获取数据的操作。

【讨论】:

  • @谢谢,请注意:这不会按时间过滤。湾。它返回 first 或 last reading ,而不是两者。但我想我明白了。
  • 更新增加了句点部分,每米会在该句点内取最新和最旧的读数。
  • 这似乎确实提高了性能。但是对于每一米,它只给出一个读数,而不是两个。关于如何最后获得第一个广告的任何想法?谢谢
  • 是的,我可以看到问题在于,当翻译成 sql 时,这会返回包含两个结果的一行,而不是每个结果的一行(最小值和最大值)
【解决方案3】:

创建一个名为Result的新类作为返回类型,如下所示

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

我通过列出仪表并填充一些数据来模拟您的情况,但您的查询应该几乎相同

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()
                          });

【讨论】:

  • 或者 OP 可以使用匿名类型。
  • 是的,我只是喜欢打字,但匿名也很好。我认为使用返回类型会增加一点清晰度。
【解决方案4】:
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;            
}

【讨论】:

    【解决方案5】:
    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()
       });
    

    如果每米有很多读数,这将不会很好,你应该考虑读数是 SortedList 类。

    【讨论】:

      【解决方案6】:

      我的解决方案将准确返回您想要的(包含给定时间段内读数的所有仪表列表)

      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;
          }
      

      【讨论】:

        【解决方案7】:

        感谢所有响应者,我得到了一些非常好的线索。 这是对我有用的解决方案:

                /// <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);
                            }
        
                        }
        
                    }
                }
        
        
            }
        

        【讨论】:

          猜你喜欢
          • 2019-02-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多