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

时间:2015-01-27 08:44:24

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

我有一个包含ID的列表:

var myList = new List<int>();

我想从db中选择来自myList的所有对象:

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

但是当myList.Count > 8000我收到错误时:

  

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

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

6 个答案:

答案 0 :(得分:14)

您可以在客户端执行查询,方法是添加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行都会在过滤之前被提取到客户端。

您收到错误的原因是因为实体框架会将您的查询转换为以下内容:

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

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

生成此SQL的实体框架“查找”是ICollection<T>List<T>HashSet<T>实现的,因此如果您尝试在服务器端保留查询,则会得到使用HashSet<T>没有改善性能。但是,在客户端,Contains O(1) HashSet<T> O(N)List<T> {{1}}的情况不同。

答案 1 :(得分:8)

如果您想要表现良好,我建议您使用表值参数和存储过程。

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

答案 2 :(得分:5)

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

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

基本上,DB(查询处理器)需要查看两个列表才能进行过滤。如果第二个列表太大而不适合查询,则必须另外提供(例如作为临时表)

答案 3 :(得分:5)

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

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

当然,你需要以适合自己的某种方式找出好的“块大小”。根据表和列表的大小,在代码中加载整个表和过滤器可能更方便,如其他答案所示。

答案 4 :(得分:0)

为什么不试试

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

答案 5 :(得分:0)

如果您的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;
        }
    }

希望有帮助!