【问题标题】:Find matching items by complex combination of attributes通过属性的复杂组合查找匹配项
【发布时间】:2019-10-21 00:50:42
【问题描述】:

在我的数据库中,我有一些项目,这些项目反映了用户填写的文档的属性。文档中给出的每个值,例如您为字段选择某个选项或选中一个复选框,将成为我表中的项目/属性。

这些属性可能是:吸烟者、非吸烟者、地区(欧洲、美国、...)、头发颜色

在表格中,大致如下所示:

Document
ID | Name
1  | doc-1
2  | doc-2
3  | doc-3

Attribute
ID | Name
1  | Smoker
2  | Non-Smoker
3  | Region-Europe
4  | Region-USA
5  | Hair-Brown
6  | Hair-Blond

Item
ID | Document | Attribute
1  | 1        | 1
2  | 1        | 4
3  | 2        | 2
4  | 2        | 3
5  | 2        | 5
6  | 3        | 2
7  | 3        | 6

为了提供搜索可能性,应允许用户构建通用查询。例如,我想查找具有以下属性的文档:

(Smoker AND Region-USA) OR (Non-Smoker AND Region-Europe AND Hair-Blond)

(将导致找到文档 #1)

我怎样才能以最有效的方式执行这样的查询,并可能使用 EF-core 和 linq-to-sql 将其推送到 SQL? 我如何才能以最有效的方式在计划 SQL 中实际查询?

我可以在内存中轻松完成,但由于我的数据库包含 100k+ 项,所以这可能很快就会变慢。

感谢您对此的任何帮助!


更新:关于 SO 的相关问题

【问题讨论】:

  • 我会说继续使用你在内存中工作的任何方法,看看它在实践中是否真的太慢了​​。你可能会发现它工作得很好。 “真正的问题是程序员在错误的地方和错误的时间花费了太多时间来担心效率;过早的优化是编程中万恶(或至少大部分)的根源。” ~唐纳德·高德纳
  • 您想知道如何回答列出的具体查询,还是需要在运行时处理一般查询?
  • @NetMage:我会稍微改一下这个问题。我应该能够处理一般查询,因为用户可以使用 UI 构建查询

标签: c# sql linq linq-to-sql entity-framework-core


【解决方案1】:

更多研究向我展示了我已经预料到的情况:可以使用使用 SQL IN 语句的解决方案,并且实际上可以很好地将查询工作分派到服务器,但在大量标签上可能效率不高。

幸运的是,用户不会定期执行非常复杂的查询,并且会接受复杂查询的一点等待时间,所以我可以忽略这一点。

链接这些语句的来源:

现在粗略地勾画出最终的解决方案,这里有一些代码:

通过使用 IN 语句,在子查询中,我可以过滤所有应用了某个属性的文档。通过使用 AND/OR 组合这些 IN 语句,我可以构建我想要的表达式。

SELECT i.Document
FROM   Item i INNER JOIN Attribute a on i.Attribute = a.ID
WHERE
    i.Document IN (
       SELECT ii.Document 
       FROM Item ii INNER JOIN Attribute ai on ii.Attribute = ai.ID
       WHERE ai.Name = "Smoker"
    )
    AND
    i.Document IN (
       SELECT ii.Document 
       FROM Item ii INNER JOIN Attribute ai on ii.Attribute = ai.ID
       WHERE ai.Name = "Region-USA"
    )
    OR
    i.Document IN (
       SELECT ii.Document 
       FROM Item ii INNER JOIN Attribute ai on ii.Attribute = ai.ID
       WHERE ai.Name = "Non-Smoker"
    )
    AND
    i.Document IN (
       SELECT ii.Document 
       FROM Item ii INNER JOIN Attribute ai on ii.Attribute = ai.ID
       WHERE ai.Name = "Region-Europe"
    )
    AND
    i.Document IN (
       SELECT ii.Document 
       FROM Item ii INNER JOIN Attribute ai on ii.Attribute = ai.ID
       WHERE ai.Name = "Hair-Blond"
    )

性能提升

为了可能限制子查询中所需的 JOIN 数量,可以先选择所需属性的 ID。

SELECT ID, Name FROM Attribute WHERE Name in ('Smoker', 'Non-Smoker', ...)

使用这些 ID,子查询看起来会更容易,因为我们可以跳过 JOIN:

SELECT i.Document
FROM   Item i INNER JOIN Attribute a on i.Attribute = a.ID
WHERE
    i.Document IN (SELECT ii.Document FROM Item ii WHERE ii.Attribute = 1) -- Smoker
    AND
    i.Document IN (SELECT ii.Document FROM Item ii WHERE ii.Attribute = 4) -- Region-USA
    OR
    ...

更新

两种方法的测量时间

我确实执行了与上述查询类似的查询:(1 AND 2) OR (3 AND 4 AND 4) 在 SQL Server 上,具有合理大小的一组文档 (130)、项目 (4122) 和属性 ( 〜400)。 在我的机器上可以测量以下时间:

  • 第一种方法,在 IN 的子查询中加入 JOIN:~ 12 秒
  • 第二种方法,首先查找属性的 ID:~3.5 秒

【讨论】:

    【解决方案2】:

    这是一个帮助构建查询的 LINQ 扩展类。我将解析表达式并构建正确的查询作为练习留给读者:)。

    首先,这是我们将要构建的基础:

    public class DocItemJoin {
        public Documents d { get; set; }
        public IEnumerable<int> ig { get; set; }
    }
    
    var DocItems = Document.GroupJoin(Item, d => d.ID, i => i.Document, (d, ig) => new DocItemJoin { d = d, ig = ig.Select(i => i.Attribute) });
    
    // (Smoker AND Region-USA) OR (Non-Smoker AND Region-Europe AND Hair-Blond)    
    var ans = DocItems.Where(dig => (dig.ig.Contains(1) && dig.ig.Contains(4)) || (dig.ig.Contains(2) && dig.ig.Contains(3) && dig.ig.Contains(6)))
                      .Select(dig => dig.d);
    

    DocItems为基础,我们可以使用Contains查询每个属性。

    使用扩展库,我们可以动态构建相同的查询:

    var whereLeft = 1.HasAttrib().qAnd(4.HasAttrib());
    var whereRight = 2.HasAttrib().qAnd(3.HasAttrib()).qAnd(6.HasAttrib());
    var whereBody = whereLeft.qOr(whereRight);
    var ans = DocItems.Query(whereBody);
    

    最后,这是构建Expression 树的扩展类:

    public static class QueryBuilder {
        private static MethodInfo containsMethod = typeof(Enumerable).GetMethods().Single(mi => mi.Name == "Contains" && mi.GetParameters().Length == 2).MakeGenericMethod(typeof(int));
    
        public static MethodCallExpression qContains(this Expression p, int attrib) => Expression.Call(containsMethod, p, Expression.Constant(attrib));
        public static BinaryExpression qAnd(this Expression l, Expression r) => Expression.AndAlso(l, r);
        public static BinaryExpression qOr(this Expression l, Expression r) => Expression.OrElse(l, r);
    
        static ParameterExpression digParm = Expression.Parameter(typeof(DocItemJoin), "dig");
        static MemberExpression digParmig = Expression.Property(digParm, "ig");
    
        public static MethodCallExpression HasAttrib(this int attrib) => digParmig.qContains(attrib);
    
        static Expression<Func<DocItemJoin, Documents>> selectLambda = Expression.Lambda<Func<DocItemJoin, Documents>>(Expression.Property(digParm, "d"), digParm);
    
        public static IQueryable<Documents> Query(this IQueryable<DocItemJoin> src, Expression whereBody)
            => src.Where(Expression.Lambda<Func<DocItemJoin, bool>>(whereBody, digParm)).Select(selectLambda);
    }
    

    【讨论】:

    • 感谢您提供的解决方案。我喜欢表达式的扩展。但是,我怀疑我是否可以将其降低到 SQL 级别
    • @HaraldKöstinger 我用 LINQ to SQL 测试了手动表达式,它可以很好地转换为 SQL 表达式吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-19
    • 1970-01-01
    • 2022-01-11
    • 2020-06-20
    • 2023-03-08
    • 1970-01-01
    相关资源
    最近更新 更多