Melhor design para referenciar várias tabelas da coluna única?

18

Esquema proposto

Primeiro e acima de tudo, aqui está um exemplo do meu esquema proposto para referência em toda a minha postagem:

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

Declaração do problema

Eu tenho uma mesa de roupas com colunas como nome, cor, preço, marca e assim por diante para descrever os atributos de uma determinada peça de roupa.

Aqui está o meu problema: marcas diferentes de roupas exigem informações diferentes. Qual é a melhor prática para lidar com um problema como esse?

Observe que, para meus propósitos, é necessário encontrar informações específicas da marca a partir de uma entrada de roupas . Isso ocorre porque eu exibo primeiro as informações de uma entrada de roupas para o usuário, após o qual devo usar as informações específicas da marca para comprar o item. Em resumo, deve haver uma relação direcional entre roupas (de) e as tabelas brand_x .

Solução proposta / atual

Para lidar com isso, pensei no seguinte esquema de design:

A tabela de roupas terá uma coluna de marca que pode ter valores de ID que variam de 1 a x, onde um ID específico corresponde a uma tabela específica de marca. Por exemplo, o valor do ID 1 corresponderá à tabela brand_1 (que pode ter uma coluna de URL ), o id 2 corresponderá à marca_2 (que pode ter uma coluna de fornecedor ) etc.

Assim, para associar uma entrada de roupas específica a suas informações específicas da marca, imagino que a lógica no nível do aplicativo seja algo parecido com isto:

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.

Outros comentários e pensamentos

Estou tentando normalizar meu banco de dados inteiro no BCNF e, embora tenha sido o que eu criei, o código do aplicativo resultante me deixa muito ansioso. Não há como impor relações, exceto no nível do aplicativo, e, portanto, o design parece muito invasivo e, antecipadamente, muito suscetível a erros.

Pesquisa

Fiz questão de examinar as entradas anteriores antes de fazer uma postagem. Aqui está um post com um problema quase idêntico que eu consegui encontrar. Fiz este post de qualquer maneira, porque parece que a única resposta fornecida não tem uma solução baseada em design ou SQL (isto é, menciona OOP, herança e interfaces).

Também sou um novato no que diz respeito ao design de banco de dados e, por isso, gostaria de receber informações.


Parece que há respostas mais úteis no Stack Overflow:

Mencionei as soluções lá e sugiro que outras pessoas também encontrem minha pergunta.

Apesar dos links fornecidos acima, ainda estou à procura de respostas aqui e agradeceria qualquer solução fornecida!

Estou usando o PostgreSQL.

youngrrrr
fonte

Respostas:

7

Pessoalmente, não gosto de usar um esquema de várias tabelas para esse fim.

  • É difícil garantir a integridade.
  • É difícil de manter.
  • É difícil filtrar resultados.

Eu configurei uma amostra do dbfiddle .

Meu esquema de tabela proposto:

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)
);

Deixe-me inserir alguns dados:

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');

Se você precisar buscar atributos comuns:

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

Ou você pode facilmente obter roupas por marca:

Dê-me todas as roupas da 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

Mas para mim, um dos melhores desse esquema é que você pode filtrar por Attibutes:

Dê-me todas as roupas que têm o atributo: Tamanho

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

Usar um esquema de várias tabelas, independentemente das consultas anteriores, será necessário para lidar com um número ilimitado de tabelas ou com campos XML ou JSON.

Outra opção com esse esquema é que você pode definir modelos, por exemplo, você pode adicionar uma nova tabela BrandAttrTemplates. Toda vez que você adiciona um novo registro, pode usar um gatilho ou um SP para gerar um conjunto de atributos predefinidos para esta ramificação.

Sinto muito, gostaria de estender minhas explicações, pois acho que é mais claro que meu inglês.

Atualizar

Minha resposta atual deve funcionar independentemente do RDBMS. De acordo com seus comentários, se você precisar filtrar os valores dos atributos, sugiro pequenas alterações.

Na medida em que o MS-Sql não permite matrizes, configurei um novo exemplo mantendo o mesmo esquema de tabela, mas alterando AttrValue para um tipo de campo ARRAY.

De fato, usando POSTGRES, você pode tirar proveito dessa matriz usando um índice GIN.

(Deixe-me dizer que o @EvanCarrol tem um bom conhecimento sobre o Postgres, certamente melhor do que eu. Mas deixe-me acrescentar um pouco.)

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}');

Agora, você pode consultar adicionalmente usando valores de atributos individuais como:

Dê-me uma lista de todas as calças Tamanho: 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;

Este é o resultado:

clothes name | brand name | attribute | values 
------------- ------------ ----------  ---------------- 
Pants          Brand1       Size        32, 33, 34
McNets
fonte
Eu realmente gosto dessa explicação, mas parece que estamos apenas trocando um esquema de várias tabelas por ter esses vários CSVs em uma única coluna - se isso fizer sentido. Por outro lado, sinto que gosto mais dessa abordagem porque ela não requer alterações no esquema, mas novamente parece que estamos empurrando o problema para outro lugar (ou seja, por ter colunas de comprimento variável). Isso pode ser um problema; e se eu quisesse consultar calças de tamanho 3 no banco de dados? Talvez não exista uma solução agradável e limpa para esse tipo de problema. Existe um nome para esse conceito, para que eu possa investigar mais?
youngrrrr
Na verdade ... para responder ao problema que eu coloquei, talvez a resposta possa ser emprestada da solução da @ EvanCarroll: ou seja, usando tipos jsonb em vez de simplesmente TEXT / STRINGS no formato CSV. Mas, novamente - se houver um nome para esse conceito, entre em contato!
youngrrrr
1
É um tipo de solução de valor de atributo de entidade. Não é um mau compromisso entre desempenho e bom design. É uma troca, no entanto. Você troca algum desempenho por um design mais limpo, não repleto de infinitas tabelas "Brand_X". A penalidade de desempenho, seguindo a direção mais comum declarada, deve ser mínima. Indo para o outro lado será mais doloroso, mas esse é o compromisso. en.wikipedia.org/wiki/…
Jonathan Fite
4

O que você está descrevendo é, pelo menos em parte, um catálogo de produtos. Você tem vários atributos comuns a todos os produtos. Eles pertencem a uma tabela bem normalizada.

Além disso, você tem uma série de atributos específicos da marca (e eu espero que possam ser específicos do produto). O que seu sistema precisa fazer com esses atributos específicos? Você tem lógica de negócios que depende do esquema desses atributos ou apenas os lista em uma série de pares "label": "value"?

Outras respostas sugerem usar o que é essencialmente uma abordagem CSV (seja essa JSONouARRAY ou não) - estas abordagens, renunciar esquema relacional regulares manipulação movendo o esquema de metadados e para os dados em si.

Existe um padrão de design portátil para isso, que se encaixa muito bem nos bancos de dados relacionais. É EAV (entidade-atributo-valor). Tenho certeza que você leu em muitos lugares que "EAV é o mal" (e é). No entanto, há um aplicativo específico em que os problemas com o EAV não são importantes e são os catálogos de atributos do produto.

Todos os argumentos usuais contra o EAV não se aplicam a um catálogo de recursos do produto, pois os valores dos recursos do produto geralmente são regurgitados apenas em uma lista ou, na pior das hipóteses, em uma tabela de comparação.

O uso de um JSONtipo de coluna exige que você imponha restrições de dados fora do banco de dados e o force à lógica do aplicativo. Além disso, o uso de uma tabela de atributos para cada marca tem as seguintes desvantagens:

  • Não é bem dimensionável se você tiver centenas de marcas (ou mais).
  • Se você alterar os atributos permitidos em uma marca, precisará alterar uma definição de tabela em vez de apenas adicionar ou remover linhas em uma tabela de controle de campo da marca.
  • Você ainda pode acabar com tabelas pouco preenchidas se a marca tiver muitos recursos em potencial, dos quais apenas um pequeno subconjunto é conhecido.

Não é especialmente difícil recuperar dados sobre um produto com recursos específicos da marca. É indiscutivelmente mais fácil criar um SQL dinâmico usando o modelo EAV do que seria usando o modelo de tabela por categoria. Na tabela por categoria, você precisa de reflexão (ou sua JSON) para descobrir quais são os nomes das colunas dos recursos. Em seguida, você pode criar uma lista de itens para uma cláusula where. No modelo EAV, WHERE X AND Y AND Ztorna - se INNER JOIN X INNER JOIN Y INNER JOIN Z, portanto, a consulta é um pouco mais complicada, mas a lógica para criar a consulta ainda é totalmente orientada por tabela e será mais do que escalável o suficiente se você tiver os índices adequados criados.

Existem várias razões para não usar o EAV como uma abordagem geral. Esses motivos não se aplicam a um catálogo de recursos do produto, portanto não há nada errado com o EAV neste aplicativo específico.

Certamente, esta é uma resposta curta para um tópico complexo e controverso. Eu já respondi perguntas semelhantes antes e entrei em mais detalhes sobre a aversão geral ao EAV. Por exemplo:

Eu diria que o EAV é usado com menos frequência ultimamente do que costumava ser, principalmente por boas razões. No entanto, acho que também não é bem compreendido.

Joel Brown
fonte
3

Aqui está o meu problema: marcas diferentes de roupas exigem informações diferentes. Qual é a melhor prática para lidar com um problema como esse?

Usando JSON e PostgreSQL

Eu acho que você está fazendo isso mais difícil do que precisa e você será mordido mais tarde. Você não precisa do modelo de entidade-atributo-valor, a menos que realmente precise do 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
);

Não há absolutamente nada de errado com esse esquema.

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 )
;

Agora você pode consultá-lo usando uma associação simples

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

E qualquer um dos operadores JSON trabalha em uma cláusula where.

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

Como uma observação lateral, não coloque os URLs no banco de dados. Eles mudam com o tempo. Simplesmente crie uma função que os aceite.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

como queiras. Se você estiver usando o PostgreSQL, pode até usar hashids .

Também de nota especial, jsonb digno é armazenado como binário (portanto, o 'b') e também pode ser indexado, ou SARGable ou qualquer outra coisa que as crianças legais estejam chamando nos dias de hoje:CREATE INDEX ON brands USING gin ( attributes );

A diferença aqui está na simplicidade da consulta ..

Dê-me todas as roupas da Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Dê-me todas as roupas que têm o atributo: Tamanho

SELECT * FROM clothes WHERE attributes ? 'size';

Que tal um diferente ..

Dê-me todas as roupas e atributos para todas as roupas disponíveis em tamanho grande.

SELECT * FROM clothes WHERE attributes->>'size' = 'large';
Evan Carroll
fonte
Portanto, se eu entendi direito, a essência do que você disse é que existe uma relação entre marcas e atributos (isto é, se é válido ou não), então a solução da McNets seria a preferida (mas as consultas seriam mais caras / lentas). Por outro lado, se esse relacionamento não for importante / mais "ad-hoc", pode-se preferir sua solução. Você pode explicar um pouco mais o que quis dizer quando disse "eu nunca o usaria com o PostgreSQL?" Não parecia haver uma explicação para esse comentário. Desculpe por todas as perguntas !! Eu realmente aprecio as suas respostas até agora :)
youngrrrr
1
Existe claramente um relacionamento, a única questão é quanto você precisa para gerenciá-lo. Se eu estiver usando um termo vago, como propriedades , atributos ou algo semelhante, geralmente quero dizer que é praticamente ad-hoc ou altamente desestruturado. Por isso, o JSONB é apenas melhor porque é mais simples. você pode encontrar este post informativo coussej.github.io/2016/01/14/…
Evan Carroll
-1

Uma solução fácil é incluir todos os atributos possíveis como colunas na tabela de roupas principal e tornar todas as colunas específicas da marca anuláveis. Essa solução interrompe a normalização do banco de dados, mas é muito fácil de implementar.

Matthew Sontum
fonte
Eu acho .. Eu tenho uma idéia do que você está dizendo, mas pode ser útil incluir mais detalhes e talvez um exemplo também.
youngrrrr