¿Cómo implementar una relación de muchos a muchos en PostgreSQL?

95

Creo que el título se explica por sí mismo. ¿Cómo se crea la estructura de la tabla en PostgreSQL para establecer una relación de varios a varios?

Mi ejemplo:

Product(name, price);
Bill(name, date, Products);
Radu Gheorghiu
fuente
2
eliminar productos de la tabla de facturas, crear una nueva tabla llamada "bill_products" con dos campos: uno apunta a productos y otro a factura. Convierta esos dos campos en la clave principal de esta nueva tabla.
Marc B
Entonces bill_products (factura, productos); ? ¿Y ambos PK?
Radu Gheorghiu
1
Si. serían individualmente un FK apuntando a sus respectivas mesas, y juntos serían el PK de la nueva mesa.
Marc B
Entonces, bill_product (el producto hace referencia a product.name, bill hace referencia a bill.name, (producto, factura) clave principal)?
Radu Gheorghiu
Señalarían cuáles serían los campos PK de las tablas Producto y Factura.
Marc B

Respuestas:

298

Las declaraciones SQL DDL (lenguaje de definición de datos) podrían verse así:

CREATE TABLE product (
  product_id serial PRIMARY KEY  -- implicit primary key constraint
, product    text NOT NULL
, price      numeric NOT NULL DEFAULT 0
);

CREATE TABLE bill (
  bill_id  serial PRIMARY KEY
, bill     text NOT NULL
, billdate date NOT NULL DEFAULT CURRENT_DATE
);

CREATE TABLE bill_product (
  bill_id    int REFERENCES bill (bill_id) ON UPDATE CASCADE ON DELETE CASCADE
, product_id int REFERENCES product (product_id) ON UPDATE CASCADE
, amount     numeric NOT NULL DEFAULT 1
, CONSTRAINT bill_product_pkey PRIMARY KEY (bill_id, product_id)  -- explicit pk
);

Hice algunos ajustes:

  • La relación n: m normalmente se implementa mediante una tabla separada, bill_producten este caso.

  • Agregué serialcolumnas como claves primarias sustitutas . En Postgres 10 o posterior, considere una IDENTITYcolumna en su lugar. Ver:

    Lo recomiendo encarecidamente, porque el nombre de un producto no es único (no es una buena "clave natural"). Además, imponer la unicidad y hacer referencia a la columna en claves externas suele ser más económico con 4 bytes integer(o incluso 8 bytes bigint) que con una cadena almacenada como texto varchar.

  • No utilice nombres de tipos de datos básicos datecomo identificadores . Si bien esto es posible, es de mal estilo y genera errores confusos y mensajes de error. Utilice identificadores legales, en minúsculas y sin comillas . Nunca use palabras reservadas y evite los identificadores de casos mixtos entre comillas dobles si puede.

  • "nombre" no es un buen nombre. Cambié el nombre de la columna de la tabla producta product( product_nameo similar). Esa es una mejor convención de nomenclatura . De lo contrario, cuando une un par de tablas en una consulta, lo que hace mucho en una base de datos relacional, termina con varias columnas llamadas "nombre" y tiene que usar alias de columna para solucionar el desorden. Eso no ayuda. Otro anti-patrón generalizado sería simplemente "id" como nombre de columna.
    No estoy seguro de cuál sería el nombre de a bill. bill_idprobablemente será suficiente en este caso.

  • pricees de tipo de datosnumeric para almacenar números fraccionarios exactamente como se ingresaron (tipo de precisión arbitraria en lugar de tipo de coma flotante). Si se ocupa exclusivamente de números enteros, hágalo integer. Por ejemplo, puede guardar los precios como centavos .

  • El amount( "Products"en su pregunta) va a la tabla de enlace bill_producty también es de tipo numeric. Nuevamente, integersi se trata exclusivamente de números enteros.

  • ¿Ves las claves externas en bill_product? He creado tanto a los cambios en cascada: ON UPDATE CASCADE. Si un product_ido bill_iddebe cambiar, el cambio se aplica en cascada a todas las entradas dependientes de bill_producty no se rompe nada. Esas son solo referencias sin significado propio.
    También utilicé ON DELETE CASCADEpara bill_id: Si se elimina una factura, sus detalles mueren con ella.
    No es así para los productos: no desea eliminar un producto que se usa en una factura. Postgres arrojará un error si intenta esto. En su lugar, agregaría otra columna para productmarcar filas obsoletas ("eliminación suave").

  • Todas las columnas de este ejemplo básico terminan siendo NOT NULL, por NULLlo que no se permiten valores. (Sí, todas las columnas; las columnas de clave principal se definen UNIQUE NOT NULLautomáticamente). Esto se debe a que los NULLvalores no tendrían sentido en ninguna de las columnas. Facilita la vida de un principiante. Pero no se escapará tan fácilmente, debe comprender el NULLmanejo de todos modos. Las columnas adicionales pueden permitir NULLvalores, las funciones y las combinaciones pueden introducir NULLvalores en consultas, etc.

  • Lea el capítulo sobre CREATE TABLEen el manual .

  • Las claves primarias se implementan con un índice único en las columnas de claves, que agiliza las consultas con condiciones en la (s) columna (s) PK. Sin embargo, la secuencia de columnas clave es relevante en claves multicolumna. Dado que el PK bill_productestá activado (bill_id, product_id)en mi ejemplo, es posible que desee agregar otro índice en solo product_ido (product_id, bill_id)si tiene consultas que buscan un dado product_idy no bill_id. Ver:

  • Lea el capítulo sobre índices en el manual .

Erwin Brandstetter
fuente
¿Cómo puedo crear un índice para la tabla de mapeo bill_product? Normalmente se debe el siguiente aspecto: CREATE INDEX idx_bill_product_id ON booked_rates(bill_id, product_id). ¿Es esto correcto?
codyLine
1
@codyLine: el PK crea automáticamente este índice.
Erwin Brandstetter
1
@ErwinBrandstetter: ¿No debería crearse un índice en bill_product para la columna product_id?
Christian
2
@ ChristianB.Almeida: Eso es útil en muchos casos, sí. Agregué un poco sobre indexación.
Erwin Brandstetter
1
@Jakov: Solo hay una fila para cada billete en la tabla bill. Necesitamos la cantidad por artículo agregado en bill_product.
Erwin Brandstetter