【问题标题】:SQL Server query by column pairSQL Server 按列对查询
【发布时间】:2016-02-29 21:27:29
【问题描述】:

我正在研究像亚马逊这样的产品过滤器(分面搜索)。我有一个具有如下属性(颜色、内存、屏幕)的表格:

ArticleID  PropertyID  Value
---------  ----------  ------------
1          1           Black
1          2           8 GB
1          3           15"
2          1           White
2          2           8 GB
3          3           13"

我必须根据选择的属性来选择文章。您可以为一个属性选择多个值(例如 RAM:4 GB 和 8 GB),也可以选择多个属性(例如 RAM 和屏幕大小)。

我需要这样的功能:

SELECT ArticleID
FROM ArticlesProperties
WHERE (PropertyID = 2 AND Value IN ('4 GB', '8 GB'))
  AND (PropertyID = 3 AND Value IN ('13"'))

我曾经通过创建动态查询然后执行该查询来做到这一点:

SELECT ArticleID
FROM ArticlesProperties
WHERE PropertyID = 2 AND Value IN ('4 GB', '8 GB')

INTERSECT

SELECT ArticleID
FROM ArticlesProperties
WHERE PropertyID = 3 AND Value IN ('13"')

但我认为这不是好方法,必须有更好的解决方案。表中有数百万个属性,因此需要优化。

一个解决方案应该可以在 SQL Server 2014 标准版上运行,而不需要像 solr 等一些附加组件或搜索引擎。

我处于困境中,所以如果有人有一些想法或解决方案,我将非常感激。谢谢!

【问题讨论】:

  • 每个属性可以有多少个不同的值?这里有多少个不同的 propertyID?
  • 没有限制,但一个属性的值通常不超过 20-30 个,大约有 3000 个不同的属性,但文章是分组的,您只能在一个组内过滤,有 5 个一组 -15 个属性

标签: sql sql-server faceted-search


【解决方案1】:

我制作了一个 sn-p,显示了我将沿着哪些路线工作。良好的索引选择对于加快查询速度很重要。始终检查执行计划以调整索引。

注意事项:

  • 脚本使用临时表,但本质上它们与常规表没有什么不同。除了#select_properties,如果您打算使用脚本中概述的工作方式,则临时表应该成为常规表。

  • 使用属性选择值的 ID 存储文章属性,而不是实际选择值。当这些表被 SQL Server 缓存时,这可以节省磁盘空间和内存。 SQL Server 将尽可能多地在内存中缓存表,以更快地为 select 语句提供服务。

    如果文章属性表太大,SQL Server 可能不得不做磁盘 IO 来执行 select 语句,这肯定会拖慢语句。

    额外的好处是,对于查找,您正在寻找 ID(整数)而不是文本(VARCHAR's)。查找整数比查找字符串快很多。

  • 为表提供合适的索引以加快查询速度。为此,最好通过检查Actual Execution Plan 来分析查询。

    我在下面的 sn-p 中包含了几个这样的索引。 SQL Server会根据文章属性表中的行数和统计数据,选择最佳的索引来加快查询速度。

    如果 SQL Server 认为查询缺少 SQL 语句的正确索引,则实际执行计划将指示您缺少索引。当您的查询变慢时,最好通过检查 SQL Server Management Studio 中的实际执行计划来分析这些查询。

  • sn-p 使用一个临时表来指定您要查找的属性:#select_properties。通过插入属性 ID 和属性选择值 ID 来提供该表中的条件。最终选择查询选择至少有一个属性选择值适用于每个属性的文章。

    您将在要选择文章的会话中创建此临时表。然后插入搜索条件,触发 select 语句,最后删除临时表。


CREATE TABLE #articles(
    article_id INT NOT NULL,
    article_desc VARCHAR(128) NOT NULL,
    CONSTRAINT PK_articles PRIMARY KEY CLUSTERED(article_id)
);

CREATE TABLE #properties(
    property_id INT NOT NULL, -- color, size, capacity
    property_desc VARCHAR(128) NOT NULL,
    CONSTRAINT PK_properties PRIMARY KEY CLUSTERED(property_id)
);

CREATE TABLE #property_values(
    property_id INT NOT NULL,
    property_choice_id INT NOT NULL, -- eg color -> black, white, red
    property_choice_val VARCHAR(128) NOT NULL,
    CONSTRAINT PK_property_values PRIMARY KEY CLUSTERED(property_id,property_choice_id),
    CONSTRAINT FK_values_to_properties FOREIGN KEY (property_id) REFERENCES #properties(property_id)
);

CREATE TABLE #article_properties(
    article_id INT NOT NULL,
    property_id INT NOT NULL,
    property_choice_id INT NOT NULL
    CONSTRAINT PK_article_properties PRIMARY KEY CLUSTERED(article_id,property_id,property_choice_id),
    CONSTRAINT FK_ap_to_articles FOREIGN KEY (article_id) REFERENCES #articles(article_id),
    CONSTRAINT FK_ap_to_property_values FOREIGN KEY (property_id,property_choice_id) REFERENCES #property_values(property_id,property_choice_id)

);
CREATE NONCLUSTERED INDEX IX_article_properties ON #article_properties(property_id,property_choice_id) INCLUDE(article_id);

INSERT INTO #properties(property_id,property_desc)VALUES
    (1,'color'),(2,'capacity'),(3,'size');

INSERT INTO #property_values(property_id,property_choice_id,property_choice_val)VALUES
    (1,1,'black'),(1,2,'white'),(1,3,'red'),
    (2,1,'4 Gb') ,(2,2,'8 Gb') ,(2,3,'16 Gb'),
    (3,1,'13"')  ,(3,2,'15"')  ,(3,3,'17"');

INSERT INTO #articles(article_id,article_desc)VALUES
    (1,'First article'),(2,'Second article'),(3,'Third article');

-- the table you have in your question, slightly modified
INSERT INTO #article_properties(article_id,property_id,property_choice_id)VALUES 
    (1,1,1),(1,2,2),(1,3,2), -- article 1: color=black, capacity=8gb, size=15"
    (2,1,2),(2,2,2),(2,3,1), -- article 2: color=white, capacity=8Gb, size=13"
    (3,1,3),        (3,3,3); -- article 3: color=red, size=17"

-- The table with the criteria you are selecting on
CREATE TABLE #select_properties(
    property_id INT NOT NULL,
    property_choice_id INT NOT NULL,
    CONSTRAINT PK_select_properties PRIMARY KEY CLUSTERED(property_id,property_choice_id)
);
INSERT INTO #select_properties(property_id,property_choice_id)VALUES
    (2,1),(2,2),(3,1); -- looking for '4Gb' or '8Gb', and size 13"

;WITH aid AS (  
    SELECT ap.article_id
    FROM #select_properties AS sp
         INNER JOIN #article_properties AS ap ON
            ap.property_id=sp.property_id AND
            ap.property_choice_id=sp.property_choice_id
    GROUP BY ap.article_id
    HAVING COUNT(DISTINCT ap.property_id)=(SELECT COUNT(DISTINCT property_id) FROM #select_properties)
    -- criteria met when article has a number of properties matching, equal to the distinct number of properties in the selection set
)
SELECT a.article_id,a.article_desc
FROM aid 
     INNER JOIN #articles AS a ON 
         a.article_id=aid.article_id
ORDER BY a.article_id;
-- result is the 'Second article' with id 2

DROP TABLE #select_properties;
DROP TABLE #article_properties;
DROP TABLE #property_values;
DROP TABLE #properties;
DROP TABLE #articles;

【讨论】:

  • 似乎有效:)),我会使用这个解决方案,非常感谢,我不知道如何报答你,你救了我
  • @Mladen 不客气,✔ 是表达感谢的最佳方式 ;-)。
【解决方案2】:

intersect 可能工作得很好。

另一种方法是构造where 子句并使用聚合和having

SELECT ArticleID
FROM ArticlesProperties
WHERE ( PropertyID = 2 AND Value IN ('4 GB', '8 GB') ) OR
      ( PropertyID = 3 AND Value IN ('13"') )
GROUP BY ArticleId
HAVING COUNT(DISTINCT PropertyId) = 2;

但是,INTERSECT 方法可能会更好地利用 ArticlesProperties(PropertyId, Value) 上的索引,因此请先尝试一下,看看替代方法的性能要优于什么。

【讨论】:

  • @TT。 . . .谢谢。
【解决方案3】:

我假设(ArticleID, PropertyID) 是一个键。

这看起来像一个实体属性值 (EAV) 表或“开放模式”设计,因此基本上没有好的方法可以查询任何内容。您甚至可以考虑设置动态 PIVOT,但这相当复杂。

一种方法是EXISTS 表达式:

SELECT DISTINCT ArticleID
FROM ArticlesProperties ap
WHERE EXISTS (SELECT 1 FROM ArticlesProperties 
        WHERE ArticleID = ap.ArticleID AND PropertyID = 2 AND Value IN ('4 GB', '8 GB'))
    AND (SELECT 1 FROM ArticlesProperties 
        WHERE ArticleID = ap.ArticleID AND PropertyID = 3 AND Value IN ('13"'));

或者您可以尝试将ORCOUNT()HAVING 结合使用:

SELECT ArticleID
FROM ArticlesProperties
WHERE (PropertyID = 2 AND Value IN ('4 GB', '8 GB'))
    OR (PropertyID = 3 AND Value IN ('13"'))
GROUP BY ArticleID
HAVING COUNT(PropertyID) = 2;

【讨论】:

  • 一个 ArticleID 可以有多个 PropertyID 具有不同的 Value
  • 这是在同一个表中存储不同属性及其值的固有问题。将“Ram”和“Screen Size”作为“Article”表中的列有什么问题?
  • @Mladen 我没有关注你。一篇文章可以是白色和黑色?他们没有不同的 ArticleID?那么ArticlesProperties 的主键是什么?
  • @Max 因为今天晚些时候他们需要增加 CPU 速度,明天他们需要增加厚度,周五他们需要增加重量,下周二他们需要增加光折射。 EAV can be much more practical 而不是不断添加列、更改所有应用程序代码并重新部署 - 特别是在应用程序已分发且按需添加属性的情况下。
  • @AaronBertrand,谢谢你的解释。我一直在一个(内部)最终用户灵活性低得多的环境中工作。这确实有点道理。
【解决方案4】:

XML 参数

您的程序采用 XML 参数 @criteria XML 我曾经调试过的几件事: 删除表#properties 删除表#criteria

create table #properties (propertyId int)
insert into #properties values (1), (2) --presuming that you have a list of all the possible properties somewhere

-- This would be passed in by the application
declare @criteria XML = '<criteria>
<property id="1">
    <item value="8 GB" />
    <item value="4 GB" />
</property>
<property id="2">
    <item value="13 in" /> 
    <item value="4 in" />
</property>
</criteria>'

--encode the '"' and replace 'in' as needed

您需要的代码从这里开始:

create table #criteria 
(propertyId int, searchvalue nvarchar(20))


insert into #criteria (propertyId, searchvalue)
select  
    cc.propertyId,
    c.value('@value','nvarchar(20)')  
from #properties cc
cross apply @criteria.nodes(N'/criteria/property[@id=sql:column("PropertyID")]/item') t(c)

SELECT ArticleID, count(1)
FROM ArticlesProperties ap
join #criteria cc on  cc.propertyId = ap.propertyId and cc.searchvalue = ap.value
group by ArticleID 
having count(1) = (select count(distinct propertyid from #criteria))

【讨论】:

  • 如果我有固定数量的属性就可以了,用户可以选择任意数量的属性,它可以是 5-10 个不同的属性,每个属性平均两个值或 3 个属性和 10每个属性的值或任何可能的组合
  • 组合的 where 子句总是 "and" 吗?例如:prop1 = 1 AND prop2 in (2, 3, 4) AND ...
  • 编写要传递的 XML 变得更有趣了,现在您必须知道属性 ID...
  • 这不起作用,它返回具有 4 gb ram 或 8 gb ram 或 15" 屏幕的文章,它应该返回具有 (4 gb ram 或 8 gb ram) AND 15" 屏幕的文章跨度>
猜你喜欢
  • 1970-01-01
  • 2019-12-06
  • 1970-01-01
  • 1970-01-01
  • 2021-10-14
  • 2013-09-11
  • 2023-03-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多