最好的设计从单个列引用多个表?


18

拟议方案

首先,这是我提议的架构示例,供我在整个帖子中参考:

Clothes
---------- 
ClothesID (PK) INT NOT NULL
Name VARCHAR(50) NOT NULL
Color VARCHAR(50) NOT NULL
Price DECIMAL(5,2) NOT NULL
BrandID INT NOT NULL
...

Brand_1
--------
ClothesID (FK/PK) int NOT NULL
ViewingUrl VARCHAR(50) NOT NULL
SomeOtherBrand1SpecificAttr VARCHAR(50) NOT NULL

Brand_2
--------
ClothesID (FK/PK) int NOT NULL
PhotoUrl VARCHAR(50) NOT NULL
SomeOtherBrand2SpecificAttr VARCHAR(50) NOT NULL

Brand_X
--------
ClothesID (FK/PK) int NOT NULL
SomeOtherBrandXSpecificAttr VARCHAR(50) NOT NULL

问题陈述

我有一个衣服桌子,上面有诸如名称,颜色,价格,白兰地酒等列用于描述特定服装的属性。

这是我的问题:不同品牌的服装需要不同的信息。解决此类问题的最佳实践是什么?

请注意,出于我的目的,有必要从衣服条目开始查找特定于品牌的信息。这是因为我首先将服装条目中的信息显示给用户,然后我必须使用其品牌特定信息来购买商品。总之,衣服(来源)和brand_x表之间必须有方向性关系。

拟议/现行解决方案

为了解决这个问题,我想到了以下设计方案:

衣服表将具有品牌柱可具有的ID值范围为1至x,其中一个特定的ID对应于一个特定品牌的表。例如,id值1对应于表brand_1(可能具有url列),id 2对应于brand_2(可能具有供应商列),依此类推

因此,要将特定的服装条目与其特定于品牌的信息相关联,我可以想象应用程序级的逻辑如下所示:

clothesId = <some value>
brand = query("SELECT brand FROM clothes WHERE id = clothesId")

if (brand == 1) {
    // get brand_1 attributes for given clothesId
} else if (brand == 2) {
    // get brand_2 attributes for given clothesId
} ... etc.

其他意见和想法

我试图在BCNF中标准化我的整个数据库,尽管这是我想出的,但是生成的应用程序代码使我感到非常焦虑。除了在应用程序级别之外,没有其他方法可以强制执行关系,因此设计感觉很hacky,并且我预计很容易出错。

研究

我确保在发帖之前先浏览一下以前的条目。这是我设法找到的几乎相同的问题的帖子。我之所以发表这篇文章,是因为似乎唯一的答案没有SQL或基于设计的解决方案(即,它提到了OOP,继承和接口)。

在数据库设计方面,我还是一个新手,因此,我将不胜感激。


似乎在堆栈溢出方面有更多有用的响应:

我已经提到了那里的解决方案,并建议其他人也找到我的问题。

尽管提供了上述链接,但我仍在这里寻找答复,我们将不胜感激!

我正在使用PostgreSQL。

Answers:


7

我个人不喜欢为此目的使用多表架构。

  • 很难确保完整性。
  • 很难维护。
  • 很难过滤结果。

我设置了一个dbfiddle 示例

我建议的表架构:

CREATE TABLE #Brands
(
BrandId int NOT NULL PRIMARY KEY,
BrandName nvarchar(100) NOT NULL 
);

CREATE TABLE #Clothes
(
ClothesId int NOT NULL PRIMARY KEY,
ClothesName nvarchar(100) NOT NULL 
);

-- Lookup table for known attributes
--
CREATE TABLE #Attributes
(
AttrId int NOT NULL PRIMARY KEY,
AttrName nvarchar(100) NOT NULL 
);

-- holds common propeties, url, price, etc.
--
CREATE TABLE #BrandsClothes
(
BrandId int NOT NULL REFERENCES #Brands(BrandId),
ClothesId int NOT NULL REFERENCES #Clothes(ClothesId),
VievingUrl nvarchar(300) NOT NULL,
Price money NOT NULL,
PRIMARY KEY CLUSTERED (BrandId, ClothesId),
INDEX IX_BrandsClothes NONCLUSTERED (ClothesId, BrandId)
);

-- holds specific and unlimited attributes 
--
CREATE TABLE #BCAttributes
(
BrandId int NOT NULL REFERENCES #Brands(BrandId),
ClothesId int NOT NULL REFERENCES #Clothes(ClothesId),
AttrId int NOT NULL REFERENCES #Attributes(AttrId),
AttrValue nvarchar(300) NOT NULL,
PRIMARY KEY CLUSTERED (BrandId, ClothesId, AttrId),
INDEX IX_BCAttributes NONCLUSTERED (ClothesId, BrandId, AttrId)
);

让我插入一些数据:

INSERT INTO #Brands VALUES 
(1, 'Brand1'), (2, 'Brand2');

INSERT INTO #Clothes VALUES 
(1, 'Pants'), (2, 'T-Shirt');

INSERT INTO #Attributes VALUES
(1, 'Color'), (2, 'Size'), (3, 'Shape'), (4, 'Provider'), (0, 'Custom');

INSERT INTO #BrandsClothes VALUES
(1, 1, 'http://mysite.com?B=1&C=1', 123.99),
(1, 2, 'http://mysite.com?B=1&C=2', 110.99),
(2, 1, 'http://mysite.com?B=2&C=1', 75.99),
(2, 2, 'http://mysite.com?B=2&C=2', 85.99);

INSERT INTO #BCAttributes VALUES
(1, 1, 1, 'Blue, Red, White'),
(1, 1, 2, '32, 33, 34'),
(1, 2, 1, 'Pearl, Black widow'),
(1, 2, 2, 'M, L, XL'),
(2, 1, 4, 'Levis, G-Star, Armani'),
(2, 1, 3, 'Slim fit, Regular fit, Custom fit'),
(2, 2, 4, 'G-Star, Armani'),
(2, 2, 3, 'Slim fit, Regular fit'),
(2, 2, 0, '15% Discount');

如果您需要获取常用属性:

SELECT     b.BrandName, c.ClothesName, bc.VievingUrl, bc.Price
FROM       #BrandsClothes bc
INNER JOIN #Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN #Clothes c
ON         c.ClothesId = bc.ClothesId
ORDER BY   bc.BrandId, bc.ClothesId;

BrandName   ClothesName   VievingUrl                  Price
---------   -----------   -------------------------   ------
Brand1      Pants         http://mysite.com?B=1&C=1   123.99
Brand1      T-Shirt       http://mysite.com?B=1&C=2   110.99
Brand2      Pants         http://mysite.com?B=2&C=1    75.99
Brand2      T-Shirt       http://mysite.com?B=2&C=2    85.99

或者,您可以轻松获得各品牌的衣服:

给我Brand2的所有衣服

SELECT     c.ClothesName, b.BrandName, a.AttrName, bca.AttrValue
FROM       #BCAttributes bca
INNER JOIN #BrandsClothes bc
ON         bc.BrandId = bca.BrandId
AND        bc.ClothesId = bca.ClothesId
INNER JOIN #Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN #Clothes c
ON         c.ClothesId = bc.ClothesId
INNER JOIN #Attributes a
ON         a.AttrId = bca.AttrId
WHERE      bca.ClothesId = 2
ORDER BY   bca.ClothesId, bca.BrandId, bca.AttrId;

ClothesName   BrandName   AttrName   AttrValue
-----------   ---------   --------   ---------------------
T-Shirt       Brand1      Color      Pearl, Black widow
T-Shirt       Brand1      Size       M, L, XL
T-Shirt       Brand2      Custom     15% Discount
T-Shirt       Brand2      Shape      Slim fit, Regular fit
T-Shirt       Brand2      Provider   G-Star, Armani

但是对我来说,这种模式的最好之处之一是您可以按Attibutes进行过滤:

给我所有具有以下属性的衣服:尺寸

SELECT     c.ClothesName, b.BrandName, a.AttrName, bca.AttrValue
FROM       #BCAttributes bca
INNER JOIN #BrandsClothes bc
ON         bc.BrandId = bca.BrandId
AND        bc.ClothesId = bca.ClothesId
INNER JOIN #Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN #Clothes c
ON         c.ClothesId = bc.ClothesId
INNER JOIN #Attributes a
ON         a.AttrId = bca.AttrId
WHERE      bca.AttrId = 2
ORDER BY   bca.ClothesId, bca.BrandId, bca.AttrId;

ClothesName   BrandName   AttrName   AttrValue
-----------   ---------   --------   ----------
Pants         Brand1      Size       32, 33, 34
T-Shirt       Brand1      Size       M, L, XL

使用多表模式时,无论使用任何先前的查询,都将需要处理无限数量的表或XML或JSON字段。

此模式的另一个选项是,您可以定义模板,例如,可以添加新表BrandAttrTemplates。每次添加新记录时,都可以使用触发器或SP来为此分支生成一组预定义属性。

对不起,我想扩大我的解释,我认为这比我的英语更清楚。

更新资料

我的当前答案应该适用于任何RDBMS。根据您的评论,如果您需要过滤属性值,我建议进行一些小的更改。

就MS-Sql不允许使用数组而言,我设置了一个保留相同表模式的新示例,但是将AttrValue更改为ARRAY字段类型。

实际上,使用POSTGRES,您可以使用GIN索引来利用此数组。

(让我说@EvanCarrol对Postgres有很好的了解,当然比我更好。但是让我补充一点。)

CREATE TABLE BCAttributes
(
BrandId int NOT NULL REFERENCES Brands(BrandId),
ClothesId int NOT NULL REFERENCES Clothes(ClothesId),
AttrId int NOT NULL REFERENCES Attrib(AttrId),
AttrValue text[],
PRIMARY KEY (BrandId, ClothesId, AttrId)
);

CREATE INDEX ix_attributes on BCAttributes(ClothesId, BrandId, AttrId);
CREATE INDEX ix_gin_attributes on BCAttributes using GIN (AttrValue);


INSERT INTO BCAttributes VALUES
(1, 1, 1, '{Blue, Red, White}'),
(1, 1, 2, '{32, 33, 34}'),
(1, 2, 1, '{Pearl, Black widow}'),
(1, 2, 2, '{M, L, XL}'),
(2, 1, 4, '{Levis, G-Star, Armani}'),
(2, 1, 3, '{Slim fit, Regular fit, Custom fit}'),
(2, 2, 4, '{G-Star, Armani}'),
(2, 2, 3, '{Slim fit, Regular fit}'),
(2, 2, 0, '{15% Discount}');

现在,您还可以使用单个属性值查询,例如:

给我所有裤子的清单尺寸:33

AttribId = 2 AND ARRAY['33'] && bca.AttrValue

SELECT     c.ClothesName, b.BrandName, a.AttrName, array_to_string(bca.AttrValue, ', ')
FROM       BCAttributes bca
INNER JOIN BrandsClothes bc
ON         bc.BrandId = bca.BrandId
AND        bc.ClothesId = bca.ClothesId
INNER JOIN Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN Clothes c
ON         c.ClothesId = bc.ClothesId
INNER JOIN Attrib a
ON         a.AttrId = bca.AttrId
WHERE      bca.AttrId = 2
AND        ARRAY['33'] && bca.AttrValue
ORDER BY   bca.ClothesId, bca.BrandId, bca.AttrId;

结果如下:

clothes name | brand name | attribute | values 
------------- ------------ ----------  ---------------- 
Pants          Brand1       Size        32, 33, 34

我真的很喜欢这种解释,但似乎我们只是在多表模式之间进行权衡,以便在单个列中包含多个CSV-如果可以的话。另一方面,我觉得我更喜欢这种方法,因为它不需要更改架构,但是再次感觉就像是我们将问题推向其他地方(即通过具有可变长度的列)。这可能是个问题。如果我想在数据库中查询3号裤子怎么办?对于这种问题,也许没有一个好的,干净的解决方案。这个概念是否有名称,以便让我进一步研究?
youngrrrr

实际上...要回答我提出的问题,也许可以从@EvanCarroll的解决方案中借用答案:即,通过使用jsonb类型而不是简单的CSV格式的TEXT / STRINGS。但是,如果有这个概念的名字,请告诉我!
youngrrrr

1
这是解决方案的实体属性值类型。性能和良好设计之间的妥协不是很坏。不过,这是一个权衡。您可以用一些性能来换取更整洁的设计,而不要乱扔无休止的“ Brand_X”表。从您陈述的最常见的方向出发,性能损失应该最小。换种方式会更痛苦,但这就是折衷方案。 en.wikipedia.org/wiki/…–
乔纳森·菲特

4

您所描述的至少部分是产品目录。您具有所有产品共有的几个属性。这些都属于标准化表。

除此之外,您还具有一系列特定于品牌的属性(我希望可能是特定于产品的属性)。您的系统需要使用这些特定属性做什么?您是否具有依赖于这些属性的架构的业务逻辑,或者只是将它们以一系列“标签”:“值”对列出?

其他答案建议使用什么本质上是一个CSV的方法(这是否是JSONARRAY或其他) -这些方法放弃常规关系模式,通过移动架构出元数据,进入数据处理本身。

为此有一个可移植的设计模式,非常适合关系数据库。它是EAV(实体属性值)。我相信您已经在很多地方读到“ EAV是邪恶的”(的确如此)。但是,在一个特定的应用程序中,EAV的问题并不重要,那就是产品属性目录。

所有针对EAV的通常论点都不适用于产品功能目录,因为产品功能值通常仅重新输入到列表中,或者最坏的情况下重新输入到比较表中。

使用JSON列类型使您能够从数据库中强制执行任何数据约束,并将其强制到应用程序逻辑中。同样,对每个品牌使用一个属性表也有以下缺点:

  • 如果您最终拥有数百个(或更多)品牌,它的伸缩性将不佳。
  • 如果更改品牌上的允许属性,则必须更改表定义,而不仅仅是在品牌字段控制表中添加或删除行。
  • 如果该品牌具有许多潜在功能,但其中只有一小部分是已知的,您可能仍然会得到人口稀少的表格。

检索具有品牌特定功能的产品的数据并不是特别困难。可以说,使用EAV模型创建动态SQL比使用按类别表模型要容易。在每个类别的表格中,您需要进行反射(或JSON)来找出要素列的名称。然后,您可以为where子句构建项目列表。在EAV模型中,WHERE X AND Y AND Z变为INNER JOIN X INNER JOIN Y INNER JOIN Z,因此查询稍微复杂一些,但是构建查询的逻辑仍然完全是表驱动的,并且如果您构建了适当的索引,它的伸缩性将绰绰有余。

有很多原因不使用EAV作为一般方法。这些原因不适用于产品功能目录,因此在此特定应用程序中EAV没有任何问题。

可以肯定的是,这是一个复杂而有争议的话题的简短答案。我之前已经回答过类似的问题,并进一步详细介绍了对EAV的普遍反感。例如:

我要说的是,EAV最近使用的频率比以前减少了,主要是出于充分的理由。但是,我认为它也不是很了解。


3

这是我的问题:不同品牌的服装需要不同的信息。解决此类问题的最佳实践是什么?

使用JSON和PostgreSQL

我认为您正在使这项工作变得比其应有的艰巨,并且以后会被其咬住。除非您实际需要EAV,否则不需要实体属性值模型

CREATE TABLE brands (
  brand_id     serial PRIMARY KEY,
  brand_name   text,
  attributes   jsonb
);
CREATE TABLE clothes (
  clothes_id   serial        PRIMARY KEY,
  brand_id     int           NOT NULL REFERENCES brands,
  clothes_name text          NOT NULL,
  color        text,
  price        numeric(5,2)  NOT NULL
);

此架构绝对没有错。

INSERT INTO brands (brand_name, attributes)
VALUES
  ( 'Gucci', $${"luxury": true, "products": ["purses", "tawdry bougie thing"]}$$ ),
  ( 'Hugo Boss', $${"origin": "Germany", "known_for": "Designing uniforms"}$$ ),
  ( 'Louis Vuitton', $${"origin": "France", "known_for": "Designer Purses"}$$ ),
  ( 'Coco Chanel', $${"known_for": "Spying", "smells_like": "Banana", "luxury": true}$$ )
;

INSERT INTO clothes (brand_id, clothes_name, color, price) VALUES
  ( 1, 'Purse', 'orange', 100 ),
  ( 2, 'Underwear', 'Gray', 10 ),
  ( 2, 'Boxers', 'Gray', 10 ),
  ( 3, 'Purse with Roman Numbers', 'Brown', 10 ),
  ( 4, 'Spray', 'Clear', 100 )
;

现在,您可以使用简单的联接查询它

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

而且任何JSON运算符都可以在where子句中使用。

SELECT *
FROM brands
JOIN clothes
  USING (brand_id)
WHERE attributes->>'known_for' ILIKE '%Design%';

附带说明,请勿将URL放入数据库中。它们随着时间而变化。只需创建一个接受它们的函数。

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

管他呢。如果您使用的是PostgreSQL,甚至可以使用hashids

还要特别注意的jsonb是,它存储为二进制文件(因此是-'b'),并且也是可索引的,或SARGable的,或者这些天酷孩子称之为的其他东西:CREATE INDEX ON brands USING gin ( attributes );

这里的区别在于查询的简单性。

给我Brand2的所有衣服

SELECT * FROM clothes WHERE brand_id = 2;

给我所有具有以下属性的衣服:尺寸

SELECT * FROM clothes WHERE attributes ? 'size';

不一样的..

给我所有衣服的所有属性和所有衣服的属性。

SELECT * FROM clothes WHERE attributes->>'size' = 'large';

因此,如果我理解正确,那么您要说的要点是,如果品牌和属性之间存在某种关系(即,是否有效),那么将首选McNets的解决方案(但查询的成本更高/更慢)。另一方面,如果这种关系不重要/更“临时”,则可能更喜欢您的解决方案。当您说“我永远不会在PostgreSQL中使用它”时,您可以用什么意思解释一下吗?该评论似乎没有任何解释。抱歉有这些问题!!到目前为止,我非常感谢您的答复:)
youngrrrr

1
显然存在一种关系,唯一的问题是您需要管理多少。如果我使用诸如属性属性之类的模糊术语,我通常是说它是非常特殊的或高度非结构化的。为此,JSONB更好,因为它更简单。你会发现这个信息后coussej.github.io/2016/01/14/...
埃文·卡罗尔

-1

一种简单的解决方案是将所有可能的属性作为主衣服表中的列包括在内,并使所有品牌特定列可为空。该解决方案破坏了数据库的规范化,但是非常易于实现。


我认为..我对您的意思有所了解,但包括更多细节和示例可能也会有所帮助。
youngrrrr
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.