如何在动态搜索查询中使用关系划分来过滤结果?
How to filter results using relational division in a dynamic search query?
使用以下查询,我将根据所选 tags
或 categories
:
筛选结果
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)
returns:
+----+----------+-------+
| id | title | price |
+----+----------+-------+
| 3 | iphone 8 | 100 |
+----+----------+-------+
在性能方面,将其重写为具有动态 sql 执行或至少 option (recompile)
的过程将受益,如这些参考资料所示:
- An Updated "Kitchen Sink" Example - Aaron Bertand
- Parameter Sniffing, Embedding, and the RECOMPILE Options - Paul White
- Dynamic Search Conditions - Erland Sommarskog
- Catch-all queries - Gail Shaw
这是一个动态 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
returns:
+----+----------+-------+
| id | title | price |
+----+----------+-------+
| 3 | iphone 8 | 100 |
+----+----------+-------+
使用以下查询,我将根据所选 tags
或 categories
:
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)
returns:
+----+----------+-------+
| id | title | price |
+----+----------+-------+
| 3 | iphone 8 | 100 |
+----+----------+-------+
在性能方面,将其重写为具有动态 sql 执行或至少 option (recompile)
的过程将受益,如这些参考资料所示:
- An Updated "Kitchen Sink" Example - Aaron Bertand
- Parameter Sniffing, Embedding, and the RECOMPILE Options - Paul White
- Dynamic Search Conditions - Erland Sommarskog
- Catch-all queries - Gail Shaw
这是一个动态 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
returns:
+----+----------+-------+
| id | title | price |
+----+----------+-------+
| 3 | iphone 8 | 100 |
+----+----------+-------+