【问题标题】:What construction can I use instead of Contains?我可以使用什么结构来代替包含?
【发布时间】:2015-01-27 08:44:24
【问题描述】:

我有一个带有 ID 的列表:

var myList = new List<int>();

我想从 db 中选择所有具有 myList id 的对象:

var objList= myContext.MyObjects.Where(t => myList.Contains(t.Id)).ToList();

但是当myList.Count &gt; 8000 我得到一个错误:

查询处理器用尽了内部资源,无法 生成查询计划。这是一个罕见的事件,只预计 极其复杂的查询或引用非常大的查询 表或分区的数量。请简化查询。如果你 相信您错误地收到了此消息,请联系客户 支持服务了解更多信息。

我认为这是因为我使用了Contains()。我可以用什么来代替 Contains?

【问题讨论】:

  • 您可以尝试避免ToList 和结尾并在需要时产生结果吗?
  • @Vignesh.N - 结果仍然需要构建查询计划,所以我认为这不会改变结果。
  • 关于这件事有一个open bug on connect
  • 您可以将列表拆分为具有较少元素的几个子列表并运行单独的查询。
  • myList 从何而来?如果它以某种方式来自数据库,您应该可以加入它。

标签: c# linq sql-server-2008-r2


【解决方案1】:

您可以在客户端执行查询,方法是添加AsEnumerable() 以“隐藏”实体框架中的Where 子句:

var objList = myContext
  .MyObjects
  .AsEnumerable()
  .Where(t => myList.Contains(t.Id))
  .ToList();

为了提高性能,您可以将列表替换为HashSet

var myHashSet = new HashSet<int>(myList);

然后相应地修改Where中的谓词:

  .Where(t => myHashSet.Contains(t.Id))

就实施时间而言,这是“简单”的解决方案。但是,由于查询是在客户端运行的,因此性能可能会很差,因为所有 MyObjects 行在被过滤之前都会被拉到客户端。

您收到错误的原因是因为 Entity Framework 将您的查询转换为如下内容:

SELECT ...
FROM ...
WHERE column IN (ID1, ID2, ... , ID8000)

因此,列表中的所有 8000 个 ID 基本上都包含在生成的 SQL 中,超出了 SQL Server 可以处理的限制。

生成此 SQL 的实体框架“寻找”的是 ICollection&lt;T&gt;,它由 List&lt;T&gt;HashSet&lt;T&gt; 实现,因此如果您尝试将查询保留在服务器端,使用 @ 不会提高性能987654333@。但是,在客户端,情况有所不同,ContainsO(1) 代表 HashSet&lt;T&gt;O(N) 代表 List&lt;T&gt;

【讨论】:

  • @KliverMax:你添加了AsEnumerable()吗?
  • 抱歉忘记AsEnumerable()。让您的解决方案有效。
  • 很多时候这实际上是最高效的方式。
  • @Magnus:是的,除非MyObjects 返回数百万行。然后你必须使用更复杂的服务器端查询。
  • 假设该表包含 800 万行,并非不合理。假设所需的“id”之一是数据库返回的最后一个。 99.99% 的返回数据将被丢弃。我还断言,通过网络传输选定的然后丢弃的数据所花费的时间将大大超过任何其他性能节省。即使您只选择 ID,也就是浪费了 31MB 的传输来获取 32Kb 的数据。
【解决方案2】:

如果您不希望它表现良好,我建议您使用表值参数和存储过程。

在您的数据库中,使用 TSQL,

CREATE TYPE [dbo].[IdSet] AS TABLE
(
    [Id] INT
);
GO

CREATE PROCEDURE [dbo].[Get<table>]
    @ids [dbo].[IdSet] READONLY
AS
    SET NOCOUNT ON;

    SELECT
                <Column List>
        FROM
                [dbo].[<table>] [T]
        WHERE
                [T].[Id] IN (SELECT [Id] FROM @ids);
RETURN 0;
GO

然后,在 C# 中

var ids = new DataTable()
ids.Columns.Add("Id", typeof(int));

foreach (var id in myList)
{
    ids.Rows.Add(id);
}

var objList = myContext.SqlQuery<<entity>>(
    "[dbo].[Get<table>] @ids",
    new SqlParameter("@ids", SqDbType.Structured)
        { 
            Value = ids,
            TypeName = "[dbo].[IdSet]"
        }));

【讨论】:

    【解决方案3】:

    您可以将列表拆分为多个子列表,并运行单独的查询:

    int start = 0;
    int count = 0;
    const int chunk_size = 1000;
    do {
        count = Math.Min(chunk_size, myList.Count - start);
        var tmpList = myList.GetRange(start, count);
        // run query with tmpList
        var objList= myContext.MyObjects.Where(t => tmpList.Contains(t.Id)).ToList();
        // do something with results...
        start += count;
    } while (start < myList.Count);
    

    当然,您需要以某种适合您的方式找出合适的“块大小”。根据表格和列表的大小,加载整个表格并在代码中过滤可能更方便,如其他答案中所建议的那样。

    【讨论】:

    • 只是对这种方法的一些其他想法。 int 列表可以在分块之前排序,where 子句也可以包括 id 的范围。也许这会提供一点优化?
    • 或者var objList = myList.Select((i, x) =&gt; new { I = i, X = x}).GroupBy(o =&gt; o.X / 1000).SelectMany(g =&gt; myContext.Where(t =&gt; g.Select(o =&gt; o.I).Contains(t.Id)).ToList(); 在单个语句中完成。
    【解决方案4】:

    您可以创建一个代表myList 的临时数据库表,并使用该临时列表将您的查询重构为JOIN

    错误的原因是产生的实际查询包含myList的所有元素。

    基本上,数据库(查询处理器)需要查看两个列表来进行过滤。如果第二个列表太大而无法放入查询中,则必须以其他方式提供它(例如作为临时表)

    【讨论】:

    • 这听起来太过分了,而且是硬编码的。如果 id 是动态的,并且每次的集合都不同怎么办?
    • 是的,这就是重点:OP 必须为 每个 查询创建这个临时表
    • 这似乎不是一个可行的解决方案。
    • 您可以在表中添加一个 query-id 列,这样您就可以将该表用于每个查询。在我看来,当您拥有大量 id 时,唯一真正可行的解决方案。
    • @TravisJ 将 8000 小行添加到表中通常需要多长时间?不太好。
    【解决方案5】:

    为什么不试试

    var objList= from obj in myContext.MyObjects
         join myId in myList on obj.Id equals myId
         select obj;
    

    【讨论】:

      【解决方案6】:

      如果您的 ID 列表来自 db,那么不要使用 List 保留它 IQueryable 因为这不会形成带有数千个参数的 IN 子句的 sql 查询。 IN 子句现在将具有子查询。我很惊讶没有人提到它。

      IQueryable< int > myList = myContext.Obj1.Where(...).Select(x => x.Id);
      var objList = myContext.MyObjects.Where(t => myList.Contains(t.Id)).ToList();
      

      如果不是这样,那么,我会建议创建一个扩展方法来分块处理它。您可以定义自己的块大小。

      类似这样的:

       public static class QueryableExtensions
          {
              public static List<T1> WhereContains<T1, T2>(this IQueryable<T1> set, List<T2> values, string property)
              {
                  int chunkSize = 5000;
                  int currentChunk = 1;
                  List<T1> results = new List<T1>();
                  int valuesLeft = values.Count;
                  while (valuesLeft > 0)
                  {
                      List<T2> currentValues = values.Skip((currentChunk - 1) * chunkSize).Take(chunkSize).ToList();
                      results.AddRange(set.Where($"@0.Contains(outerIt.{property})", new object[] { currentValues }).ToList());
                      valuesLeft -= chunkSize;
                      currentChunk++;
                  }
      
                  return results;
              }
          }
      

      希望对你有帮助!

      【讨论】:

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