如何在动态搜索查询中使用关系除法过滤结果?

时间:2017-10-28 08:32:02

标签: sql sql-server relational-division

使用以下查询我将根据选定的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

2 个答案:

答案 0 :(得分:1)

您可以使用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

返回:

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

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

以下是使用exists ...having count()...代替left join... where... having count(distinct ...)的查询的动态sql搜索过程的示例,该过程简化了计划(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

返回:

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

答案 1 :(得分:0)

根据您的示例数据,我认为您加入了错误的列tag_FK而不是product_FK,因此表LEFT JOIN上的tbl_product_tags应为:

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, ','))))
相关问题