用于搜索具有无限个位域的表的 SQL 设计方法

Posted

技术标签:

【中文标题】用于搜索具有无限个位域的表的 SQL 设计方法【英文标题】:SQL design approach for searching a table with an unlimited number of bit fields 【发布时间】:2010-12-03 01:14:12 【问题描述】:

考虑搜索包含公寓租赁信息的表:使用该接口的客户端选择在数据库中表示为位字段的多个条件,例如:

允许宠物 有停车场 有甲板 现代厨房

等等。

我们面临的情况是,我们软件的每个新客户都有额外的字段,他们希望最终用户可以在其中进行搜索。位域的数量可以达到数百个。

我正在考虑三种方法,希望得到意见和/或不同的方法。

当前方法:添加更多位字段,动态构建sql查询并使用EXEC执行:SET @SQL = @SQL + 'l.[NumUnits],' exec(@SQL))

继续添加更多位域。 (有 300 列的表格?)

将数据表示为一个字段中的一系列位。我不清楚这种方法是否可行,请考虑我上面提供的 4 个示例位字段。该字段可能如下所示:1011 表示“hasparking”为 false,但其他所有字段为 true。我不清楚的是你将如何构建一个你不关心它是假还是真的查询,例如 1?11 搜索的人需要 1,3 和 4 为真,但不关心是否'HasParking' 是真还是假。

移动到基于属性的方法,其中您有一个表“AttributeTypeID”和一个表 PropertyAttributes,它将 PropertyID 连接到 AttributeTypeId,新的位字段只是 AttributeTypeID 表中的一行。

其他方法?这是众所周知的 SQL 设计模式吗?

感谢您的帮助

KM- 编辑每条评论

属性表中还有其他几行,称为listingattributes 创建表 [dbo].[ListingAttributes]( [ListingID] [bigint] 非空, [AttributeID] [int] IDENTITY(1,1) 非空, [AttributeType] [smallint] 非空, [BoardID] [int] 非空, [ListingMLS] [varchar](30) 非空, [PropertyTypeID] [char](3) 非空, [StatusID] [varchar](2) 非空, 主键集群 ( [属性ID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY] ) 开 [主要] ;使用 GetMatchingAttributes 作为 ( 选择 ListingID,COUNT(AttributeID) AS CountOfMatches FROM 列表属性 在哪里 板ID = 1 和 状态ID IN ('A') 和 --PropertyTypeID in (select * from @PropertyType) - 和 属性类型 IN (2,3,6) 按列表 ID 分组 有 COUNT(AttributeID)=(3) ) 选择 计数(l.listingid) 从清单 l INNER JOIN GetMatchingAttributes m ON l.ListingID=m.ListingID - 在哪里 -- StatusID IN(从@Status中选择*) - 和 --PropertyTypeID in (select * from @PropertyType) 1 1 0 NULL NULL 1 NULL 1 NULL NULL NULL 0.1934759 NULL空选择 0 空 |--Compute Scalar(DEFINE:([Expr1006]=CONVERT_IMPLICIT(int,[Expr1012],0))) 1 2 1 Compute Scalar Compute Scalar DEFINE:([Expr1006]=CONVERT_IMPLICIT(int,[Expr1012],0))[Expr1006]=CONVERT_IMPLICIT(int,[Expr1012],0) 1 0 0.001483165 11 0.1934759 [Expr1006] NULL PLAN_ROW 0 1 |--Stream Aggregate(DEFINE:([Expr1012]=Count(*))) 1 3 2 Stream Aggregate NULL [Expr1012]=Count(*) 1 0 0.001483165 11 0.1934759 [Expr1012] NULL PLAN_ROW 0 1 |--Filter(WHERE:([Expr1005]=(3))) 1 4 3 Filter Filter WHERE:([Expr1005]=(3))空 2471.109 0 0.00440886 9 0.1919928 空 空计划行 0 1 |--Compute Scalar(DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0))) 1 5 4 Compute Scalar Compute Scalar DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0))[Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0) 9185.126 0 0.01422281 11 0.1875839 [Expr1005] NULL PLAN_ROW 0 1 |--Stream Aggregate(GROUP BY:(.[dbo].[ListingAttributes].[ListingID]) DEFINE:([Expr1011]=Count(*))) 1 6 5 Stream Aggregate GROUP BY:(.[dbo] .[ListingAttributes].[ListingID]) [Expr1011]=Count(*) 9185.126 0 0.01422281 11 0.1875839 [Expr1011] NULL PLAN_ROW 0 1 |--Index Seek(OBJECT:(.[dbo].[ListingAttributes].[_dta_index_ListingAttributes_BoardID_ListingID__AttributeType_PropertyTypeID_StatusID_6_7]), SEEK:(.[dbo].[ListingAttributes].[BoardID]=(1)), WHERE:(.[dbo ].[ListingAttributes].[StatusID]='A' AND (.[dbo].[ListingAttributes].[AttributeType]=(2) OR .[dbo].[ListingAttributes].[AttributeType]=(3) OR . [dbo].[ListingAttributes].[AttributeType]=(6))) ORDERED FORWARD) 1 7 6 Index Seek Index Seek OBJECT:(.[dbo].[ListingAttributes].[_dta_index_ListingAttributes_BoardID_ListingID__AttributeType_PropertyTypeID_StatusID_6_7]), SEEK:(.[dbo ].[ListingAttributes].[BoardID]=(1)), WHERE:(.[dbo].[ListingAttributes].[StatusID]='A' AND (.[dbo].[ListingAttributes].[AttributeType]=( 2) 或 .[dbo].[ListingAttributes].[AttributeType]=(3) 或 .[dbo].[ListingAttributes].[Attr ibuteType]=(6))) 向前排序 .[dbo].[ListingAttributes].[ListingID], .[dbo].[ListingAttributes].[AttributeID], .[dbo].[ListingAttributes].[AttributeType], . [dbo].[ListingAttributes].[StatusID] 16050.41 0.09677318 0.0315279 26 0.1283011 .[dbo].[ListingAttributes].[ListingID], .[dbo].[ListingAttributes].[AttributeID], .[dbo].[ListingAttributes] .[AttributeType]、.[dbo].[ListingAttributes].[StatusID] NULL PLAN_ROW 0 1 (7 行受影响)

【问题讨论】:

添加了新查询以排除给定属性,请参阅最新编辑。 根据 KM 对基于属性的方法的反馈为每个 KM cmets 添加了展示计划 【参考方案1】:

创建一个表来存储基于公寓的属性或搜索列。 绝对不要继续添加更多位字段列..噩梦维护和噩梦编码。并且绝对不要请不要动态生成 where 语句并使用 exec。

【讨论】:

【参考方案2】:

这样的事情可能对你有用:

定义表格:

CREATE TABLE #Apartments
(
     ApartmentID    int          not null primary key identity(1,1)
    ,ApartmentName  varchar(500) not null
    ,Status         char(1)      not null default ('A') 
    --....
)

CREATE TABLE #AttributeTypes
(
    AttributeType         smallint     not null primary key
    ,AttributeDescription varchar(500) not null
)

CREATE TABLE #Attributes  --boolean attributes, if row exists apartment has this attribute 
(
     ApartmentID     int not null --FK to Apartments.ApartmentID    
    ,AttributeID     int not null primary key identity(1,1)
    ,AttributeType   smallint  not null --fk to AttributeTypes
)

插入样本数据:

SET NO COUNT ON
INSERT INTO #Apartments VALUES ('one','A')
INSERT INTO #Apartments VALUES ('two','A')
INSERT INTO #Apartments VALUES ('three','I')
INSERT INTO #Apartments VALUES ('four','I')

INSERT INTO #AttributeTypes VALUES (1,'dishwasher')
INSERT INTO #AttributeTypes VALUES (2,'deck')
INSERT INTO #AttributeTypes VALUES (3,'pool')
INSERT INTO #AttributeTypes VALUES (4,'pets allowed')
INSERT INTO #AttributeTypes VALUES (5,'washer/dryer')
INSERT INTO #AttributeTypes VALUES (6,'Pets Alowed')
INSERT INTO #AttributeTypes VALUES (7,'No Pets')

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,2)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,3)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,4)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,5)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (1,6)

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,2)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,3)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,4)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (2,7)

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,2)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,3)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (3,4)

INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (4,1)
INSERT INTO #Attributes (ApartmentID, AttributeType) VALUES (4,2)
SET NOCOUNT OFF

示例搜索查询:

;WITH GetMatchingAttributes AS
(
SELECT
    ApartmentID,COUNT(AttributeID) AS CountOfMatches
    FROM #Attributes
    WHERE AttributeType IN (1,2,3)  --<<change dynamically or split a CSV string and join in
    GROUP BY ApartmentID
    HAVING COUNT(AttributeID)=3--<<change dynamically or split a CSV string and use COUNT(*) from resulting table
)
SELECT
    a.*
    FROM #Apartments                      a
        INNER JOIN GetMatchingAttributes m ON a.ApartmentID=m.ApartmentID
    WHERE a.Status='A'
    ORDER BY m.CountOfMatches DESC

输出:

ApartmentID ApartmentName 
----------- --------------
1           one           
2           two           

(2 row(s) affected)

在上面的搜索查询中,我只包含了一个属性 ID 的 CSV 字符串来搜索。实际上,您可以创建一个搜索存储过程,在其中传入一个包含要搜索的 ID 的 CSV 参数。您可以查看 this answer 了解如何将 CSV 字符串无循环拆分为您可以加入的表格。这将导致不需要使用任何动态 SQL。

EDIT 基于许多 cmets:

如果您向#AttributeTypes 表添加几列,您可以动态构建搜索页面。以下是一些建议:

状态:“活跃”“不活跃” ListOrder:可以使用它来排序来构建屏幕 ColumnNumber:可以帮助组织同一屏幕行上的字段 AttributeGroupID:对字段进行分组,见下文 等。

您可以将所有字段设置为复选框,或添加另一个名为#AttributesGroups 的表,然后将一些字段组合在一起并使用单选按钮。例如,由于“Pets Allowed”和“No Pets”是互斥的,因此在#AttributesGroups 表“Pets”中添加一行。应用程序将对界面中的属性进行分组。组中的属性与常规未分组属性的工作方式相同,只需收集选定的 ID 并将其传递给搜索过程。但是,对于每个组,您需要让应用程序包含一个“无偏好”单选按钮并将其默认打开。此选项将没有属性 ID,也不会传入,因为您不想考虑该属性。

在我的示例中,我确实展示了 #Apartments 中的“超级属性”示例 表,“状态”。您应该只考虑此表的主要属性。如果您开始使用这些,您可能希望将 CTE 更改为 FROM #Apartments,并对这些字段进行过滤,然后加入 #Attributes。但是你会遇到Dynamic Search Conditions, so read this article by Erland Sommarskog 的问题。

编辑最新的 cmets:

这里有一个排除属性列表的代码:

;WITH GetMatchingAttributes AS
(
SELECT
    ApartmentID,COUNT(AttributeID) AS CountOfMatches
    FROM #Attributes
    WHERE AttributeType IN (1,2,3)  --<<change dynamically or split an include CSV string and join in
    GROUP BY ApartmentID
    HAVING COUNT(AttributeID)=3--<<change dynamically or split a CSV string and use COUNT(*) from resulting include table
)
, SomeRemoved AS
(
SELECT
    m.ApartmentID
    FROM GetMatchingAttributes      m
        LEFT OUTER JOIN #Attributes a ON m.ApartmentID=a.ApartmentID 
            AND a.AttributeType IN (5,6)   --<<change dynamically or split an exclude CSV string and join in
    WHERE a.ApartmentID IS NULL
)
SELECT
    a.*
    FROM #Apartments           a
        INNER JOIN SomeRemoved m ON a.ApartmentID=m.ApartmentID
    WHERE a.Status='A'

我不认为我会走这条路。我会采用我在上面之前的编辑中概述的方法。当需要包含/排除一个属性时,我只需为每个属性添加一个属性:“允许携带宠物”和“禁止携带宠物”。

我更新了原始帖子中的示例数据以显示这一点。

运行原始查询:

(..,..,6,..) 寻找可以养宠物的公寓 (..,..,7,..) 查找不允许携带宠物的公寓 (..,..,..) 如果没有偏好。

我认为这是更好的方法。结合上次编辑中描述的分组想法和动态构建的搜索页面,我认为这会更好,运行速度会更快。

【讨论】:

+1 - 这非常巧妙。我没有想过在比赛中进行计数,然后找到所有 X 匹配的那些。它不适用于我的问题,但可能适用于汤姆。但是有一个警告-在我看来,如果您想处理某些属性会排除公寓的情况(例如,不想要有宠物的地方的人),则您必须同时存储属性和“非属性”允许)。或者您必须更改 GetMatchingAttributes 以便包含 IN 和 NOT IN 子句。不过干得好! 另外,TOM 可能想考虑在他的公寓表中包含核心属性(那些经常被要求的属性),然后将属性表用于更晦涩的项目以优化性能。 这看起来很棒,感谢您的意见,我检查后会发布更多内容 这太棒了!我认为链接中的无循环拆分功能可能是我见过的最有用的 SQL 实用程序!再次感谢! 我只是想在这里扔一件事。 SQL 2008 支持表值参数。这意味着您可以将整个搜索条件作为一个表传递,并简单地对该表进行连接以实现相同的目标。哪个更快,因为它不依赖解析函数调用来工作。【参考方案3】:

我曾多次尝试存储健康状态标记!

当我第一次开始(2000 年?)时,我尝试了一种角色定位方法(你的#2),发现它很快变得非常笨拙,因为我一遍又一遍地与相同的问题搏斗:“哪个位置是‘允许宠物’再次?”或者,更糟糕的是,“这个字符串现在有多长?/我在哪个位置?”你能解决这个问题——开发对象来为你管理事情吗?嗯,是的,在某种程度上。但是,与由数据库为我管理字段身份相比,我真的不明白它要花费多少额外的工作。

第二次,我使用了类似于您的解决方案 #3 的属性/值对方法。这基本上是可行的,并且出于特殊需要,我仍然使用 PIVOT 生成属性/值对。另外,我的背景是人工智能,我们在机械定理证明中一直使用属性/值对,所以这对我来说很自然。

然而,这种方法存在一个巨大的问题:提取任何一个事实(“告诉我允许养宠物的公寓”)很容易,但提取所有满足多个约束的记录很快就会得到非常非常难看(见下面我的例子)。

**SO...**我最终将字段添加到表中。我理解 Jon、'Unknown' 和 'New In Town' 给出的偏爱其他方法的理论原因,我会同意其中一个或两个。但经验是一个相当苛刻的老师......

更多的东西

首先,我不同意添加更多位字段是维护的噩梦 - 至少与字符位方法(您的#2)相比。也就是说,每个属性都有一个不同的字段可以确保不需要“管理”来确定哪个槽属于哪个属性。

其次,拥有 300 个字段并不是真正的问题 - 任何体面的数据库都可以毫无问题地做到这一点。

第三,您的真正的问题痛苦的根源实际上是动态生成查询的问题。如果你和我一样,这个问题真的是关于“我真的必须有这个庞大、肮脏和不优雅的“IF”语句链来构造一个查询吗?”

很遗憾,答案是肯定的。您建议的所有三种方法仍将归结为一系列 IF 语句。

数据库位域方法中,您最终会得到一系列 IF 语句,其中所有列都必须像这样添加:

string SQL = "Select X,Y,Z Where ";

if (AllowsPets == 0)
  SQL += "(AllowsPets = 0) AND ";
else if (AllowsPets == )
  SQL += "(AllowsPets = 1) AND ";  // Else AllowsPets not in query
.
.
.
SQL = SQL.Substring(SQL.Length - 4);  // Get rid of trailing 'AND' / alternatively append '(1=1)'

字符位置方法中,您将执行相同的操作,但您的“附加”会在您的 SQL 中添加“0”、“1”或“_”。当然,您还会遇到维护问题,决定哪一个是我上面讨论的(枚举有帮助,但不能完全解决问题)。

如上所述,属性值方法实际上是最糟糕的。您必须创建一个讨厌的子查询链(这肯定会导致某种堆栈溢出,包含 300 个子句),或者您需要像这样的 IF-THEN:

// Kill any previously stored selections.
SQLObject.Execute("Delete From SelectedApts Where SessionKey=X");
// Start with your first *known* attr/value and fill a table with the results.
.
.
Logic to pick first known attr/value pair
.
.
SQLObject.Execute("Insert Into SelectedApts Select X as SessionKey, AptID From AttrValue Where AllowsPets=1");

// Now you have the widest set that meets your criteria. Time to whittle it down.
if (HasParking == 1)
  SQLObject.Execute("Delete From SelectedApts Where AptID not in (Select AptID From AttrValue Where AllowsChildren=1));
if (AllowsChildren == 0)
  SQLObject.Execute("Delete From SelectedApts Where AptID not in (Select AptID From AttrValue Where AllowsChildren=0));
.
.
.
// Perform 2-300 more queries to keep whittling down your set to the actual match.

现在,您可以稍微优化一下,以便运行更少的查询(PIVOT、子查询集或使用 UNION 运算符),但事实是,与您可以使用的单个查询相比,这变得非常昂贵(但必须构建)使用其他方法。

因此,无论您采取何种方法,这都是一个痛苦的问题 - 确实没有任何魔法可以帮助您避免它。但是,以前去过那里,我绝对会推荐方法#1。

更新:如果您真的专注于提取标准匹配项(“所有具有 A、B 和 C 的公寓”)并且不需要其他查询(例如“...Sum( AllowsPets)、Sum(AllowsChildren)..." 或 "...(AllowsPets=1) OR (AllowsChildren=1)...") 那么我越看越喜欢 KM 的回答。它非常聪明,看起来可能很快。

【讨论】:

感谢您的冗长、乐于助人和消息灵通的回答马克。我整个下午都在跟踪 eav/pivot 线索,我越是研究这个,我就越开始觉得在我的一个查询中添加 100 个特定位字段和 100 个 if 语句(我们只有一个搜索查询)是方式去 大声笑 - 男孩,我真希望早在 2000 年就有人告诉我了!不过,正如我所说,这是一个丑陋的问题。每当优秀的开发人员面临一个丑陋的问题时,他们都会不由自主地认为必须有更好的方法。所以你的搜索是可以理解的。在这一点上,大部分内部都隐藏在我编写的各种类中,但经历它的痛苦一直伴随着我! @Mark Brittingham,你觉得我的回答怎么样?我当然不喜欢 EAV 表设计,至少不是。我只是想我会试一试。 Tom - 不过请参阅 KM 的回答。它非常巧妙,并且解决了我对属性/值对的反对意见。我仍然无法使用它,因为我需要跨记录集报告统计信息,而不是查找与特定标准集匹配的集。但是,它可能对您有用。当然,我上面的建议可能仍然成立——您可能仍然会发现创建一个包含许多字段的表是最简单的。至少 SO 给了你很多思考! KM - 请在您的回答中查看我的 cmets。我只是希望我能想出它!不过,在我写 Tom 的过程中,我并没有特别关注我的项目中的“具有属性 X 的对象”。所以...我会放慢自己的步伐,说我没有调整到那个特定的应用程序。尽管如此,我还是很欣赏您从纯技术角度所做的工作。【参考方案4】:

我从未对此进行过测试,但如果您要创建一个 varchar(256) 字段,将您的所有标志存储为一长串 0 和 1 的字符串。

例如,

AllowsPets = 1 HasParking = 0 HasDeck = 1 现代厨房 = 1

应该是:

PropertyFlags = 1011

如果您正在寻找 AllowsPets 和 HasDeck 的内容,那么搜索查询将如下所示:

WHERE PropertyFlags LIKE '1_1_'(下划线表示like 子句中的单个通配符)

这将解决您将来在搜索中添加其他列的问题,但我不确定这将如何提高性能。

有没有人尝试过类似的方法?

【讨论】:

这里的问题是很容易丢失属性标志和值的定位之间的关系。此外,它使为每个客户扩展它成为一场噩梦。 如果将其存储为 Int,然后使用按位掩码进行过滤,效率会更高。【参考方案5】:

我建议您采用第二种方法,称为Entity-attribute-value model。这可能是唯一可以根据需要进行扩展的方法。

您还可以进行两种搜索,基本搜索和高级搜索。您将基本搜索的属性保存在一个表中,并将所有高级属性保存在另一个表中。这样至少基本搜索会保持快速,因为属性的数量会随着时间的推移而增加。

【讨论】:

@Tom:否则,你会回来问我们这样的问题:***.com/questions/1494535/… 我刚刚对 EAV 做了一些快速研究,从过去 20 分钟的发现来看,它的名声相当糟糕。查询很残酷,20,30 个内部连接......跨度> @Tom,这些查询的连接通常是由于对如何编写 sql 的误解。 那么如何使用 EAV 方法编写查询来选择 hasparking = true、hasdeck = true、allowspets = false 的属性? @Tom,澄清一下,在使用 EAV 模型时,您的搜索查询应动态创建数据透视表以运行常规选择。这消除了所有的连接。如有必要,您可以将动态创建的数据透视表代码存储为视图。添加新属性时,只需使视图无效并重新创建。

以上是关于用于搜索具有无限个位域的表的 SQL 设计方法的主要内容,如果未能解决你的问题,请参考以下文章

在这种情况下,设计这个具有 3 个唯一列的简单表的最佳方法是啥?

创建返回具有复杂 SQL 的表的 Oracle 视图或过程,是不是可能以及如何?

SQL Serever学习6——数据表2

Mysql 6 业务设计(逻辑设计)+物理设计

可以使用 Apache Kafka“无限保留策略”作为具有 CQRS 的事件源系统的基础吗?

mongodb的设计特征