Las bases de datos relacionales están construidas sobre relaciones discretas. Cada tabla es una relación, cada clave foránea conecta dos relaciones, cada índice es una función de mapeo. Entender las relaciones subyacentes es clave para diseñar esquemas correctos y eficientes.
Una base de datos relacional es un conjunto de relaciones (tablas) conectadas mediante funciones y relaciones discretas. Cada fila es una tupla, cada columna es un atributo, y las claves primarias y foráneas son las funciones que conectan las relaciones.
El término "base de datos relacional" viene precisamente de esto: está fundamentada en la teoría matemática de las relaciones.
En este tema veremos cómo las relaciones discretas se materializan en diseño de esquemas, normalización, consultas (SQL) y optimización.
Una tabla es una relación matemática donde cada fila es una tupla y cada columna es un atributo. Por ejemplo, una tabla Empleados con columnas {id, nombre, departamento_id, salario} es una relación binaria sobre el conjunto de tuplas de 4 elementos.
| id | nombre | departamento_id | salario |
|---|---|---|---|
| 1 | Ana García | 10 | 50000 |
| 2 | Luis Pérez | 20 | 60000 |
| 3 | Marta López | 10 | 55000 |
| 4 | Carlos Ruiz | 30 | 45000 |
Tabla Empleados (como relación): {(1, "Ana García", 10, 50000), (2, "Luis Pérez", 20, 60000), (3, "Marta López", 10, 55000), (4, "Carlos Ruiz", 30, 45000)}
Cada operación SQL se traduce en operaciones sobre relaciones:
// Tabla como array de objetos (cada objeto es una tupla)
const empleados = [
{ id: 1, nombre: "Ana García", departamento_id: 10, salario: 50000 },
{ id: 2, nombre: "Luis Pérez", departamento_id: 20, salario: 60000 },
{ id: 3, nombre: "Marta López", departamento_id: 10, salario: 55000 },
{ id: 4, nombre: "Carlos Ruiz", departamento_id: 30, salario: 45000 }
];
// SELECT nombre, salario FROM empleados WHERE departamento_id = 10
function selectEmpleados(tabla, predicado, columnas) {
return tabla
.filter(predicado)
.map(fila => {
const resultado = {};
columnas.forEach(col => resultado[col] = fila[col]);
return resultado;
});
}
const resultado = selectEmpleados(
empleados,
e => e.departamento_id === 10,
["nombre", "salario"]
);
console.log(resultado);
// [
// { nombre: "Ana García", salario: 50000 },
// { nombre: "Marta López", salario: 55000 }
// ]
Una clave primaria es un atributo (o conjunto de atributos) que identifica unívocamente cada tupla. Matemáticamente, define una función inyectiva desde las tuplas a los valores de la clave.
id es clave primaria en la tabla Empleados, entonces la función f(tupla) = tupla.id es inyectiva: cada tupla mapea a un id único, y no hay dos tuplas con el mismo id.
// Validar que una columna es clave primaria (inyectividad)
function esClaveP rimaria(tabla, columna) {
const valores = tabla.map(fila => fila[columna]);
const valoresUnicos = new Set(valores);
return valores.length === valoresUnicos.size;
}
console.log(esClaveP rimaria(empleados, "id")); // true
console.log(esClaveP rimaria(empleados, "nombre")); // true (si nombres son únicos)
console.log(esClaveP rimaria(empleados, "departamento_id")); // false
// Tabla con clave primaria compuesta
const asignaciones = [
{ empleado_id: 1, proyecto_id: 100 },
{ empleado_id: 1, proyecto_id: 101 },
{ empleado_id: 2, proyecto_id: 100 },
{ empleado_id: 2, proyecto_id: 102 }
];
// Clave primaria compuesta: (empleado_id, proyecto_id)
function esClavePrimariaCompuesta(tabla, columnas) {
const claves = tabla.map(fila =>
columnas.map(col => fila[col]).join("|")
);
const clavesUnicas = new Set(claves);
return claves.length === clavesUnicas.size;
}
console.log(esClavePrimariaCompuesta(asignaciones, ["empleado_id", "proyecto_id"]));
// true
Una clave foránea es una función que relaciona tuplas de una tabla con tuplas de otra. Si la tabla Empleados tiene una columna departamento_id, esa columna es una clave foránea que mapea cada empleado a un departamento.
Tabla Departamentos:
| id | nombre |
|---|---|
| 10 | Ingeniería |
| 20 | Ventas |
| 30 | Administración |
Tabla Empleados (con Claves Foráneas):
| id | nombre | departamento_id FK | salario |
|---|---|---|---|
| 1 | Ana García | 10 | 50000 |
| 2 | Luis Pérez | 20 | 60000 |
const departamentos = [
{ id: 10, nombre: "Ingeniería" },
{ id: 20, nombre: "Ventas" },
{ id: 30, nombre: "Administración" }
];
const empleados = [
{ id: 1, nombre: "Ana García", departamento_id: 10, salario: 50000 },
{ id: 2, nombre: "Luis Pérez", departamento_id: 20, salario: 60000 },
{ id: 3, nombre: "Marta López", departamento_id: 10, salario: 55000 }
];
// Validar integridad referencial
function validarIntegridadReferencial(tablaPrincipal, columnaFK,
tableFK, columnaPK) {
for (const fila of tablaPrincipal) {
const valor_fk = fila[columnaFK];
if (!tableFK.find(f => f[columnaPK] === valor_fk)) {
return false; // FK invalida
}
}
return true;
}
console.log(validarIntegridadReferencial(
empleados, "departamento_id",
departamentos, "id"
)); // true
// JOIN: combinar empleados con sus departamentos
function join(tablaIzquierda, tablaD erecha,
columnasIzquierda, columnasD erecha,
condición) {
const resultado = [];
for (const filaIzq of tablaIzquierda) {
for (const filaDer of tablaD erecha) {
if (condición(filaIzq, filaDer)) {
resultado.push({
...filaIzq,
...Object.fromEntries(
columnasD erecha.map(col =>
[col, filaDer[col]]
)
)
});
}
}
}
return resultado;
}
const empleadosConDepartamentos = join(
empleados,
departamentos,
["id", "nombre", "salario"],
["nombre"],
(e, d) => e.departamento_id === d.id
);
console.log(empleadosConDepartamentos);
// [
// { id: 1, nombre: "Ana García", salario: 50000, nombre: "Ingeniería" },
// { id: 2, nombre: "Luis Pérez", salario: 60000, nombre: "Ventas" },
// { id: 3, nombre: "Marta López", salario: 55000, nombre: "Ingeniería" }
// ]
La normalización es el proceso de diseñar un esquema de base de datos sin redundancias. Se basa en identificar dependencias funcionales: dado un atributo, ¿qué otros atributos se determinan unívocamente?
id → nombre significa que el id determina unívocamente el nombre. No puede haber dos empleados con el mismo id y diferente nombre.
Las formas normales (1FN, 2FN, 3FN, BCNF) eliminan redundancias identificando estas dependencias:
❌ Mal diseño (sin normalizar):
| empleado_id | nombre | departamento | jefe |
|---|---|---|---|
| 1 | Ana | Ingeniería | Carlos |
| 2 | Luis | Ingeniería | Carlos |
| 3 | Marta | Ventas | María |
Problema: "Ingeniería" y "Carlos" se repiten. La dependencia funcional departamento → jefe no es sobre la clave primaria, indicando falta de normalización.
✅ Diseño normalizado:
Tabla Departamentos:
| id | nombre | jefe_id |
|---|---|---|
| 10 | Ingeniería | 4 |
| 20 | Ventas | 5 |
Tabla Empleados:
| id | nombre | departamento_id |
|---|---|---|
| 1 | Ana García | 10 |
| 2 | Luis Pérez | 10 |
| 3 | Marta López | 20 |
// Detectar dependencias funcionales
function dependenciaFuncional(tabla, atributoX, atributoY) {
const mapeo = {};
for (const fila of tabla) {
const valorX = fila[atributoX];
const valorY = fila[atributoY];
if (mapeo[valorX] !== undefined && mapeo[valorX] !== valorY) {
return false; // No es dependencia funcional
}
mapeo[valorX] = valorY;
}
return true;
}
const datosNoNormalizados = [
{ id: 1, nombre: "Ana", depto: "Ing", jefe: "Carlos" },
{ id: 2, nombre: "Luis", depto: "Ing", jefe: "Carlos" },
{ id: 3, nombre: "Marta", depto: "Ventas", jefe: "María" }
];
console.log(dependenciaFuncional(datosNoNormalizados, "id", "nombre"));
// true (id → nombre)
console.log(dependenciaFuncional(datosNoNormalizados, "depto", "jefe"));
// true (depto → jefe) - dependencia no primaria, indica falta de normalización
// En datos normalizados:
const departamentos = [
{ id: 10, nombre: "Ingeniería", jefe_id: 4 },
{ id: 20, nombre: "Ventas", jefe_id: 5 }
];
console.log(dependenciaFuncional(departamentos, "id", "nombre"));
// true (id → nombre en tabla Departamentos)
console.log(dependenciaFuncional(departamentos, "nombre", "jefe_id"));
// true (nombre → jefe_id, pero name es ahora clave primaria)
Un índice es una estructura que invierte una función para acelerar búsquedas. En lugar de recorrer toda la tabla, un índice mapea valores de atributo a posiciones de fila.
// Tabla sin índice
const empleados = [
{ id: 1, nombre: "Ana García", departamento_id: 10 },
{ id: 2, nombre: "Luis Pérez", departamento_id: 20 },
{ id: 3, nombre: "Marta López", departamento_id: 10 },
{ id: 4, nombre: "Carlos Ruiz", departamento_id: 30 }
];
// Búsqueda sin índice: O(n)
function buscarSinIndice(tabla, departamento_id) {
return tabla.filter(e => e.departamento_id === departamento_id);
}
// Crear índice en departamento_id
function crearIndice(tabla, atributo) {
const indice = {};
for (const fila of tabla) {
const valor = fila[atributo];
if (!indice[valor]) {
indice[valor] = [];
}
indice[valor].push(fila);
}
return indice;
}
const indiceDeptos = crearIndice(empleados, "departamento_id");
console.log(indiceDeptos);
// {
// "10": [
// { id: 1, nombre: "Ana García", departamento_id: 10 },
// { id: 3, nombre: "Marta López", departamento_id: 10 }
// ],
// "20": [ { id: 2, nombre: "Luis Pérez", departamento_id: 20 } ],
// "30": [ { id: 4, nombre: "Carlos Ruiz", departamento_id: 30 } ]
// }
// Búsqueda con índice: O(1) promedio
function buscarConIndice(indice, departamento_id) {
return indice[departamento_id] || [];
}
console.log(buscarConIndice(indiceDeptos, 10));
// Resultado instantáneo sin recorrer toda la tabla
// Índice compuesto (en dos atributos)
function crearIndiceCompuesto(tabla, atributos) {
const indice = {};
for (const fila of tabla) {
const clave = atributos.map(a => fila[a]).join("|");
if (!indice[clave]) {
indice[clave] = [];
}
indice[clave].push(fila);
}
return indice;
}
const empleadosProyectos = [
{ empleado_id: 1, proyecto_id: 100, horas: 40 },
{ empleado_id: 1, proyecto_id: 101, horas: 20 },
{ empleado_id: 2, proyecto_id: 100, horas: 30 }
];
const indiceCompuesto = crearIndiceCompuesto(empleadosProyectos,
["empleado_id", "proyecto_id"]);
console.log(indiceCompuesto["1|100"]);
// [{ empleado_id: 1, proyecto_id: 100, horas: 40 }]
SQL es un lenguaje declarativo para operar sobre relaciones. Las operaciones principales (SELECT, WHERE, JOIN, GROUP BY) tienen correspondencias directas en álgebra relacional:
| Operación SQL | Álgebra Relacional | Descripción |
|---|---|---|
SELECT columnas |
Proyección (π) | Selecciona columnas específicas |
WHERE condición |
Selección (σ) | Filtra filas según un predicado |
JOIN |
Producto cartesiano (×) + selección | Combina tablas según una condición |
GROUP BY |
Agrupación | Agrupa filas por atributo |
UNION, EXCEPT, INTERSECT |
Operaciones de conjuntos | Unión, diferencia, intersección |
// Motor relacional simple en JavaScript
class BDRelacional {
constructor() {
this.tablas = {};
}
crearTabla(nombre, datos) {
this.tablas[nombre] = datos;
}
// SELECT col1, col2 FROM tabla
select(tabla, columnas) {
return this.tablas[tabla].map(fila => {
const resultado = {};
columnas.forEach(col => resultado[col] = fila[col]);
return resultado;
});
}
// SELECT * FROM tabla WHERE condición
where(tabla, predicado) {
return this.tablas[tabla].filter(predicado);
}
// SELECT * FROM t1 JOIN t2 ON condición
join(tabla1, tabla2, condición) {
const resultado = [];
for (const f1 of this.tablas[tabla1]) {
for (const f2 of this.tablas[tabla2]) {
if (condición(f1, f2)) {
resultado.push({ ...f1, ...f2 });
}
}
}
return resultado;
}
// SELECT atributo, COUNT(*) FROM tabla GROUP BY atributo
groupBy(tabla, atributo) {
const grupos = {};
for (const fila of this.tablas[tabla]) {
const valor = fila[atributo];
if (!grupos[valor]) grupos[valor] = [];
grupos[valor].push(fila);
}
return Object.entries(grupos).map(([valor, filas]) => ({
[atributo]: valor,
count: filas.length
}));
}
}
const bd = new BDRelacional();
bd.crearTabla("empleados", [
{ id: 1, nombre: "Ana", depto_id: 10 },
{ id: 2, nombre: "Luis", depto_id: 20 },
{ id: 3, nombre: "Marta", depto_id: 10 }
]);
bd.crearTabla("departamentos", [
{ id: 10, nombre: "Ing" },
{ id: 20, nombre: "Ventas" }
]);
// SELECT nombre FROM empleados WHERE depto_id = 10
console.log(bd.where("empleados", e => e.depto_id === 10).map(e => e.nombre));
// ["Ana", "Marta"]
// SELECT e.nombre, d.nombre FROM empleados e JOIN departamentos d
// ON e.depto_id = d.id
console.log(bd.join("empleados", "departamentos",
(e, d) => e.depto_id === d.id));
// SELECT depto_id, COUNT(*) FROM empleados GROUP BY depto_id
console.log(bd.groupBy("empleados", "depto_id"));
// [{ depto_id: 10, count: 2 }, { depto_id: 20, count: 1 }]
Las transacciones garantizan que las operaciones sobre relaciones múltiples se completan atómicamente. Las propiedades ACID (Atomicity, Consistency, Isolation, Durability) aseguran que las relaciones entre tablas se mantienen en un estado válido.
// Transacción: transferencia de dinero
class CuentaBancaria {
constructor() {
this.cuentas = {
"001": { saldo: 1000 },
"002": { saldo: 500 }
};
this.enTransacción = false;
}
transferir(cuentaOrigen, cuentaDestino, monto) {
if (this.enTransacción) {
throw new Error("Ya hay una transacción en curso");
}
this.enTransacción = true;
try {
// Validaciones
if (!this.cuentas[cuentaOrigen]) {
throw new Error("Cuenta origen no existe");
}
if (!this.cuentas[cuentaDestino]) {
throw new Error("Cuenta destino no existe");
}
if (this.cuentas[cuentaOrigen].saldo < monto) {
throw new Error("Fondos insuficientes");
}
// Operación atómica
this.cuentas[cuentaOrigen].saldo -= monto;
this.cuentas[cuentaDestino].saldo += monto;
return {
éxito: true,
saldoOrigen: this.cuentas[cuentaOrigen].saldo,
saldoDestino: this.cuentas[cuentaDestino].saldo
};
} catch (error) {
// Rollback en caso de error
console.error("Transacción fallida:", error.message);
return { éxito: false, error: error.message };
} finally {
this.enTransacción = false;
}
}
obtenerSaldo(cuenta) {
return this.cuentas[cuenta]?.saldo ?? null;
}
}
const banco = new CuentaBancaria();
console.log(banco.transferir("001", "002", 200));
// { éxito: true, saldoOrigen: 800, saldoDestino: 700 }
console.log(banco.obtenerSaldo("001")); // 800
console.log(banco.obtenerSaldo("002")); // 700
Las bases de datos relacionales son el ejemplo más exitoso de cómo las matemáticas discretas (en particular, la teoría de relaciones) se aplican en informática práctica. Cada decisión de diseño—normalización, índices, claves, restricciones—emerge de entender las relaciones discretas subyacentes.
En el próximo tema exploraremos cómo las relaciones se aplican al análisis de algoritmos y complejidad computacional, donde la eficiencia depende de entender las operaciones que realizamos sobre relaciones.