9

我正在寻找通过表的 ID 列检索表的最后一行。我目前使用的作品:

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

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

4

1 回答 1

5

我看不到这个查询通过整个表。

ID 列上没有索引吗?

您能否将分析查询的结果添加到您的问题中,因为这不是应该的样子。

以及分析结果,产生的SQL。select top 1 * from MyTable order by id desc除了显式的列名和一些别名之外,我看不出它会是什么。id除了对该索引的扫描之外,也没有关于它如何的索引。

编辑:承诺的解释。

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

这使我们能够表达类似的任务,而不管底层来源如何。例如,您的查询包括源,但我们可以做一个更一般的形式:

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 类相关的对象,我们会做a什么b

  1. 创建一个新类来表示a' id 和b' 名称的组合,以及运行我们需要生成实例的查询的相关代码。
  2. 运行查询以获取有关每个a和相关的所有信息b,并与废物一起生活。
  3. 运行查询以获取我们关心的每个字段的信息,并为其他字段设置默认值ab

其中,选项 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 在调用存储过程甚至调用 UDF 更是如此,但即使在那里这仍然适用 - 仅查看代码之间的关系就不太清楚了)。

*AFAIK, this particular optimisation isn't applied, but we're talking about the best possible implementation at this point, so it doesn't matter if it is, isn't, or is in some versions only.

†I'll admit though that Linq2SQL would often produce queries that use APPLY that I would not think of, as I was used to thinking of how to write queries in versions of SQLServer before 2005 introduced it, while code doesn't have those sort of human tendencies to go with old habits. It pretty much taught me how to use APPLY.

于 2012-09-02T01:29:23.593 回答