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_idsolo puede contener valores existentes enclients.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_iddebe coincidir conclients.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 CASCADEsolo cuando estés seguro de que quieres borrar datos secundarios. - Usa
ON DELETE RESTRICTcuando 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.