通过属性的复杂组合查找匹配项

时间:2019-06-05 13:40:25

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

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

此类属性可能是:吸烟者,不吸烟者,地区(欧洲,美国,...),头发颜色

在表中,这大致如下:

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中进行查询?

我可以很容易地在内存中完成此操作,但是由于我的数据库包含超过10万个项目,因此很快就会变慢。

谢谢您的帮助!


更新:关于SO的相关问题

2 个答案:

答案 0 :(得分:0)

更多研究向我展示了我已经期望的结果:可以使用使用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
    ...

更新

两种方法的测量时间

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

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

答案 1 :(得分:0)

这是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);
}