【问题标题】:Linq .Contains with large set causes TDS errorLinq .Contains with large set 导致 TDS 错误
【发布时间】:2010-11-03 17:17:42
【问题描述】:

我有点过于简化了,因为我正在寻找一个通用的答案。假设我有一个这样的表设置:

Parent
recno    int (unique, pk)
date     datetime
stuff    varchar(50)

Child
parentrecno (int, fk)   --- PK
sequence    (int)       --- PK
data     varchar(50)

在我的 C# 程序中,我经历了很多麻烦来找到我感兴趣的 Parent 记录并将它们填充到一个列表中。 Parent 是一个非常大的表,我宁愿不要对它进行不必要的查询。所以我把钥匙藏起来了:

List<int> recs = (from d in Parent where [.....] select d.recno).ToList();

稍后在 Linq 中,我可以说,找到关联父母的所有子记录:

var kids = from k in database.Childs
      where recs.Contains(k.parentrecno)
      select new { k };

这一切都很好,直到 recs 包含超过 2100 个条目。然后我得到一个 TDS RPC 错误(参数太多)。

我认为我可以:

  • 直接用 SQL 完成整个事情(真的不想用 DataReader 等麻烦来做)。有一个外部系统参与了对记录的限定,所以我也不知道这是否完全可能。另外,我会生成该列表两次——一次是在我需要在 .Contains() 中使用它时,另一次用于其他目的。

  • 分解列表(recs),然后分块读取 Child。

如果我把它分成几块,那么我漂亮的 Linq 再往下一点就完全不能工作了:

var kids2 = (from kid in paydb.Childs
         where
             recs.Contains(kid.parentrecno)
         group pay by kid.parentrecno into kgroup
         select new { ParentRecNo = kgroup.Key, KidRecords = kgroup })
              .ToDictionary(kx => kx.ParentRecNo);

因为 List recs 将包含需要组合在一起的内容,但必须为 Linq 查询分开。

【问题讨论】:

    标签: linq tsql


    【解决方案1】:

    我们使用一个 SQL 函数,将一个 varchar(max) 分隔的值列表作为参数并返回一个表变量。 linq 看起来像这样:

    from a in Context.SomeTable    
    join nl in Context.ParseDelimited(nodeIdList) on a.NodeId.ToString() equals nl.FieldValue
    

    其中 nodeIdList 是一个字符串变量,其中包含来自先前查询的 id 列表。丑陋,但它确实绕过了 2100 参数限制。

    create function dbo.ParseDelimited(@delimitedList NVARCHAR(MAX)) returns @tblSample table(counter int, fieldValue NVARCHAR(100)) 
    WITH SCHEMABINDING
    as
    begin
      declare @counter    NVARCHAR(  4)
      declare @fieldValue NVARCHAR(100)
    
      declare @tmpTable table(counter int primary key, fieldValue NVARCHAR(100))
    
      set @counter = 1
    
      while charindex(',', @delimitedList) > 0
      begin
        set @fieldValue = ltrim(rtrim(substring(@delimitedList, 1, charIndex(',', @delimitedList)-1)))
    
        insert into @tmpTable select @counter, @fieldValue
    
        set @delimitedList = ltrim(rtrim(substring(@delimitedList, (charindex(',', @delimitedList) + 1), len(@delimitedList))))
    
        set @counter = @counter + 1
      end
    
      if ltrim(rtrim(@delimitedList)) != ''
      begin
        insert into @tmpTable select @counter, @delimitedList
      end
    
      insert into @tblSample select counter, fieldValue from @tmpTable
    
      return
    end
    

    【讨论】:

    • 约翰,您能否提供一些您的解决方案示例。
    【解决方案2】:

    这看起来像是 Linq .Join() 的工作。我使用下面的对象而不是数据库作为数据源,但如果您的 LinqToSql 提供程序支持,则概念是相同的。

    List<int> recs = new List<int>(Enumerable.Range(1, 2500));
    List<Child> children = new List<Child>(Enumerable.Range(2000, 1000)
        .Select(x => new Child
        {
            ParentRecNo = x
        }));
    
    var kids = children.Join(recs, x => x.ParentRecNo, y => y, (x, y) => x);
    
    Console.WriteLine(kids.First().ParentRecNo);
    Console.WriteLine(kids.Last().ParentRecNo);
    

    输出:

    2000
    2500
    

    您可以在字典创建代码中使用相同的 Join,如下所示:

    var kids2 = children
        .Join(recs, x => x.ParentRecNo, y => y, (x, y) => x)
        .GroupBy(x => x.ParentRecNo)
        .Select(kgroup => new
            {
                ParentRecNo = kgroup.Key,
                KidRecords = kgroup
            })
        .ToDictionary(kx => kx.ParentRecNo);
    

    【讨论】:

    • 不错的解决方案。我唯一担心的是当数据库中可能的记录数量非常多时。数据是被检索然后加入——在检索后消除不需要的记录,还是在 SQL Server 端消除它们然后将结果拉过来?
    • 这不起作用。您尝试在 SQL 查询中使用客户端 IEnumerable (recs)(仅支持包含,它被转换为 IN 表达式)。问问自己 LINQtoSQL 应该如何通知数据库 recs 的内容。唯一可行的方法是首先将整个 Childs 表的内容发送到客户端,然后使用 recs 加入。
    • 仅获取 ChildId 和 ParentRecordId 以在客户端进行连接可能会更便宜。随后查询匹配记录的子详细信息和/或将其分解为小于 2100 的块,可能使用 InSetsOf 实现 stackoverflow.com/questions/1034429/… 。最后,出现 SQL Server 2008,如果这是一个选项 Clint,则添加了 Table-Valued Parameters msdn.microsoft.com/en-us/library/bb675163.aspx 作为绕过 2100 参数限制的方法。
    【解决方案3】:

    除了使用 SQL 和 DataReader,您还可以编写两个存储过程并通过 LINQ 使用它们。甚至可以通过 LINQ 读取 id 列表并将其作为输入提供给您的存储过程。

    您可能无法解决参数过多的问题(您是否有权访问数据库)并且块解决方案不是很好,并且由于第二个查询而不能解决整个问题。

    编辑:由于recs 集合并非完全由数据库生成,因此您需要某种方式告诉数据库该集合的内容。我认为您最好的选择是使用一个(或两个)存储过程,将集合作为一个大的逗号分隔字符串接受。在存储过程中,您再次将字符串拆分为 id。

    Somelinks 解释了如何编写和使用拆分字符串函数。

    顺便说一句,如果您使用的是 SQL Server 2008,有一种比字符串解析更好的方法:table-valued parameters。您可以将表作为参数传递给您的存储过程。

    【讨论】:

    • 我最大的问题是,recs 中的记录的限定并非全部在 SQL 中完成——还涉及另一个系统。能够将这些键铲到临时表中也可以,但我认为我也无法从 Linq 访问该临时表。
    • 我用一些关于如何解决问题的附加信息更新了我的答案。
    【解决方案4】:

    我想这就是你要找的东西:

    List<Child> children = 
    database.Parents.Where( 'your favourite lambda expression').Select(x=>x.Childs).ToList();
    

    所以...我不知道您使用什么条件来获取父母,但希望可以使用 lambda 表达式来完成,例如:

    List<Child> children = database.Parents
         .Where(p=>p.recno > 10 && p.recno < 40)
         .Select(x=>x.Childs).ToList();
    

    【讨论】:

      猜你喜欢
      • 2018-06-28
      • 1970-01-01
      • 1970-01-01
      • 2011-09-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多