Relaciones y Cardinalidad
Qué es una relación (en serio)
En el modelo relacional, una relación no es “una amistad entre tablas” ni un “link” informal:
Es una correspondencia formal entre entidades, basada en claves que garantizan integridad.
Ejemplo cotidiano:
- Un cliente realiza varios pedidos.
- Un pedido tiene uno o varios productos.
- Un usuario tiene un perfil.
Estas relaciones se definen en el diseño de la base de datos, no en el código de la aplicación.
Tipos de cardinalidades
La cardinalidad indica cuántas instancias de una entidad se relacionan con cuántas de otra.
En bases de datos relacionales, hay tres tipos fundamentales:
- 1 a 1 (uno a uno)
- 1 a N (uno a muchos)
- N a N (muchos a muchos)
Vamos a verlas una por una con ejemplos reales, diagramas conceptuales y simulación práctica con CSV + JS.
Relación 1 a 1 — “Identidad extendida”
Una relación 1 a 1 se da cuando:
- Cada fila de la tabla A se relaciona con una sola fila de la tabla B.
- Y cada fila de la tabla B solo pertenece a una de la tabla A.
Se usa cuando una entidad se divide en dos tablas por motivos lógicos, técnicos o de seguridad.
Ejemplo real:
- Tabla
Usuario(información general) - Tabla
Perfil(información sensible o adicional)
| Usuario | Perfil |
|---|---|
| id_usuario (PK) | id_perfil (PK = FK) |
| nombre | dirección |
| correo | fecha_nacimiento |
| teléfono |
Nota:
id_perfil es a la vez PK y FK → esto fuerza la relación 1:1 estricta.
Si hay un usuario, hay (como máximo) un perfil asociado.
Ejemplo en CSV:
usuarios.csv
id_usuario,nombre,correo
1,Ana,ana@example.com
2,Luis,luis@example.com
perfiles.csv
id_perfil,direccion,fecha_nacimiento,telefono
1,Calle 123,1995-03-10,611000000
2,Calle 456,1990-07-12,612000000
Ejercicio práctico — Validar 1:1
Vamos a verificar que cada usuario tenga como máximo un perfil y viceversa.
import fs from "fs";
// Importamos el módulo "fs" del núcleo de Node.js.
// Este módulo permite leer, escribir y manipular archivos del sistema.
// En este caso lo usamos para cargar archivos CSV desde una carpeta local.
// Función genérica para leer archivos CSV y convertirlos en arrays de objetos
function leerCSV(ruta) {
// La función recibe la ruta al archivo CSV
// y devuelve un array de objetos con claves basadas en la cabecera del archivo.
const data = fs.readFileSync(ruta, "utf-8").trim();
// readFileSync lee el archivo completo de forma síncrona.
// Es decir, bloquea la ejecución hasta que el archivo se ha leído completamente.
// "utf-8" indica que queremos decodificar el archivo como texto.
// trim() elimina espacios o saltos de línea innecesarios al principio y final,
// lo cual evita que aparezcan filas vacías.
const [cabecera, ...filas] = data.split("\n");
// Dividimos el contenido en líneas usando "\n".
// La primera línea será la cabecera (nombres de columnas).
// El resto de líneas son los datos reales.
//
// Ejemplo:
// id_usuario,nombre,edad
// 1,Ana,21
// 2,Juan,30
//
// cabecera → "id_usuario,nombre,edad"
// filas → ["1,Ana,21", "2,Juan,30"]
const campos = cabecera.split(",");
// Convertimos la cabecera en un array con los nombres de las columnas.
// ["id_usuario", "nombre", "edad"]
return filas.map(fila => {
// Para cada fila de datos del CSV generamos un objeto JavaScript.
const valores = fila.split(",");
// Convertimos la fila (cadena de texto) en un array de valores separados por coma.
return Object.fromEntries(
valores.map((v, i) => [campos[i], v])
);
// Aprovechamos que "campos" y "valores" tienen la misma longitud y orden.
// Creamos pares clave-valor con cada columna y su valor correspondiente.
// Luego Object.fromEntries convierte ese conjunto de pares en un objeto.
//
// Ejemplo:
// campos = ["id_usuario", "nombre"]
// valores = ["1", "Ana"]
// Resultado final: { id_usuario: "1", nombre: "Ana" }
});
}
// Cargamos los datos desde los archivos CSV correspondientes
const usuarios = leerCSV("./datos/usuarios.csv");
// usuarios será un array de objetos, uno por cada usuario.
const perfiles = leerCSV("./datos/perfiles.csv");
// perfiles será otro array donde cada fila representa un perfil vinculado
// a un usuario por el campo id_perfil.
// Creamos conjuntos con los identificadores existentes
const idsUsuarios = new Set(usuarios.map(u => u.id_usuario));
// Este Set contiene todos los id_usuario registrados.
// Usamos un Set porque permite búsquedas en tiempo constante
// y porque evita duplicados automáticamente.
const idsPerfiles = new Set(perfiles.map(p => p.id_perfil));
// Lo mismo para perfiles.
// Si hay duplicados, el tamaño del Set será menor que el número de filas reales.
// Verificamos que cada perfil corresponde a un usuario existente
for (const perfil of perfiles) {
// Recorremos todos los perfiles registrados (perfil → cada objeto del CSV).
if (!idsUsuarios.has(perfil.id_perfil)) {
// Comprobamos si el id_perfil aparece como una clave válida en la tabla usuarios.
console.error(
`Perfil ${perfil.id_perfil} no corresponde a ningún usuario`
);
// Si no existe, hay un problema de integridad referencial.
// Esto significa que en la relación 1:1 se ha asignado un perfil
// a un usuario inexistente.
}
}
// Verificamos duplicados (por si acaso)
// ------------------------------------------------
// Esta comprobación revisa si hay claves primarias repetidas.
// Se hace comparando:
// tamaño del Set (valores únicos)
// vs
// número total de registros.
if (idsUsuarios.size !== usuarios.length) {
// Si el tamaño del Set es menor, significa que en el CSV había duplicados.
console.error(`Hay duplicados en usuarios`);
}
if (idsPerfiles.size !== perfiles.length) {
console.error(`Hay duplicados en perfiles`);
}
// Si no se ha detectado ningún error, llegamos al final
console.log("Relación 1:1 verificada correctamente");
// Este mensaje indica que:
// - Todos los perfiles hacen referencia a usuarios existentes.
// - No hay duplicados en claves principales.
// Por lo tanto, la relación uno a uno está correctamente definida.
Esto es lo que en un motor real se implementaría con una PK/FK compartida.
Relación 1 a N — “Padre e hijos”
La relación 1 a N es la más común:
- Una fila en A puede estar asociada a muchas filas en B.
- Pero cada fila en B solo pertenece a una de A.
Ejemplo real:
- Un cliente puede hacer muchos pedidos.
- Pero cada pedido solo pertenece a un cliente.
| Cliente | Pedido |
|---|---|
| id_cliente (PK) | id_pedido (PK) |
| nombre | id_cliente (FK) |
| correo | fecha |
| total |
Esta relación se implementa poniendo la PK del padre como FK en la tabla hija.
Ejemplo en CSV:
clientes.csv
id_cliente,nombre,correo
1,Ana,ana@example.com
2,Luis,luis@example.com
pedidos.csv
id_pedido,id_cliente,fecha,total
A001,1,2025-10-10,70.00
A002,1,2025-10-11,20.00
A003,2,2025-10-11,35.00
El cliente 1 (Ana) tiene 2 pedidos; el cliente 2 (Luis) tiene 1 pedido.
Ejercicio práctico — Validar 1:N
const clientes = leerCSV("./datos/clientes.csv");
// Cargamos los datos del archivo clientes.csv.
// Esta llamada devuelve un array de objetos, donde cada objeto representa
// un cliente con las propiedades definidas en la cabecera del CSV.
// Ejemplo: { id_cliente: "1", nombre: "Ana", email: "ana@mail.com" }
const pedidos = leerCSV("./datos/pedidos.csv");
// Cargamos también los pedidos. Cada registro tendrá al menos:
// id_pedido, id_cliente y posiblemente más campos como fecha o total.
// Creamos un conjunto con los identificadores de cliente existentes
const idsClientes = new Set(clientes.map(c => c.id_cliente));
// Aquí tomamos todos los id_cliente y los convertimos en un Set.
// Un Set se usa porque guarda cada valor una sola vez y permite
// comprobar muy rápidamente si algo existe o no.
// Esto será útil para validar la integridad de los pedidos.
// Validamos que cada pedido haga referencia a un cliente existente
for (const pedido of pedidos) {
// Recorremos uno a uno todos los pedidos registrados.
if (!idsClientes.has(pedido.id_cliente)) {
// Comprobamos si el id_cliente del pedido aparece en el Set de clientes.
// Si no existe, significa que en el CSV de pedidos hay un pedido huérfano:
// apunta a un cliente que no está registrado.
console.error(
`Pedido ${pedido.id_pedido} apunta a un cliente inexistente`
);
// Mostramos un mensaje de error señalando la incoherencia detectada.
}
}
// Contar pedidos por cliente
// -------------------------------------------------------------
// Ahora creamos un contador. Queremos saber cuántos pedidos tiene cada cliente.
// Esta estructura permitirá hacer un "group by" básico usando un objeto plano.
const contador = {};
// "contador" será un objeto donde las claves serán id_cliente
// y los valores serán el número total de pedidos realizados por cada uno.
//
// Ejemplo final esperado:
// {
// "1": 3, // cliente con id 1 hizo 3 pedidos
// "2": 1, // cliente con id 2 hizo 1 pedido
// "3": 0 // si no tiene pedidos, luego mostramos 0
// }
for (const pedido of pedidos) {
// Recorremos cada pedido para ir sumando.
const id = pedido.id_cliente;
// Extraemos el id_cliente, que será la clave en el contador.
contador[id] = (contador[id] || 0) + 1;
// Si contador[id] ya existe, sumamos 1.
// Si no existe, contador[id] será undefined, por lo que usamos 0 como valor inicial.
// Esto permite acumular sin tener que inicializar manualmente la estructura.
}
// Mostrar el resultado final para todos los clientes
// -------------------------------------------------------------
for (const cliente of clientes) {
// Recorremos cada cliente registrado.
const totalPedidos = contador[cliente.id_cliente] || 0;
// Si el cliente no aparece en contador, significa que no tiene pedidos,
// así que mostramos 0.
console.log(
`${cliente.nombre} tiene ${totalPedidos} pedidos`
);
// Mostramos el nombre del cliente junto al número de pedidos realizados.
// Con esto obtenemos un pequeño informe de actividad por cliente.
}
Salida esperada:
Ana tiene 2 pedidos
Luis tiene 1 pedidos
Así es como se implementa conceptualmente un JOIN 1:N sin motor.
Relación N a N — “Redes reales”
Una relación N a N significa:
- Una fila de A puede asociarse con muchas de B.
- Y una fila de B puede asociarse con muchas de A.
Ejemplo real:
- Un producto puede estar en muchos pedidos.
- Un pedido puede tener muchos productos.
| Pedido | PedidoProducto (intermedia) | Producto |
|---|---|---|
| id_pedido (PK) | id_pedido (FK) | id_producto (PK) |
| id_producto (FK) | nombre | |
| cantidad | precio |
Esto no se implementa directamente entre las dos tablas principales:
se usa una tabla intermedia que contiene las relaciones (y a veces atributos adicionales como cantidad).
Ejemplo en CSV:
productos.csv
id_producto,nombre,precio
1,Teclado,20.00
2,Ratón,10.00
3,Monitor 24",120.00
pedidos.csv (como antes)
id_pedido,id_cliente,fecha,total
A001,1,2025-10-10,70.00
A002,1,2025-10-11,20.00
A003,2,2025-10-11,35.00
pedido_producto.csv
id_pedido,id_producto,cantidad
A001,1,2
A001,3,1
A002,1,1
A003,2,3
Pedido A001 tiene dos productos: Teclado (2 uds) y Monitor (1 ud).
El producto Teclado aparece en varios pedidos.
Ejercicio práctico — Validar N:N
const productos = leerCSV("./datos/productos.csv");
// Cargamos el archivo de productos. Cada fila representa un producto
// y se convertirá en un objeto con sus propiedades, por ejemplo:
// { id_producto: "10", nombre: "Ratón", precio: "12.99" }
const pedidoProducto = leerCSV("./datos/pedido_producto.csv");
// Este archivo representa la tabla intermedia en una relación N:N.
// En una relación N:N, ni los pedidos ni los productos pueden almacenar
// todas las referencias del otro lado. Se necesita una tabla puente.
// Cada fila de este CSV será algo como:
// { id_pedido: "200", id_producto: "10" }
//
// Esto significa que el pedido 200 incluye el producto 10.
// Esta tabla puede tener múltiples filas por pedido y múltiples filas por producto.
const idsPedidos = new Set(pedidos.map(p => p.id_pedido));
// Creamos un Set con todos los ids de pedidos válidos.
// Un Set permite verificar de manera muy eficiente si un elemento existe.
// Ejemplo de contenido: Set { "100", "101", "102" }
const idsProductos = new Set(productos.map(p => p.id_producto));
// Lo mismo con los productos. Tendremos un Set con todos los id_producto válidos.
// Ejemplo: Set { "10", "11", "12" }
// Validación de integridad referencial en relaciones N:N
// -------------------------------------------------------------
// En una relación N:N, la tabla intermedia (pedido_producto) debe cumplir:
// 1. Cada id_pedido debe corresponder a un pedido real.
// 2. Cada id_producto debe corresponder a un producto real.
// Si cualquiera de estos valores no existe, se rompe la integridad referencial.
// En bases de datos reales, esto lo garantizaría una clave foránea (foreign key).
for (const fila of pedidoProducto) {
// Recorremos cada fila de la tabla intermedia. Cada fila une un pedido y un producto.
if (!idsPedidos.has(fila.id_pedido)) {
// Comprobamos si el pedido al que apunta esta fila realmente existe.
console.error(
`Relación a pedido inexistente: ${fila.id_pedido}`
);
// Si no existe, hay un error en los datos:
// se está intentando vincular un producto a un pedido que no está registrado.
}
if (!idsProductos.has(fila.id_producto)) {
// También comprobamos que el producto referenciado por esta fila exista.
console.error(
`Relación a producto inexistente: ${fila.id_producto}`
);
// Si no existe el producto, significa que la tabla intermedia tiene datos corruptos.
}
}
// Si el script llega hasta aquí sin errores graves,
// significa que todas las relaciones N:N fueron validadas correctamente.
console.log("Relaciones N:N verificadas correctamente");
Esto simula lo que en SQL sería una tabla de unión con claves compuestas.
Resumen visual rápido
| Tipo relación | Ejemplo real | Estructura típica | Observación clave |
|---|---|---|---|
| 1:1 | Usuario ↔ Perfil | PK = FK | Extiende entidad |
| 1:N | Cliente → Pedido | FK en la tabla hija | Caso más común |
| N:N | Pedido ↔ Producto | Tabla intermedia con FK compuestas | Escalable y flexible |
Buenas prácticas con relaciones y cardinalidades
- Define siempre en qué tabla va la FK (lado “muchos”).
- Usa tablas intermedias con PK compuesta para N:N.
- Nombra las claves y relaciones de forma consistente (
id_cliente,id_pedido…). - Evita meter información repetida en varias tablas.
- Usa NULL con cuidado en relaciones opcionales (por ejemplo, “un perfil opcional”).
Errores comunes de principiantes
- Intentar modelar N:N con columnas repetidas o listas de IDs → desnormalización chapucera.
- No definir restricciones → relaciones inconsistentes.
- No usar PK compuesta en tablas intermedias → duplicados silenciosos.
- Usar claves naturales que cambian con el tiempo.