使用EF4获取SQL表的最后一行的最有效方法是什么?

时间:2012-09-02 01:15:39

标签: c# linq entity-framework entity-framework-4

我希望通过表的ID列检索表的最后一行。我目前使用的是:

var x = db.MyTable.OrderByDescending(d => d.ID).FirstOrDefault();

有没有办法以更有效的速度获得相同的结果?

1 个答案:

答案 0 :(得分:5)

我无法通过整个表格查看此查询。

您是否在ID列上没有索引?

您可以将分析查询的结果添加到您的问题中,因为这不是它应该如何。

除了分析结果外,还生成了SQL。我只能通过显式列名和一些别名来查看除select top 1 * from MyTable order by id desc之外的其他内容。如果id上的索引除了对该索引进行扫描之外的任何其他内容,也不是。

编辑:承诺解释。

Linq为我们提供了一组通用接口,在C#和VB.NET的情况下提供了一些关键字支持,用于对返回0或更多项的源进行各种操作(例如内存中集合,数据库调用,解析XML文档等。)。

这允许我们表达类似的任务,无论底层来源如何。例如,您的查询包含源代码,但我们可以采用更一般的形式:

public static YourType FinalItem(IQueryable<YourType> source)
{
  return source.OrderByDesending(d => d.ID).FirstOrDefault();
}

现在,我们可以做到:

IEnumerable<YourType> l = SomeCallThatGivesUsAList();
var x = FinalItem(db.MyTable);//same as your code.
var y = FinalItem(l);//item in list with highest id.
var z = FinalItem(db.MyTable.Where(d => d.ID % 10 == 0);//item with highest id that ends in zero.

但真正重要的是,虽然我们已经有了定义我们想要完成的操作的方法,但我们可以隐藏实际的实现。

OrderByDescending的调用产生一个对象,该对象具有关于其源的信息,以及它将在订购时使用的lambda函数。

FirstOrDefault的调用依次提供相关信息,并使用它来获取结果。

在列表的情况下,实现是生成基于Enumerable的等效代码(QueryableEnumerable镜像彼此的公共成员,他们使用的接口,如IOrderedQueryableIOrderedEnumerable等等。

这是因为,我们不知道的列表已按照我们关心的顺序(或以相反的顺序排序)排序,没有比检查每个元素更快的方法。我们可以期望的最好的是O(n)操作,我们可能会得到一个O(n log n)操作 - 取决于是否优化了排序的实现,只能从中获取一个项目*。

或者换句话说,我们可以手动编写只能在枚举上运行的代码,但效率要高于:

public static YourType FinalItem(IEnumerable<YourType> source)
{
  YourType highest = default(YourType);
  int highestID = int.MinValue;
  foreach(YourType item in source)
  {
    curID = item.ID;
    if(highest == null || curID > highestID)
    {
      highest = item;
      highestID = curID;
    }
  }
  return highest;
}

对于直接处理枚举器的一些微操作,我们可以做得稍微好一些,但只是略微增加并且额外的复杂性只会使不太好的示例代码。

由于我们不能手工做得更好,而且由于linq代码对我们的信息源的了解不多,所以我们可能希望这是最好的。它匹配。它可能做得不太好(再次,取决于我们唯一想要的一个项目的特殊情况是否被认为),但它不会被击败。

然而,这是 linq将采取的唯一方法。它采用与内存中可枚举源类似的方法,但您的来源并非如此。

db.MyTable代表一张桌子。通过它进行枚举,我们得到的SQL查询结果或多或少等同于:

SELECT * FROM MyTable

但是,db.MyTable.OrderByDescending(d => d.ID) 相当于调用它,然后将结果排序在内存中。因为查询在执行时会作为一个整体进行处理,所以我们实际上得到的SQL查询的结果或多或少像:

SELECT * FROM MyTable ORDER BY id DESC

最后,整个查询db.MyTable.OrderByDescending(d => d.ID).FirstOrDefault()会产生如下查询:

SELECT TOP 1 * FROM MyTable ORDER BY id DESC

或者

SELECT * FROM MyTable ORDER BY id DESC LIMIT 1

取决于您使用的数据库服务器类型。然后结果传递给相当于以下基于ADO.NET的代码的代码:

return dataReader.Read() ?
  new MyType{ID = dataReader.GetInt32(0), dataReader.GetInt32(1), dataReader.GetString(2)}//or similar
  : null;

你可以变得更好。

至于那个SQL查询。如果id列上有索引(因为它看起来像主键,肯定应该有),那么该索引将用于非常快速地找到有问题的行,而不是检查每一行。

总之,因为不同的linq提供商使用不同的方式来完成查询,所以他们都可以尽力以最好的方式这样做。当然,在一个不完美的世界里,我们毫无疑问会发现有些人比其他人更好。更重要的是,他们甚至可以为不同条件选择最佳方法。其中一个例子是,与数据库相关的提供程序可以选择不同的SQL来利用不同版本数据库的功能。另一个是在内存枚举中使用的Count()版本的实现有点像这样;

public static int Count<T>(this IEnumerable<T> source)
{
  var asCollT = source as ICollection<T>;
  if(asCollT != null)
    return asCollT.Count;
  var asColl = source as ICollection;
  if(asColl != null)
    return asColl.Count;
  int tally = 0;
  foreach(T item in source)
    ++tally;
  return tally;
}

这是一个更简单的案例(在我的例子中再次简化了一点,我显示的想法不是实际的代码),但它显示了代码的基本原理,当利用更高效的方法时它们是可用的 - 数组的O(1)长度和集合上的Count属性有时是O(1)而且它们不像我们在它是O(n)的情况 - 然后当它们无法恢复到效率较低但仍然有效的方法时。

所有这一切的结果是Linq在性能方面倾向于给予很好的支持。

现在,一个体面的编码器应该能够在大多数时间内匹配或击败任何给定情况的方法†,即使Linq提出了完美的方法,它本身也有一些开销。

然而,在整个项目的范围内,使用Linq意味着我们可以简明地创建与相对受限制的明确定义的实体相关的合理有效的代码(就数据库而言,通常每个表一个)。特别是,使用匿名函数和连接意味着我们获得非常好的查询。考虑:

var result = from a in db.Table1
  join b in db.Table2
  on a.relatedBs = b.id
  select new {a.id, b.name};

在这里,我们忽略了我们不关心的列,并且生成的SQL也会这样做。考虑如果我们使用手工编码的DAO类创建ab相关的对象,我们会怎么做:

  1. 创建一个新类来表示a的id和b名称的组合,以及运行我们生成实例所需查询的相关代码。< / LI>
  2. 运行查询以获取有关每个a和相关b的所有信息,并与垃圾一起生活。
  3. 运行查询以获取我们关注的每个ab的信息,并为其他字段设置默认值。
  4. 其中,备选方案2将是浪费,可能非常浪费。选项3将有点浪费并且非常容易出错(如果我们不小心尝试在代码中的其他地方使用未正确设置的字段会怎么样?)。只有选项1比linq方法产生的效率更高,但这仅仅是一种情况。在一个大项目中,这可能意味着产生数十个甚至数百或数千个略有不同的类(与编译器不同,我们不必发现他们实际上相同的情况)。因此,在实践中,linq在效率方面可以为我们带来一些好处。

    高效linq的良好政策是:

    1. 尽可能保持您开始使用的查询类型。每当您使用ToList()ToArray等将内容抓取到内存中时,请考虑您是否真的需要。除非你需要,否则你可以清楚地说明这样做的优势给你,不要。
    2. 如果您确实需要转移到内存处理,请AsEnumerable()优先于ToList()和其他方式,因此您一次只能抓取一个。
    3. 使用SQLProfiler或类似方法检查长时间运行的查询。在少数情况下,此处的政策1是错误的,并且使用AsEnumerable()移动到内存实际上更好(大多数情况与使用不在非分组字段中使用聚合的GroupBy相关,因此实际上并没有与它们对应的单个SQL查询。)
    4. 如果复杂的查询多次被点击,那么CompiledQuery可以提供帮助(因为它有自动优化功能可以覆盖其帮助的一些情况,因此4.5更少),但通常情况下更好将其从第一种方法中删除,然后仅在效率问题的热点中使用它。
    5. 你可以让EF运行任意SQL,但要避免使用它,除非它获得了很大的收益,因为过多的这样的代码会降低使用linq方法的一致可读性(我不得不说,我认为Linq2SQL胜过EF)在调用存储过程时甚至更多地调用UDF,但即便如此,这仍然适用 - 仅仅通过查看代码相互之间的相关性就不那么清楚了。)
    6. * AFAIK,此特定优化尚未应用,但我们此时正在讨论最佳实施方案,因此如果不是,则不重要,或仅在某些版本中。

      †我承认,虽然Linq2SQL经常会产生使用APPLY的查询,但我不习惯这样,因为我习惯于在2005年之前考虑如何在SQLServer版本中编写查询,而代码并没有。没有那种与旧习惯相符的人类倾向。它几乎教我如何使用APPLY。