Skip to main content

Relaciones y claves foráneas

En el documento anterior aprendiste a unir datos de varias tablas mediante JOIN.

Eso te permite consultar de forma relacional.

Pero para diseñar bases de datos reales, necesitas dar un paso más:

Entender cómo se crean, mantienen y protegen las relaciones entre tablas usando claves foráneas.

Sin este capítulo, cualquier proyecto real corre el riesgo de tener datos inconsistentes:

pedidos sin cliente, tareas sin usuario, productos sin categoría, matrículas que apuntan a alumnos inexistentes.

Por qué existen las relaciones en una base de datos

Una base relacional divide la información en tablas separadas.

Esto es bueno porque:

  • evita duplicar datos
  • organiza la información
  • facilita mantenimiento
  • permite consultas complejas

Pero crea un problema natural:

¿Cómo garantizamos que los datos relacionados entre sí sigan siendo válidos?

Ejemplo real:

  • Si tienes un pedido cuyo cliente ya no existe → la base queda corrupta.
  • Si asignas tareas a un usuario que no existe → tu API falla.
  • Si eliminas una categoría y deja productos huérfanos → tu sistema deja de ser coherente.

Las claves foráneas (Foreign Keys o FK) son el mecanismo que asegura que esas relaciones se mantengan válidas.

Qué es una clave foránea (FK)

Una clave foránea es una regla que dice:

“Esta columna apunta a otra columna que existe en otra tabla.”

Por ejemplo:

client_id INTEGER REFERENCES clients(id)

Esto significa:

  • client_id solo puede contener valores existentes en clients.id.
  • Si intentas insertar o actualizar algo incorrecto, SQLite lo bloquea.
  • Si eliminas un cliente, tendrás que decidir qué ocurre con sus pedidos.

Activar soporte de claves foráneas en SQLite

Muy importante:

SQLite no activa las foreign keys por defecto.

Debes activarlas en cada sesión:

PRAGMA foreign_keys = ON;

Si no activas esto:

  • las FK no se aplican
  • SQLite NO validará relaciones
  • podrás insertar datos incorrectos
  • podrás eliminar datos que rompan relaciones

Para comprobar si están activadas:

PRAGMA foreign_keys;

Debe devolver 1.

Crear relaciones 1:N — el tipo más común

La relación más frecuente es “uno a muchos”:

  • un cliente → muchos pedidos
  • una categoría → muchos productos
  • un autor → muchos libros

Se crea así:

Tabla principal

CREATE TABLE clients (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);

Tabla relacionada

CREATE TABLE orders (
id INTEGER PRIMARY KEY,
client_id INTEGER NOT NULL,
total REAL NOT NULL,
FOREIGN KEY (client_id) REFERENCES clients(id)
);

¿Qué significa esto?

  • orders.client_id debe coincidir con clients.id.
  • No puedes insertar un pedido con un client_id inexistente.
  • No puedes borrar un cliente si aún tiene pedidos (a menos que especifiques reglas especiales).

Qué pasa cuando insertas datos que violan la relación

Intentemos:

INSERT INTO orders (client_id, total)
VALUES (999, 50);

Resultado:

FOREIGN KEY constraint failed

SQLite te protege. Es lo correcto: nadie puede hacer un pedido si el cliente no existe.

Reglas de borrado y actualización (ON DELETE / ON UPDATE)

Las relaciones no solo validan datos: también definen qué ocurre cuando eliminas o cambias datos en la tabla principal.

Existen varias reglas:

1. RESTRICT (comportamiento por defecto)

No permite eliminar un registro si otro depende de él.

Ejemplo:

DELETE FROM clients WHERE id = 1;

Si tiene pedidos:

FOREIGN KEY constraint failed

Esto evita perder información secundaria.

2. CASCADE

En cascada: elimina (o actualiza) también las filas relacionadas.

Ejemplo:

FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE

Si borras un cliente:

  • se borran también todos sus pedidos.

3. SET NULL

Pone las referencias a NULL cuando eliminas el registro principal.

FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE SET NULL

Útil cuando:

  • quieres conservar pedidos pero sin cliente asociado

4. SET DEFAULT

Pone un valor por defecto cuando se elimina la fila principal.

5. NO ACTION

Parecido a RESTRICT (pero con diferencias internas de validación).

Ejemplo completo con ON DELETE

Vamos a crear dos tablas con cascada:

sqlite3 data/relaciones_demo.db
PRAGMA foreign_keys = ON;

Crear tablas:

CREATE TABLE clients (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);

CREATE TABLE orders (
id INTEGER PRIMARY KEY,
client_id INTEGER NOT NULL,
total REAL NOT NULL,
FOREIGN KEY (client_id)
REFERENCES clients(id)
ON DELETE CASCADE
);

Insertar datos:

INSERT INTO clients (name)
VALUES ('Ana'), ('Luis');

INSERT INTO orders (client_id, total)
VALUES (1, 50), (1, 90), (2, 120);

Si borras a Ana:

DELETE FROM clients WHERE id = 1;

Resultado:

  • Ana desaparece
  • Sus pedidos también se eliminan

Compruébalo:

SELECT * FROM orders;

Índices en columnas de claves foráneas (por qué son importantes)

SQLite no crea índices automáticamente para columnas con FK.

Si tienes:

FOREIGN KEY (client_id) REFERENCES clients(id)

Debes crear un índice manual:

CREATE INDEX idx_orders_client_id ON orders(client_id);

¿Por qué?

  • acelera JOINs
  • acelera búsquedas
  • evita lentitud en proyectos reales
  • es buena práctica profesional

No afecta la integridad, solo el rendimiento.

Errores comunes y cómo evitarlos

Error 1 — Olvidar activar foreign_keys

Solución:

PRAGMA foreign_keys = ON;

Error 2 — Crear tablas en orden incorrecto

No puedes crear primero la tabla dependiente con una FK si aún no existe la tabla principal.

Error 3 — Borrar datos que tienen dependencias sin conocer ON DELETE

Muchos principiantes rompen bases completas por no entender CASCADE.

Error 4 — Intentar insertar valores inválidos en la FK

Siempre comprueba:

SELECT * FROM clients;

Antes de insertar un pedido.

Error 5 — No crear índices en columnas FK

En bases medianas, esto reduce el rendimiento drásticamente.

Buenas prácticas profesionales al trabajar con relaciones

  • Activa siempre PRAGMA foreign_keys = ON.
  • Usa ON DELETE CASCADE solo cuando estés seguro de que quieres borrar datos secundarios.
  • Usa ON DELETE RESTRICT cuando prefieras proteger datos dependientes.
  • Indiza siempre las columnas FK.
  • Nunca permitas valores NULL en FK si deseas relaciones obligatorias.
  • Siempre revisa datos antes de eliminar.
  • Usa LEFT JOIN para detectar datos huérfanos.

11. Ejercicio final — Relaciones completas en CLI

Vamos a construir un pequeño sistema de categorías y productos con ON DELETE CASCADE.

Paso 1 — Crear base y activar FK

sqlite3 data/relaciones_ejercicio.db
PRAGMA foreign_keys = ON;

Paso 2 — Crear tablas

CREATE TABLE categories (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);

CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category_id INTEGER NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);

Paso 3 — Insertar datos

INSERT INTO categories (name)
VALUES ('Gaming'), ('Oficina'), ('Hogar');

INSERT INTO products (name, category_id)
VALUES
('Teclado mecánico', 1),
('Ratón RGB', 1),
('Silla ergonómica', 2),
('Lámpara LED', 3);

Paso 4 — Eliminar categoría y observar efecto

Borra la categoría Gaming:

DELETE FROM categories WHERE id = 1;

Comprueba productos:

SELECT * FROM products;

Los productos Gaming han sido eliminados automáticamente gracias a CASCADE.

Paso 5 — Detectar productos sin categoría (práctica JOIN)

SELECT p.name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE c.id IS NULL;

Si diseñaste correctamente la base, esto debe devolver cero filas.