我可以使用什么构造来代替 Contains?

What construction can I use instead of Contains?

我有一个包含 ID 的列表:

var myList = new List<int>();

我想 select 来自 db 且 ID 来自 myList 的所有对象:

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

但是当 myList.Count > 8000 我得到一个错误:

The query processor ran out of internal resources and could not produce a query plan. This is a rare event and only expected for extremely complex queries or queries that reference a very large number of tables or partitions. Please simplify the query. If you believe you have received this message in error, contact Customer Support Services for more information.

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

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

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

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

您可以通过将 AsEnumerable() 添加到 "hide" Entity Framework 中的 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))

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

你得到错误的原因是因为 Entity Framework 将你的查询转换成这样:

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

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

生成这个 SQL 的 Entity Framework "looks for" 是由 List<T>HashSet<T> 实现的 ICollection<T> 所以如果你尝试要将查询保留在服务器端,使用 HashSet<T> 不会提高性能。但是,在客户端,情况有所不同,其中 Contains 对于 HashSet<T>O(1),对于 List<T>O(N)

您可以将列表分成几个子列表,运行 单独查询:

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

当然,您需要以适合自己的方式找到好的 "chunk size"。根据 table 和列表的大小,加载整个 table 并在代码中过滤可能更方便,如其他答案中所建议的那样。

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

在您的数据库中,使用 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]"
        }));

为什么不试试

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

如果您的 ID 列表来自 db,那么不要使用 List,而是保留它 IQueryable,因为这不会形成 sql 带有具有数千个参数的 IN 子句的查询。 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;
        }
    }

希望对您有所帮助!