Skip to main content

Tablas puente en relaciones N:N

Qué es una relación N-a-N

Una relación N-a-N significa que:

  • Un registro de la tabla A puede relacionarse con muchos registros de la tabla B.
  • Y un registro de la tabla B puede relacionarse con muchos registros de la tabla A.

Ejemplos comunes:

  • Un alumno puede matricularse en muchos cursos, y un curso tener muchos alumnos.
  • Un pedido puede incluir muchos productos, y un producto estar en muchos pedidos.
  • Un usuario puede tener varios roles, y un rol pertenecer a varios usuarios.

Para representar esto no podemos usar una FK directa como en 1-a-N.

Necesitamos una tabla puente (o tabla intermedia).

Tabla puente: estructura base

Supón esta relación:

ALUMNO (N) ⟷ (N) CURSO

La forma correcta de representarla es:

ALUMNO
|
| 1-a-N
v
ALUMNO_CURSO (tabla puente)
^
| N-a-1
|
CURSO

Ejemplo DBML. Puedes probarlo en drawDB.app

Table alumno {
id_alumno int [pk, increment]
nombre varchar(100) [not null]
}

Table curso {
id_curso int [pk, increment]
titulo varchar(100) [not null]
}

Table alumno_curso {
id_alumno int [not null]
id_curso int [not null]
fecha_matricula date [not null, default: `CURRENT_DATE`]

indexes {
(id_alumno, id_curso) [pk]
}
}

Ref: alumno_curso.id_alumno > alumno.id_alumno
Ref: alumno_curso.id_curso > curso.id_curso

La tabla alumno_curso:

  • Tiene dos claves foráneas.
  • La clave primaria compuesta (id_alumno, id_curso) garantiza que un alumno no pueda matricularse dos veces en el mismo curso.
  • fecha_matricula es un atributo propio de la relación.

Atributos en la relación

En 1-a-N, los atributos suelen ir en la tabla hija. En N-a-N, los atributos pertenecen a la relación misma y por eso van en la tabla puente.

Ejemplos:

  • Fecha de matrícula en curso.
  • Cantidad de productos en un pedido.
  • Rol asignado a un usuario.
  • Precio aplicado en el momento de la venta.

Importante: no pongas esos datos en las tablas principales, porque pueden repetirse muchas veces con valores distintos.

Ejemplo con productos y pedidos:

Table producto {
id_producto int [pk, increment]
nombre varchar(100)
}

Table pedido {
id_pedido int [pk, increment]
fecha date [not null]
}

Table pedido_producto {
id_pedido int [not null]
id_producto int [not null]
cantidad int [not null] // CHECK (cantidad > 0) no se representa en DBML, pero lo anoto aquí
precio_unitario numeric(10,2) [not null]

indexes {
(id_pedido, id_producto) [pk]
}
}

Ref: pedido_producto.id_pedido > pedido.id_pedido
Ref: pedido_producto.id_producto > producto.id_producto

cantidad y precio_unitario son propios de la relación pedido-producto, no del producto ni del pedido por separado.

Claves compuestas: por qué son importantes

La clave primaria compuesta (FK A + FK B) evita duplicados sin necesidad de un ID artificial:

  • Garantiza que un alumno no se inscriba dos veces en el mismo curso.
  • Que un producto no aparezca dos veces en el mismo pedido.
  • Que un rol no se asigne dos veces al mismo usuario.

También puedes usar una PK artificial (ej. id SERIAL) si:

  • La tabla puente tiene muchos atributos adicionales,
  • O si vas a referenciar la relación desde otras tablas.

Pero en muchos casos, la PK compuesta es más natural y eficiente.

Ejemplo práctico — usuarios y roles

Supongamos que tenemos autenticación:

  • Tabla usuario
  • Tabla rol
  • Un usuario puede tener varios roles, y un rol puede pertenecer a varios usuarios.
Table usuario {
id_usuario int [pk, increment]
nombre varchar(100) [not null]
}

Table rol {
id_rol int [pk, increment]
nombre varchar(50) [unique, not null]
}

Table usuario_rol {
id_usuario int [not null]
id_rol int [not null]
asignado_en timestamp [default: `CURRENT_TIMESTAMP`]

indexes {
(id_usuario, id_rol) [pk]
}
}

Ref: usuario_rol.id_usuario > usuario.id_usuario
Ref: usuario_rol.id_rol > rol.id_rol

Así:

  • Cada usuario puede tener varios roles, pero no se duplica ninguno.
  • Podemos saber cuándo se asignó cada rol.
  • La relación queda clara y sin datos repetidos.

Tablas puente con atributos propios y PK artificial

Si necesitas identificar la relación individualmente (por ejemplo, para auditarla o referenciarla desde otra tabla), puedes usar un id artificial además de las FKs.

Table inscripcion {
id int [pk, increment]
id_alumno int [not null]
id_curso int [not null]
fecha_inscripcion date [default: `CURRENT_DATE`]
nota numeric(4,2)

indexes {
(id_alumno, id_curso) [unique]
}
}

Ref: inscripcion.id_alumno > alumno.id_alumno
Ref: inscripcion.id_curso > curso.id_curso

Aquí:

  • id sirve como identificador único de inscripción.
  • La restricción UNIQUE mantiene la integridad N-a-N.
  • Se pueden añadir más atributos (nota, estado, etc.) fácilmente.

Buenas prácticas para modelar N-a-N

  • Usa tabla puente siempre: no intentes “guardar arrays” en una sola columna.
  • Define PK compuesta si solo almacenas las FKs y algún atributo simple.
  • Usa PK artificial + UNIQUE si la relación tiene atributos complejos.
  • Aplica CHECK a los atributos de relación para validar reglas (ej. cantidad > 0).
  • Documenta la relación: es parte clave del modelo conceptual.
  • Define ON DELETE/UPDATE adecuadamente para mantener integridad (por ejemplo, CASCADE en relaciones débiles).

Errores comunes

  • Guardar IDs separados por comas en un campo VARCHAR (anti-patrón clásico).
  • No usar PK compuesta ni UNIQUE → duplicados silenciosos.
  • No usar tabla puente → modelo inconsistente.
  • Meter atributos en tablas equivocadas (producto/pedido en lugar de pedido_producto).
  • No documentar reglas de unicidad.

Con este módulo, el estudiante:

  • Comprende cuándo y cómo usar tablas puente,
  • Entiende la diferencia entre atributos de entidad y de relación,
  • Y evita anti-patrones muy comunes en principiantes.