SQL-组合搜索查询,而不是多个数据库行程

时间:2019-01-19 20:55:38

标签: sql asp.net entity-framework linq entity-framework-core

搜索词来自UI,用于搜索表的实体。这些搜索结果应在用户界面中显示的顺序如下:

  • 首先:完全匹配
  • 第二:以该词开头
  • 第三:包含该词的单词
  • 第四:以该词结尾
  • 第五:无论如何都包含该词

所以我首先从数据库获得了实体:

result = entities.Where(e => e.Name.Contains(searchTerm)).ToList();

然后我在内存中重新排列它们:

var sortedEntities = result.Where(e => e.Name.ToLower() == searchTerm.ToLower())
    .Union(result.Where(e => e.Name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)))
    .Union(result.Where(e => e.Name.Contains($" {searchTerm} ")))
    .Union(result.Where(e => e.Name.EndsWith(searchTerm, StringComparison.OrdinalIgnoreCase)))
    .Union(result.Where(e => e.Name.Contains(searchTerm)));

在添加分页之前,它一直运行良好。现在,如果第2页上有完全匹配项(来自DB的数据),则不会首先显示。

我能想到的唯一解决方案是分离请求(在这种情况下为5个请求)并手动跟踪页面大小。我的问题是,有没有一种方法可以告诉DB遵守该顺序,并在一次DB旅行中获得排序的数据?

1 个答案:

答案 0 :(得分:1)

我花了一些时间才意识到您使用Union来尝试通过“匹配强度”对数据进行排序:首先是完全匹配的数据,然后是大小写不同的数据,等等。当我看到带有谓词的Union,我的巴甫洛夫条件思维将其转换为OR。我不得不从thinking fast to slow切换。

因此,问题在于没有可预测的排序。毫无疑问,链接的Union语句确实会产生确定的最终排序顺序,但这不一定是Union的顺序,因为每个Union还会执行一个隐式的Distinct 。一般规则是,如果要特定的排序顺序,请使用OrderBy方法。

话虽如此,然后...

var result = entities
    .Where(e => e.Name.Contains(searchTerm))
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize).ToList();

...期望的结果似乎可通过以下方式获得:

var sortedEntities = result
    .OrderByDescending(e => e.Name == searchTerm)
    .ThenByDescending(e => e.Name.ToLower() == searchTerm.ToLower())
    .ThenByDescending(e => e.Name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase))
    ... etc.

(下降,因为false命令before为真)

但是,如果匹配项多于pageSize,则排序将为时已晚。如果pageSize = 20和项目21是第一个完全匹配项,则该项目将不在页面1上。这意味着:应在 分页之前进行排序。

第一步是从第一条语句中删除.ToList()。如果删除它,则第一条语句是IQueryable表达式,并且Entity Framework能够将完整的语句组合为一个SQL语句。下一步是将Skip/Take移到完整语句的末尾,它也将成为SQL的一部分。

var result = entities.Where(e => e.Name.Contains(searchTerm));

var sortedEntities = result
    .OrderByDescending(e => e.Name == searchTerm)
    .ThenByDescending(e => e.Name.ToLower() == searchTerm.ToLower())
    .ThenByDescending(e => e.Name.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase))
    ... etc
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize).ToList();

但是现在出现了一个新问题。

由于不支持与StringComparison.OrdinalIgnoreCase进行字符串比较,因此对于部分语句,Entity Framework将自动切换到client-side evaluation。所有过滤的结果将从数据库中返回,但是大多数排序和所有分页都将在内存中完成。

当滤镜很窄时,这可能不是很糟糕,但当滤镜很宽时,这是非常糟糕的。因此,最终,要做到这一点,您必须删除StringComparison.OrdinalIgnoreCase并以稍差的匹配强度来解决。带我们去

最终结果

var result = entities.Where(e => e.Name.Contains(searchTerm));
var sortedEntities = result
    .OrderByDescending(e => e.Name == searchTerm)
    .ThenByDescending(e => e.Name.StartsWith(searchTerm))
    .ThenByDescending(e => e.Name.Contains($" {searchTerm} "))
    .ThenByDescending(e => e.Name.EndsWith(searchTerm))
    .ThenByDescending(e => e.Name.Contains(searchTerm))
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize).ToList();

为什么“不够精致”?因为根据您的评论,数据库排序规则不区分大小写,所以SQL无法在不添加COLLATE语句的情况下,按大小写区分完全匹配。这是我们用LINQ无法做到的。