web-dev-qa-db-ja.com

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

提案されたスキーマ

まず第一に、これは私の投稿全体を通して参照するために提案されたスキーマの例です:

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

問題文

clothes テーブルがあり、特定のアイテムの属性を説明するname、color、price、brandidなどの列があります。衣類の。

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

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

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

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

clothes テーブルには、brand列があり、1〜xの範囲のID値を持つことができます。特定のIDはブランドに対応します固有のテーブル。たとえば、ID値1はテーブル brand_1 url列がある場合があります)に対応し、ID 2は brand_2 supplier列がある場合があります)など.

したがって、特定の服のエントリをブランド固有の情報に関連付けるために、アプリケーションレベルのロジックは次のようになると思います。

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を使用しています。

18
youngrrrr

私は個人的には、この目的でマルチテーブルスキーマを使用することを好みません。

  • 完全性を保証することは困難です。
  • 維持するのは難しいです。
  • 結果をフィルタリングすることは困難です。

私はdbfiddle sample を設定しました。

私の提案されたテーブルスキーマ:

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つは、属性でフィルタリングできることです。

次の属性を持つすべての服をください:サイズ

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が配列を許可しない限り、同じテーブルスキーマを保持する 新しいサンプル を設定しましたが、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
7
McNets

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

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

他の答えは、本質的にCSVアプローチ(これがJSONまたはARRAYであるかどうかにかかわらず)を使用することを示唆しています-これらのアプローチは、メタデータからデータにスキーマを移動することにより、通常のリレーショナルスキーマ処理を回避します自体。

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

通常、EAVに対する通常の引数はすべて製品機能カタログには適用されません。これは、製品機能の値は通常、リストまたは最悪の場合の比較表にのみ逆流されるためです。

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

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

ブランド固有の機能を持つ製品に関するデータを取得することは特に難しくありません。 EAVモデルを使用して動的SQLを作成する方が、カテゴリーごとのテーブルモデルを使用するよりも間違いなく簡単です。カテゴリごとのテーブルでは、機能の列名が何であるかを調べるためにリフレクション(またはJSON)が必要です。次に、where句の項目のリストを作成できます。 EAVモデルでは、WHERE X AND Y AND ZINNER JOIN X INNER JOIN Y INNER JOIN Zになるため、クエリは少し複雑になりますが、クエリを構築するロジックは完全にテーブル駆動であり、次の場合は十分にスケーラブルです。適切なインデックスを作成します。

EAVを一般的なアプローチとして使用しない理由はたくさんあります。これらの理由は製品機能カタログには当てはまらないため、この特定のアプリケーションのEAVには何の問題もありません。

確かに、これは複雑で議論の多いトピックに対する短い答えです。私は以前に同様の質問に答え、EAVへの一般的な嫌悪についてさらに詳しく説明しました。例えば:

EAVは最近の使用頻度が以前よりも少ないことが主な理由です。しかし、それもよくわかっていないと思います。

4
Joel Brown

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

JSONとPostgreSQLの使用

私はあなたが必要以上にこれを難しくしていると思います、そしてあなたは後でそれに噛まれるでしょう。実際にEAVが必要でない限り、 Entity–attribute–value model は必要ありません。

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';
3
Evan Carroll