¿El mejor diseño para hacer referencia a varias tablas de una sola columna?

18

Esquema propuesto

En primer lugar, aquí hay un ejemplo de mi esquema propuesto para hacer referencia a lo largo de mi publicación:

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

Planteamiento del problema

Tengo una mesa de ropa que tiene columnas como nombre, color, precio, brandid, etc. para describir los atributos de una prenda de ropa en particular.

Aquí está mi problema: las diferentes marcas de ropa requieren información diferente. ¿Cuál es la mejor práctica para tratar un problema como este?

Tenga en cuenta que para mis propósitos, es necesario encontrar información específica de la marca a partir de una entrada de ropa . Esto se debe a que primero muestro la información de una entrada de ropa al usuario, después de lo cual debo usar su información específica de la marca para comprar el artículo. En resumen, tiene que haber una relación direccional entre la ropa (de) y las tablas brand_x .

Solución propuesta / actual

Para hacer frente a esto, he pensado en el siguiente esquema de diseño:

La tabla de ropa tendrá una columna de marca que puede tener valores de identificación que varían de 1 a x, donde una identificación particular corresponde a una tabla específica de la marca. Por ejemplo, el valor de identificación 1 corresponderá a la tabla marca_1 (que podría tener una columna de URL ), la identificación 2 corresponderá a la marca_2 (que podría tener una columna de proveedor ), etc.

Por lo tanto, para asociar una entrada de ropa en particular con su información específica de la marca, imagino que la lógica a nivel de aplicación se verá más o menos así:

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.

Otros comentarios y pensamientos

Estoy intentando normalizar toda mi base de datos en BCNF, y aunque esto es lo que se me ocurrió, el código de aplicación resultante me hace sentir muy ansioso. No hay forma de hacer cumplir las relaciones, excepto a nivel de aplicación, y por lo tanto el diseño se siente muy hacky y, anticipo, muy propenso a errores.

Investigación

Me aseguré de mirar las entradas anteriores antes de hacer una publicación. Aquí hay una publicación con un problema casi idéntico que logré encontrar. Hice esta publicación de todos modos porque parece que la única respuesta proporcionada no tiene una solución SQL o basada en diseño (es decir, menciona OOP, herencia e interfaces).

También soy un poco novato cuando se trata de diseño de bases de datos, por lo que agradecería cualquier idea.


Parece que hay respuestas más útiles en Stack Overflow:

Me he referido a las soluciones allí y sugiero que otros que encuentren mi pregunta también lo hagan.

A pesar de los enlaces proporcionados anteriormente, todavía estoy buscando respuestas aquí y agradecería cualquier solución provista.

Estoy usando PostgreSQL.

youngrrrr
fuente

Respuestas:

7

Personalmente no me gusta usar un esquema de tablas múltiples para este propósito.

  • Es difícil garantizar la integridad.
  • Es difícil de mantener.
  • Es difícil filtrar resultados.

He configurado una muestra de dbfiddle .

Mi esquema de tabla propuesto:

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

Déjame insertar algunos datos:

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

Si necesita buscar atributos comunes:

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

O puede obtener fácilmente ropa por marca:

Dame toda la ropa de 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

Pero para mí, uno de los mejores de este esquema es que puedes filtrar por Attibutes:

Dame toda la ropa que tenga el atributo: talla

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

El uso de un esquema de varias tablas, cualquiera de las consultas anteriores, requerirá lidiar con un número ilimitado de tablas o con campos XML o JSON.

Otra opción con este esquema es que puede definir plantillas, por ejemplo, puede agregar una nueva tabla BrandAttrTemplates. Cada vez que agregue un nuevo registro, puede usar un activador o un SP para generar un conjunto de atributos predefinidos para esta Rama.

Lo siento, me gustaría extender mis explicaciones porque creo que es más claro que mi inglés.

Actualizar

Mi respuesta actual debería funcionar sin importar qué RDBMS. Según sus comentarios, si necesita filtrar los valores de los atributos, sugeriría pequeños cambios.

En la medida en que MS-Sql no permite matrices, he configurado una nueva muestra que mantiene el mismo esquema de tabla, pero cambiando AttrValue a un tipo de campo ARRAY.

De hecho, utilizando POSTGRES, puede aprovechar esta matriz utilizando un índice GIN.

(Permítanme decir que @EvanCarrol tiene un buen conocimiento sobre Postgres, ciertamente mejor que yo. Pero permítanme agregar mi parte).

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

Ahora, puede consultar adicionalmente utilizando valores de atributos individuales como:

Dame una lista de todos los pantalones Talla: 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 es el resultado:

clothes name | brand name | attribute | values 
------------- ------------ ----------  ---------------- 
Pants          Brand1       Size        32, 33, 34
McNets
fuente
Realmente me gusta esta explicación, pero parece que solo estamos intercambiando un esquema de varias tablas por tener esos CSV múltiples en una sola columna, si eso tiene sentido. Por otro lado, siento que me gusta más este enfoque porque no requiere cambios en el esquema, pero nuevamente parece que estamos empujando el problema a otra parte (es decir, al tener columnas de longitud variable). Esto puede ser un problema; ¿Qué pasa si quisiera consultar pantalones de talla 3 en el DB? Tal vez no haya una solución agradable y limpia para este tipo de problema. ¿Hay un nombre para este concepto para que pueda examinarlo más?
youngrrrr
En realidad ... para responder al problema que planteé, quizás la respuesta se pueda tomar prestada de la solución de @ EvanCarroll: es decir, mediante el uso de tipos jsonb en lugar de simplemente TEXT / STRINGS en formato CSV. Pero de nuevo, si hay un nombre para este concepto, ¡hágamelo saber!
youngrrrr
1
Es un tipo de solución de valor de atributo de entidad. No es un mal compromiso entre rendimiento y buen diseño. Sin embargo, es una compensación. Cambia algo de rendimiento por un diseño más limpio, no lleno de interminables tablas "Brand_X". La penalización de rendimiento, yendo desde la dirección más común indicada, debe ser mínima. Ir hacia el otro lado será más doloroso, pero ese es el compromiso. en.wikipedia.org/wiki/…
Jonathan Fite
4

Lo que está describiendo es, al menos en parte, un catálogo de productos. Tiene varios atributos que son comunes a todos los productos. Estos pertenecen a una tabla bien normalizada.

Más allá de eso, tiene una serie de atributos que son específicos de la marca (y espero que puedan ser específicos del producto). ¿Qué necesita hacer su sistema con estos atributos específicos? ¿Tiene una lógica de negocios que depende del esquema de estos atributos o simplemente los enumera en una serie de pares de "etiquetas": "valores"?

Otras respuestas están sugiriendo utilizando lo que es esencialmente un enfoque CSV (si esto es JSONo ARRAYo de otra manera) - Estos enfoques forego manejo moviendo el esquema de metadatos y en los propios datos de esquema relacional regular.

Hay un patrón de diseño portátil para esto que se adapta muy bien a las bases de datos relacionales. Es EAV (entidad-atributo-valor). Estoy seguro de que has leído en muchos, muchos lugares que "EAV es malvado" (y lo es). Sin embargo, hay una aplicación en particular en la que los problemas con EAV no son importantes, y son los catálogos de atributos del producto.

Todos los argumentos habituales contra EAV no se aplican a un catálogo de características del producto, ya que los valores de las características del producto generalmente solo se regurgitan en una lista o, en el peor de los casos, en una tabla de comparación.

El uso de un JSONtipo de columna elimina su capacidad de aplicar restricciones de datos fuera de la base de datos y la fuerza a la lógica de su aplicación. Además, el uso de una tabla de atributos para cada marca tiene las siguientes desventajas:

  • No escala bien si finalmente tiene cientos de marcas (o más).
  • Si cambia los atributos permitidos en una marca, debe cambiar la definición de una tabla en lugar de simplemente agregar o eliminar filas en una tabla de control de campo de marca.
  • Aún puede terminar con tablas escasamente pobladas si la marca tiene muchas características potenciales, de las cuales solo se conoce un pequeño subconjunto.

No es especialmente difícil recuperar datos sobre un producto con características específicas de la marca. Podría decirse que es más fácil crear un SQL dinámico con el modelo EAV que con el modelo de tabla por categoría. En la tabla por categoría, necesita reflexión (o su JSON) para descubrir cuáles son los nombres de las columnas de características. Luego puede crear una lista de elementos para una cláusula where. En el modelo EAV, se WHERE X AND Y AND Zconvierte en INNER JOIN X INNER JOIN Y INNER JOIN Z, por lo que la consulta es un poco más complicada, pero la lógica para construir la consulta todavía está totalmente basada en tablas y será más que escalable si tiene los índices adecuados creados.

Hay muchas razones para no usar EAV como un enfoque general. Esas razones no se aplican a un catálogo de características del producto, por lo que no hay nada malo con EAV en esta aplicación específica.

Sin duda, esta es una respuesta corta para un tema complejo y controvertido. He respondido preguntas similares antes y he entrado en más detalles sobre la aversión general a EAV. Por ejemplo:

Yo diría que EAV se usa con menos frecuencia últimamente de lo que solía ser, en su mayoría por buenas razones. Sin embargo, creo que tampoco se entiende bien.

Joel Brown
fuente
3

Aquí está mi problema: las diferentes marcas de ropa requieren información diferente. ¿Cuál es la mejor práctica para tratar un problema como este?

Usando JSON y PostgreSQL

Creo que lo estás haciendo más difícil de lo que debe ser y te morderán más tarde. No necesita el modelo Entidad-atributo-valor a menos que realmente necesite 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
);

No hay absolutamente nada de malo en este 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 )
;

Ahora puedes consultarlo usando una combinación simple

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

Y cualquiera de los operadores JSON trabaja en una cláusula where.

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

Como nota al margen, no ponga las URL en la base de datos. Cambian con el tiempo. Simplemente cree una función que los tome.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

o lo que sea. Si está utilizando PostgreSQL, incluso puede usar hashids .

También de nota especial, jsonbse almacena como binario (por lo tanto, la 'b') y también es indexable, o SARGable o como sea que los niños geniales lo llamen en estos días:CREATE INDEX ON brands USING gin ( attributes );

La diferencia aquí está en la simplicidad de la consulta.

Dame toda la ropa de Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Dame toda la ropa que tenga el atributo: talla

SELECT * FROM clothes WHERE attributes ? 'size';

¿Qué tal una diferente?

Dame toda la ropa y atributos para cualquier ropa disponible en grande.

SELECT * FROM clothes WHERE attributes->>'size' = 'large';
Evan Carroll
fuente
Entonces, si entiendo correctamente, la esencia de lo que dijo es si hay una relación entre marcas y atributos (es decir, si es válida o no), entonces se preferiría la solución de McNets (pero las consultas serían más costosas / más lentas). Por otro lado, si esta relación no es importante / más "ad-hoc", entonces uno podría preferir su solución. ¿Puedes explicar un poco más a qué te referías cuando dijiste "aunque nunca lo usaría con PostgreSQL?" No parecía haber una explicación para ese comentario. ¡¡Lo siento por todas las preguntas!! Realmente aprecio sus respuestas hasta ahora :)
youngrrrr
1
Es evidente que existe una relación, la única pregunta es cuánto necesita administrarla. Si estoy usando un término vago como propiedades , atributos o similares, generalmente quiero decir que es bastante ad-hoc o altamente desestructurado. Para eso, JSONB es simplemente mejor porque es más simple. puede encontrar esta publicación informativa coussej.github.io/2016/01/14/…
Evan Carroll
-1

Una solución fácil es incluir todos los atributos posibles como columnas en la mesa principal de ropa y hacer que todas las columnas específicas de la marca puedan ser anuladas. Esta solución rompe la normalización de la base de datos, pero es muy fácil de implementar.

Matthew Sontum
fuente
Creo que ... tengo una idea de lo que estás diciendo, pero puede ser útil incluir más detalles y quizás también un ejemplo.
youngrrrr