単一の列から複数のテーブルを参照するのに最適な設計ですか?


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

問題文

名前、色、価格、ブランドIDなどの列を含む衣服テーブルがあり、特定の衣服アイテムの属性を記述しています。

ここに私の問題があります:異なるブランドの服には異なる情報が必要です。このような問題に対処するためのベストプラクティスは何ですか?

私の目的のために、衣服のエントリーから始まるブランド固有の情報を見つける必要があることに注意してください。これは、最初に衣服のエントリからの情報をユーザーに表示した後、そのブランド固有の情報を使用してアイテムを購入する必要があるためです。要約すると、衣服(from)とbrand_xテーブルの間には方向関係がなければなりません。

提案/現在のソリューション

これに対処するために、次の設計スキームを考えました。

衣服のテーブルがありますブランドのブランド、特定のテーブルに1からXまでの範囲のID値を有していてもよいカラム、特定のID対応します。たとえば、id値1はテーブルbrand_1url列がある場合があります)に対応し、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でデータベース全体を正規化しようとしていますが、これが思いついたことですが、結果のアプリケーションコードは非常に不安に感じます。アプリケーションレベルを除いて関係を強制する方法はないため、デザインは非常にハック感があり、非常にエラーが発生しやすいと予想されます。

研究

投稿する前に、以前のエントリを必ず確認してください。これは、私が見つけたほぼ同一の問題のある投稿です。とにかく、この投稿をしたのは、提供された唯一の答えにSQLまたは設計ベースのソリューションがない(つまり、OOP、継承、およびインターフェイスに言及している)ようだからです。

データベースの設計に関しては初心者でもありますので、洞察をいただければ幸いです。


Stack Overflowにはさらに役立つ回答があるようです。

私はそこで解決策を参照しましたが、私の質問を見つけた他の人も同様にそうすることを提案します。

上記のリンクにもかかわらず、私はまだここで回答を探しており、提供されている解決策に感謝します!

PostgreSQLを使用しています。

回答:


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

しかし、私にとって、このスキーマの最高の1つは、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フィールドを処理する必要があります。

このスキーマのもう1つのオプションは、テンプレートを定義できることです。たとえば、新しいテーブルBrandAttrTemplatesを追加できます。新しいレコードを追加するたびに、トリガーまたはSPを使用して、このブランチの一連の定義済み属性を生成できます。

ごめんなさい、英語よりもわかりやすいと思うので説明を広げたいと思います。

更新

私の現在の答えは、どのRDBMSでも機能するはずです。あなたのコメントによると、属性値をフィルタリングする必要がある場合は、小さな変更をお勧めします。

MS-Sqlが配列を許可しない限り、新しいサンプルをセットアップしました 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を含めるためにマルチテーブルスキーマをトレードオフしているように思えます-それが理にかなっている場合。一方、スキーマを変更する必要がないため、このアプローチの方が好きだと感じていますが、ここでも問題を他の場所にプッシュしているように感じます(つまり、可変長の列を使用することによって)。これは問題になる可能性があります。DBでサイズ3のズボンを照会する場合はどうなりますか?たぶん、この種の問題に対するすてきできれいな解決策はないでしょう。この概念の名前はありますか?
youngrrrr

実際に...私が提起した問題に答えるために、おそらく答えは@EvanCarrollのソリューションから借りることができます:すなわち、CSV形式の単なるTEXT / STRINGSの代わりにjsonb型を使用することによって。繰り返しますが、このコンセプトに名前があれば、教えてください!
youngrrrr

1
これは、エンティティ属性値タイプのソリューションです。パフォーマンスと優れたデザインの間の妥協点ではありません。ただし、これはトレードオフです。無限の「Brand_X」テーブルが散らばらないように、パフォーマンスをよりクリーンなデザインと引き換えます。述べられている最も一般的な方向からのパフォーマンスのペナルティは最小限でなければなりません。他の方法で進むことはより苦痛になりますが、それは妥協です。 en.wikipedia.org/wiki/...
ジョナサンFITE

4

説明しているのは、少なくとも部分的には製品カタログです。すべての製品に共通するいくつかの属性があります。これらは、適切に正規化されたテーブルに属します。

それ以外にも、ブランド固有の一連の属性があります(製品固有の属性も考えられます)。これらの特定の属性を使用して、システムは何をする必要がありますか?これらの属性のスキーマに依存するビジネスロジックがありますか、それとも一連の「ラベル」:「値」のペアでそれらをリストしていますか?

他の回答には、本質的にCSV形式のアプローチであるものを使用して示唆している(これがあるかどうJSONARRAYまたはそうでなければ) -これらは、メタデータのうち、データ自体にスキーマを移動させることにより、定期的なリレーショナルスキーマ処理放棄近づきます。

これには、リレーショナルデータベースに非常によく適合するポータブルなデザインパターンがあります。それはEAV(エンティティ属性値)です。「EAV is Evil」(そしてそれは)という多くの場所であなたが読んだことは間違いないでしょう。ただし、EAVの問題が重要ではない特定のアプリケーションが1つあります。それは製品属性カタログです。

製品の機能の値は一般にリストに、または最悪の場合は比較テーブルに逆流されるだけなので、EAVに対する通常の引数はすべて製品の機能カタログには適用されません。

JSON列タイプを使用すると、データベースからデータ制約を強制し、アプリケーションロジックに強制することができます。また、ブランドごとに1つの属性テーブルを使用すると、次の欠点があります。

  • 最終的に数百(またはそれ以上)のブランドを持っている場合、うまく拡張できません。
  • ブランドの許容属性を変更する場合、ブランドフィールドコントロールテーブルの行を追加または削除するだけでなく、テーブル定義を変更する必要があります。
  • ブランドに多くの潜在的な機能があり、そのうちのごく一部しか知られていない場合は、表がまばらに残る可能性があります。

ブランド固有の機能を備えた製品に関するデータを取得することは特に難しくありません。カテゴリごとのテーブルモデルを使用するよりも、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 ')として保存され、インデックス可能な、またはSARGまたはその他のクールな子供たちが最近呼んでいるものでもあります: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

1つの簡単な解決策は、可能なすべての属性をメインの衣服テーブルの列として含め、ブランド固有の列をすべてヌル可能にすることです。このソリューションはデータベースの正規化を壊しますが、実装は非常に簡単です。


私はあなたが言っていることのアイデアを持っていると思いますが、より多くの詳細とおそらく例を含めることは役に立つかもしれません。
youngrrrr
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.