如何在动态搜索查询中使用关系划分来过滤结果?

How to filter results using relational division in a dynamic search query?

使用以下查询,我将根据所选 tagscategories:

筛选结果
DECLARE @categories NVARCHAR(MAX),
        @tags NVARCHAR(MAX);

SELECT @categories = '1,2,4',  -- comma separated category ids
       @tags = '2,3'           -- comma separated tag ids

SELECT p.id,
       p.title,
       p.price
FROM tbl_products p
  LEFT JOIN tbl_product_categories pc ON @categories IS NOT NULL AND pc.product_FK = p.id
  LEFT JOIN tbl_product_tags pt ON @tags IS NOT NULL AND pt.product_FK = p.id
WHERE ( p.price >= @min_price OR @min_price IS NULL )
  AND ( p.price <= @max_price OR @max_price IS NULL )
  AND ( pc.category_FK IN (SELECT value FROM STRING_SPLIT(@categories, ',')) OR @categories IS NULL )
  AND ( pt.tag_FK IN (SELECT value FROM STRING_SPLIT(@tags, ',')) OR @tags IS NULL)
GROUP BY p.id
HAVING COUNT(p.id) = ( (SELECT COUNT(*) FROM STRING_SPLIT(@categories, ',')) + (SELECT COUNT(*) FROM STRING_SPLIT(@tags, ',')) )

但是并没有产生预期的效果!我怀疑 HAVING 部分没有正确使用,因为它不会每次都根据传递的标签和类别 ID 产生正确的计数。

有谁知道我们如何实现这种情况,应用关系除法来提取所有这些共同通过 @categories@tags 的产品???还有更好的办法吗?

-- 更新: 例如使用以下示例日期:

tbl_products:
id  title     price
===================
1   mouse       10
2   keyboard    18
3   iphone 8    100
4   note 8      90

tbl_product_categories:
product_FK category_FK
======================
1           1
2           1
3           2
4           2

tbl_product_tags:
product_FK tag_FK
=================
1           1
3           1
3           2
4           2

所以如果我们通过 @categories = '2'@tags = '1,2'min_price = 50 那么我们应该得到 iphone 8

根据您的示例数据,我认为您加入的是错误的列 tag_FK 而不是 product_FK,因此 table [=16= 上的 LEFT JOIN ] 应该是:

LEFT JOIN tbl_product_tags pt ON @tags IS NOT NULL AND pt.product_FK = p.id

此外,我认为您的查询中没有必要使用 HAVING 语句,在我看来您是将其用作额外的检查;因为您的查询已经在进行过滤工作。但是,HAVING语句后的条件不正确,最好的例子就是你的例子本身:

1. count of p.Id  = 1 (p.Id = 3 ... iPhone 8)
2. count of categories = 1 (category: 2)
3. count of tags = 2  (tags: 1, 2)

那么在这种情况下 p.Id 的计数不等于传递的类别和标签的计数。

更新 :基于@dtNiro,查询应如下所示:

DECLARE @categories NVARCHAR(MAX),
        @tags NVARCHAR(MAX);

SELECT @categories = '1,2,4',  -- comma separated category ids
       @tags = '2,3'           -- comma separated tag ids

SELECT p.id,
       p.title,
       p.price
FROM tbl_products p
  LEFT JOIN tbl_product_categories pc ON @categories IS NOT NULL AND pc.product_FK = p.id
  LEFT JOIN tbl_product_tags pt ON @tags IS NOT NULL AND pt.product_FK = p.id
WHERE ( p.price >= @min_price OR @min_price IS NULL )
  AND ( p.price <= @max_price OR @max_price IS NULL )
  AND ( pc.category_FK IN (SELECT value FROM STRING_SPLIT(@categories, ',')) OR @categories IS NULL )
  AND ( pt.tag_FK IN (SELECT value FROM STRING_SPLIT(@tags, ',')) OR @tags IS NULL)
GROUP BY p.id
HAVING (@tags IS NULL OR (COUNT(p.id) = (SELECT COUNT(*) FROM STRING_SPLIT(@tags, ','))))

您可以使用 count(distinct [tags|categories]) 等于各个参数的计数,而不是尝试从您的变量中添加计数:

declare @categories nvarchar(max), @tags nvarchar(max), @min_price int, @max_price int;
select 
    @categories = '2'  -- comma separated category ids
  , @tags = '1,2'      -- comma separated tag ids
  , @min_price = 0
  , @max_price = power(2,30)

select
    p.id
  , p.title
  , p.price
from tbl_products p
  left join tbl_product_categories pc 
    on @categories is not null and pc.product_fk = p.id
  left join tbl_product_tags pt 
    on @tags is not null and pt.product_fk = p.id
where ( p.price >= @min_price or @min_price is null )
  and ( p.price <= @max_price or @max_price is null )
  and ( pc.category_fk in (select value from string_split(@categories, ',')) or @categories is null )
  and ( pt.tag_fk in (select value from string_split(@tags, ',')) or @tags is null)
group by p.id, p.title, p.price
having (count(distinct pc.category_fk) = (select count(*) from string_split(@categories, ',')) or @categories is null) 
   and (count(distinct pt.tag_fk) = (select count(*) from string_split(@tags, ',')) or @tags is null)

演示:dbfiddle.uk demo

returns:

+----+----------+-------+
| id |  title   | price |
+----+----------+-------+
|  3 | iphone 8 |   100 |
+----+----------+-------+

在性能方面,将其重写为具有动态 sql 执行或至少 option (recompile) 的过程将受益,如这些参考资料所示:


这是一个动态 sql 搜索过程的示例,它使用 exists ...having count()... 而不是 left join... where... having count(distinct ...) 来简化计划 (plan comparison demo):

create procedure product_search (
    @categories nvarchar(max)
  , @tags nvarchar(max)
  , @min_price int
  , @max_price int
) as 
begin;
set nocount, xact_abort on;
declare @sql nvarchar(max);
declare @params nvarchar(256);
set @params = '@categories nvarchar(max), @tags nvarchar(max), @min_price int, @max_price int';
set @sql = ';
select
    p.id
  , p.title
  , p.price
from tbl_products p
where 1=1'
if @min_price is not null
set @sql = @sql + '
  and p.price >= @min_price';
if @max_price is not null
set @sql = @sql + '
  and p.price <= @max_price';
if @categories is not null 
set @sql = @sql + '
  and exists (
      select 1 
      from tbl_product_categories ic
      where ic.product_fk = p.id
        and ic.category_fk in (select value from string_split(@categories, '',''))
      having count(*) = (select count(*) from string_split(@categories, '',''))
      )';
if @tags is not null 
set @sql = @sql + '
  and exists (
      select 1 
      from tbl_product_tags it
      where it.product_fk = p.id
        and it.tag_fk in (select value from string_split(@tags, '',''))
      having count(*) = (select count(*) from string_split(@tags, '',''))
      )';

exec sp_executesql @sql, @params, @categories, @tags, @min_price, @max_price;
end;

像这样执行:

declare @categories nvarchar(max), @tags nvarchar(max), @min_price int, @max_price int;
select 
    @categories = null  -- comma separated category ids
  , @tags = '1,2'      -- comma separated tag ids
  , @min_price = null
  , @max_price = power(2,30)

exec product_search @categories, @tags, @min_price, @max_price

演示:dbfiddle.uk demo

returns:

+----+----------+-------+
| id |  title   | price |
+----+----------+-------+
|  3 | iphone 8 |   100 |
+----+----------+-------+