【问题标题】:Join to an in-memory list efficiently有效地加入内存列表
【发布时间】:2013-09-11 16:29:30
【问题描述】:

在 EF 中,如果我有一个基元列表 (List),则可以很容易地将其“加入”到一个表中:

var ids = int[]{1,4,6}; //some random values
var rows = context.SomeTable.Where(r => ids.Contains(r.id))

如果您想在多个列上加入,这会变得更加复杂:

var keys = something.Select(s => new { s.Field1, s.Field2 })
var rows = context.SomeTable.Where(r => keys.Contains(r => new { s.Field1, s.Field2 })); // this won't work

我找到了两种加入方式,但都不是很好:

  1. 吸入整个表,并根据其他数据对其进行过滤。 (如果表真的很大,这会变慢)
  2. 对于每个键,查询表(如果要提取的行数很多,这会变慢)

有时,我能够做出的妥协是修改 #1:基于一个相当独特的键拉入表的子集

var keys = something.Select(s => s.Field1)
var rows = context.SomeTable.Where(r => keys.Contains(s.Field1)).ToList();
foreach (var sRow in something)
{
    var joinResult = rows.Where(r => r.Field1 == sRow.Field1 && r.Field2 == sRow.Field2);
    //do stuff
}

但即使这样也可能会拉回太多数据。

我知道有一些方法可以将表值参数引入 ADO.Net,并且我可以构建一系列 OR'd 在一起的 .Where() 子句。谁有灵丹妙药?

【问题讨论】:

  • 为了性能,使用context.ExecuteQuery()发送定制的SQL语句。

标签: c# sql performance linq entity-framework


【解决方案1】:

代替 .Contains(),您如何使用内部连接和“过滤器”:

from s in context.SomeTable
join k in keys on new {k.Field1, k.Field2} equals new {s.Field1, s.Field2}

上面可能有错别字,但你明白了......

【讨论】:

  • 除非我记错了,否则这用于生成没有 where 子句的“SELECT ... FROM SomeTable”。当我在该表中有 10+ 百万行时,这太糟糕了。
【解决方案2】:

我遇到了完全相同的问题,我想出的解决方案是:

  • 天真:对每个本地记录进行单独的查询
  • 更智能:创建 2 个唯一的 Filed1 值和唯一的 Fiels2 值列表,使用 2 个包含表达式进行查询,然后您必须对结果进行双重过滤,因为它们可能不那么准确。

看起来像这样:

 var unique1 = something.Select(x => x.Field1).Distinct().ToList();
 var unique2 = something.Select(x => x.Field2).Distinct().ToList();
 var priceData = rows.Where(x => unique1.Contains(x.Field1) && unique2.Contains(x.Field2));
  • 下一个是我自己的解决方案,我称之为 BulkSelect,其背后的想法是这样的:

    • 使用直接 SQL 命令创建临时表
    • 将 SELECT 命令的数据上传到该临时表
    • 拦截并修改EF生成的SQL。

我是为 Postgres 做的,但这可能需要移植到 MSSQL。这很好地描述了here,源代码是here

【讨论】:

    【解决方案3】:

    您可以尝试扁平化您的键,然后使用相同的Contains 模式。尽管您可以使用函数索引将展平键存储在数据库中,但这可能不会在大型查询中表现出色...

    我有表格测试,列K1 int, K2 int, Name varchar(50)

    var l = new List<Tuple<int, int>>();
    l.Add(new Tuple<int, int>(1, 1));
    l.Add(new Tuple<int, int>(1, 2));
    var s = l.Select(k => k.Item1.ToString() + "," + k.Item2.ToString());
    var q = Tests.Where(t => s.Contains(t.K1.ToString() + "," + t.K2.ToString()));
    foreach (var y in q) {
        Console.WriteLine(y.Name);
    }
    

    我已经在 LinqPad 中使用 Linq to SQL 对此进行了测试

    第一次尝试失败:

    我认为将其编写为单个查询的方式是这样的

    var keys = something.Select(s => new { s.Field1, s.Field2 })
    var rows = context.SomeTable.Where(r => keys.Any(k => r.Field1 == k.Field1 && r.Field2 == k.Field2));
    

    很遗憾,我在这台笔记本电脑上没有 EF,甚至无法测试这在语法上是否正确。

    我也不知道它的性能如何……

    【讨论】:

    • 我认为它不会起作用,因为keys 是对象的集合(在此示例中为匿名),而不是原始类型(int、string、Guid、DateTime 等)的集合。它很可能会抛出臭名昭著的“无法创建 xyz 类型的常量值。仅支持原始类型...”异常。
    • @Slauma 只需在 Linqpad 中运行一个更简单的示例,我很确定您是对的。
    【解决方案4】:
    var rows = 
    from key in keys
    join thingy in context.SomeTable
    on 1 = 1
    where thingy.Field1 == key && thingy.Field2 == key
    select thingy
    

    应该可以工作,并生成合理的 SQL

    【讨论】:

    • 我没有尝试“on 1 = 1”,但我认为它会在那时加载整个 SomeTable
    • 查询生成器将整个查询考虑在内,包括 where 子句,并且仅在必要时(即枚举结果时)才进入数据存储区,甚至可能更晚,在一堆其他查询表达式之后。我会说试一试,看看它会把你带到哪里。如果生成的 SQL 很糟糕,您可以随时决定反对它。
    • 在 JOIN 的左侧设置内存数据不是一个好习惯。您应该颠倒顺序并始终将 DbSet 数据放在左侧,将内存数据放在右侧。见this
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-13
    • 2020-07-20
    相关资源
    最近更新 更多