Claves y restricciones en SQL
En esta lección
Un tipo de dato impide guardar texto en una columna de números, pero no impide un precio negativo, un email repetido o un pedido de un cliente que no existe. Para eso están las restricciones (constraints): reglas que pones al crear la tabla y que la base de datos comprueba en cada INSERT, UPDATE y DELETE. Si una operación las incumple, se rechaza con un error.
¿Por qué no comprobarlo en la aplicación? Porque a una base de datos suelen escribir varios programas, scripts y personas. Si la regla está en la base de datos, se cumple siempre, venga de donde venga el dato.
PRIMARY KEY
La clave primaria identifica cada fila de forma única. Implica dos reglas a la vez: no puede repetirse y no puede ser NULL. Cada tabla tiene como mucho una.
INSERT INTO clientes VALUES (1, 'Repetido', 'Soria', 40);
-- Error: ya existe un cliente con id 1
-- MySQL: Duplicate entry '1' for key 'clientes.PRIMARY'
-- SQLite: UNIQUE constraint failed: clientes.id
Clave primaria compuesta
A veces una sola columna no identifica la fila, pero una combinación sí. En las líneas de un pedido, el número de línea se repite (todos los pedidos tienen una línea 1), pero la pareja pedido + línea es única:
CREATE TABLE lineas_pedido (
pedido_id INT NOT NULL,
num_linea INT NOT NULL,
producto VARCHAR(100) NOT NULL,
cantidad INT NOT NULL DEFAULT 1,
PRIMARY KEY (pedido_id, num_linea)
);
Cuando la clave tiene varias columnas, se escribe al final, como una línea más. (Más abajo verás esta misma tabla completa, con su clave foránea.) Con esta tabla puede haber (3, 1) y (3, 2), o (1, 1) y (3, 1), pero no dos veces (3, 1).
Otro caso típico son las tablas que relacionan dos cosas, como matriculas (alumno_id, asignatura_id): un alumno no puede matricularse dos veces de la misma asignatura.
NOT NULL
Obliga a que la columna tenga valor. Úsala en todo lo que sea imprescindible: el nombre de un cliente, el precio de un producto.
INSERT INTO clientes (id, nombre) VALUES (7, NULL);
-- MySQL: Column 'nombre' cannot be null
-- SQLite: NOT NULL constraint failed: clientes.nombre
Consejo: pregúntate por cada columna: “¿tiene sentido una fila sin este dato?”. Si la respuesta es no, pon
NOT NULL. Es la restricción más útil y la que más se olvida.
Un ejemplo completo
Una tienda de material de montaña con categorías y productos. En los siguientes apartados probaremos cada regla:
CREATE TABLE categorias (
id INT PRIMARY KEY,
nombre VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE productos (
id INT PRIMARY KEY,
codigo CHAR(8) NOT NULL UNIQUE,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(8, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
categoria_id INT,
CONSTRAINT chk_productos_precio CHECK (precio >= 0),
CONSTRAINT chk_productos_stock CHECK (stock >= 0),
CONSTRAINT fk_productos_categoria
FOREIGN KEY (categoria_id) REFERENCES categorias(id)
ON DELETE SET NULL
ON UPDATE CASCADE
);
INSERT INTO categorias VALUES (1, 'Escalada'), (2, 'Montaña'), (3, 'Camping');
INSERT INTO productos (id, codigo, nombre, precio, categoria_id) VALUES
(1, 'PIE-0001', 'Pies de gato', 89.90, 1),
(2, 'ARN-0002', 'Arnés', 59.00, 1),
(3, 'MOC-0003', 'Mochila 30 L', 45.50, 2),
(4, 'TIE-0004', 'Tienda 2 plazas', 120.00, 3);
UNIQUE
UNIQUE impide que se repita un valor en una columna, igual que la clave primaria. La diferencia: una tabla solo tiene una clave primaria, pero puede tener varias columnas UNIQUE, y estas sí admiten NULL.
INSERT INTO productos (id, codigo, nombre, precio)
VALUES (5, 'PIE-0001', 'Pies de gato niño', 49.90);
-- Error: el código PIE-0001 ya existe
-- SQLite: UNIQUE constraint failed: productos.codigo
Úsala en datos que identifican algo en el mundo real: DNI, email de usuario, ISBN, matrícula de un coche.
Nota: en MySQL, PostgreSQL y SQLite, una columna
UNIQUEpuede tener variosNULL, porqueNULLno se considera igual a otroNULL. Si el dato es obligatorio, combinaNOT NULL UNIQUE.
También hay UNIQUE de varias columnas: UNIQUE (socio_id, fecha) impediría que un socio reserve dos veces el mismo día.
DEFAULT
DEFAULT da un valor a la columna cuando el INSERT no lo indica. En los productos no dimos stock, así que todos quedaron con 0:
SELECT id, nombre, stock FROM productos;
| id | nombre | stock |
|---|---|---|
| 1 | Pies de gato | 0 |
| 2 | Arnés | 0 |
| 3 | Mochila 30 L | 0 |
| 4 | Tienda 2 plazas | 0 |
Valores por defecto habituales: 0 en contadores, TRUE en activo, 'pendiente' en un estado o CURRENT_TIMESTAMP en una fecha de creación. Si escribes NULL a propósito en el INSERT, no se usa el valor por defecto: se intenta guardar NULL.
CHECK
CHECK comprueba una condición en cada fila. Si la condición es falsa, la operación se rechaza:
INSERT INTO productos (id, codigo, nombre, precio)
VALUES (5, 'CUE-0005', 'Cuerda 60 m', -10);
-- MySQL: Check constraint 'chk_productos_precio' is violated.
-- SQLite: CHECK constraint failed: chk_productos_precio
También salta en un UPDATE. El stock de los pies de gato es 0; si intentas vender uno:
UPDATE productos SET stock = stock - 1 WHERE id = 1;
-- SQLite: CHECK constraint failed: chk_productos_stock
Más ejemplos: CHECK (edad >= 0 AND edad < 130), CHECK (fecha_fin >= fecha_inicio), CHECK (estado IN ('pendiente', 'enviado', 'entregado')).
Cuidado: MySQL ignoraba los
CHECKhasta la versión 8.0.16: los aceptaba sin error, pero no los comprobaba. Desde esa versión funcionan. Si unCHECKevalúa aNULL(por ejemplo, conprecioaNULL), la fila se acepta; para impedirlo, añadeNOT NULL.
Nombrar las restricciones con CONSTRAINT
Fíjate en que en productos escribimos CONSTRAINT chk_productos_precio CHECK (...). Si no les das nombre, la base de datos inventa uno (productos_chk_1, productos_ibfk_1…). Ponerles nombre tiene dos ventajas:
- Los mensajes de error dicen qué regla falló:
chk_productos_preciose entiende;productos_chk_2no. - Para borrar o cambiar la restricción más adelante necesitas su nombre (lo verás en ALTER TABLE).
Una convención habitual: pk_ para claves primarias, fk_ para foráneas, uq_ para únicas y chk_ para comprobaciones, seguidas de la tabla y la columna.
FOREIGN KEY: claves foráneas
Una clave foránea obliga a que el valor de una columna exista en otra tabla. En productos, categoria_id solo puede tener ids que estén en categorias (o NULL):
INSERT INTO productos (id, codigo, nombre, precio, categoria_id)
VALUES (5, 'CUE-0005', 'Cuerda 60 m', 139.00, 9);
-- Error: no existe la categoría 9
-- MySQL: Cannot add or update a child row: a foreign key constraint fails (...)
-- SQLite: FOREIGN KEY constraint failed
Lo mismo pasa con las tablas del curso. pedidos.cliente_id apunta a clientes.id:
INSERT INTO pedidos (id, cliente_id, fecha, importe)
VALUES (6, 99, '2026-09-20', 10.00);
-- FOREIGN KEY constraint failed: no hay cliente 99
DELETE FROM clientes WHERE id = 1;
-- FOREIGN KEY constraint failed: Ana tiene pedidos
-- MySQL: Cannot delete or update a parent row: a foreign key constraint fails (...)
En cambio, DELETE FROM clientes WHERE id = 4; funciona: Pablo no tiene pedidos, así que nadie apunta a él.
A la tabla que tiene la clave foránea (pedidos) se la llama hija, y a la tabla a la que apunta (clientes), madre. La columna de la madre tiene que ser clave primaria o UNIQUE.
Cuidado (SQLite): por compatibilidad con versiones antiguas, SQLite no comprueba las claves foráneas salvo que las actives en cada conexión con
PRAGMA foreign_keys = ON;. MySQL (con InnoDB, el motor por defecto) y PostgreSQL las comprueban siempre.
Cuidado (MySQL): escribe las claves foráneas con la forma
FOREIGN KEY (columna) REFERENCES tabla(columna). La forma corta dentro de la columna (cliente_id INT REFERENCES clientes(id)) es SQL estándar y funciona en PostgreSQL y SQLite, pero MySQL 8 y anteriores la aceptan sin aplicarla (MySQL 9 ya la aplica).
ON DELETE y ON UPDATE
¿Qué pasa con los hijos cuando borras o cambias el id de la fila madre? Tú decides, con estas opciones:
| Opción | Al borrar la fila madre… |
|---|---|
RESTRICT / NO ACTION | Se prohíbe el borrado si tiene hijos (es lo que pasa por defecto) |
CASCADE | Se borran también los hijos |
SET NULL | Los hijos se quedan con la clave foránea a NULL |
SET DEFAULT | Los hijos toman el valor por defecto (MySQL con InnoDB no lo admite) |
RESTRICT y NO ACTION casi siempre se comportan igual; la diferencia (cuándo se comprueba la regla) solo importa en casos avanzados.
SET NULL
En productos pusimos ON DELETE SET NULL. Si la tienda deja de vender material de camping y borra esa categoría, la tienda de campaña no desaparece, se queda sin categoría:
DELETE FROM categorias WHERE id = 3;
SELECT id, nombre, categoria_id FROM productos;
| id | nombre | categoria_id |
|---|---|---|
| 1 | Pies de gato | 1 |
| 2 | Arnés | 1 |
| 3 | Mochila 30 L | 2 |
| 4 | Tienda 2 plazas | NULL |
Para que funcione, la columna no puede ser NOT NULL.
ON UPDATE CASCADE
También pusimos ON UPDATE CASCADE: si cambia el id de una categoría, los productos se actualizan solos.
UPDATE categorias SET id = 10 WHERE id = 1;
SELECT id, nombre, categoria_id FROM productos;
| id | nombre | categoria_id |
|---|---|---|
| 1 | Pies de gato | 10 |
| 2 | Arnés | 10 |
| 3 | Mochila 30 L | 2 |
| 4 | Tienda 2 plazas | NULL |
En la práctica los ids casi nunca cambian, así que ON UPDATE se usa poco.
CASCADE
Las líneas de un pedido no tienen sentido sin su pedido. Aquí ON DELETE CASCADE es lo lógico:
CREATE TABLE lineas_pedido (
pedido_id INT NOT NULL,
num_linea INT NOT NULL,
producto VARCHAR(100) NOT NULL,
cantidad INT NOT NULL DEFAULT 1 CHECK (cantidad > 0),
PRIMARY KEY (pedido_id, num_linea),
FOREIGN KEY (pedido_id) REFERENCES pedidos(id) ON DELETE CASCADE
);
INSERT INTO lineas_pedido (pedido_id, num_linea, producto, cantidad) VALUES
(1, 1, 'Arnés', 1),
(3, 1, 'Magnesio', 2),
(3, 2, 'Cepillo de presas', 1);
DELETE FROM pedidos WHERE id = 3;
SELECT * FROM lineas_pedido;
| pedido_id | num_linea | producto | cantidad |
|---|---|---|---|
| 1 | 1 | Arnés | 1 |
Al borrar el pedido 3 se han borrado sus dos líneas, sin escribir ningún DELETE más.
Cuidado:
CASCADEes cómodo y peligroso. Un soloDELETEpuede borrar en cadena cientos de filas de varias tablas. Úsalo solo cuando los hijos no tengan sentido sin la madre (líneas de pedido, fotos de un anuncio). Para clientes y pedidos, lo prudente esRESTRICT: una factura no debe desaparecer porque se borre un cliente.
Integridad referencial
Todo lo anterior se resume en un concepto: integridad referencial. Significa que toda referencia apunta a algo que existe. No hay pedidos de clientes fantasma ni productos de categorías borradas.
Sin claves foráneas, un error en el código de la aplicación o un DELETE hecho a mano deja “huérfanos”: filas hijas cuyo cliente_id no corresponde a nadie. Tarde o temprano, un JOIN las hace desaparecer de los informes, los totales no cuadran y nadie sabe por qué. Con claves foráneas, la base de datos no deja llegar a ese estado.
Claves naturales y claves sustitutas
¿Qué columna eliges como clave primaria?
- Una clave natural es un dato real que ya es único: el DNI de una persona, el ISBN de un libro, el código de un país (
ES). - Una clave sustituta (surrogate key) es un número sin significado que genera la base de datos: el típico
idconAUTO_INCREMENT.
Lo más habitual y recomendable es usar una clave sustituta como clave primaria y poner UNIQUE en la clave natural. Así tienes lo mejor de las dos:
CREATE TABLE socios (
id INT AUTO_INCREMENT PRIMARY KEY, -- clave sustituta
dni CHAR(9) NOT NULL UNIQUE, -- clave natural, protegida con UNIQUE
nombre VARCHAR(100) NOT NULL
);
¿Por qué no usar el DNI directamente? Porque los datos “reales” cambian (alguien se equivocó al teclearlo y hay que corregirlo), a veces faltan, y copiar un texto de 9 caracteres en cada tabla hija ocupa más que un entero. Si el id nunca cambia, las claves foráneas nunca se rompen.
Errores frecuentes
- Crear la tabla hija antes que la madre: la clave foránea apunta a una tabla que todavía no existe.
- Tipos distintos en la clave foránea y la columna a la que apunta (
INTfrente aBIGINT, por ejemplo): MySQL no te deja crear la relación. ON DELETE SET NULLcon una columnaNOT NULL: es una contradicción. MySQL da error al crear la tabla; PostgreSQL y SQLite te dejan crearla, pero fallan al borrar la fila madre.- Probar en SQLite sin
PRAGMA foreign_keys = ONy creer que las claves funcionan. - Usar
CASCADEen todo por comodidad y perder datos al borrar una sola fila.
Resumen
| Restricción | Qué impide | Ejemplo |
|---|---|---|
PRIMARY KEY | Filas repetidas o sin identificador | id INT PRIMARY KEY |
NOT NULL | Columnas vacías | nombre VARCHAR(100) NOT NULL |
UNIQUE | Valores repetidos (admite NULL) | email VARCHAR(255) UNIQUE |
DEFAULT | — (rellena si no das valor) | stock INT DEFAULT 0 |
CHECK | Valores que no cumplen una condición | CHECK (precio >= 0) |
FOREIGN KEY | Referencias a filas que no existen | FOREIGN KEY (categoria_id) REFERENCES categorias(id) |
ON DELETEdecide qué pasa con los hijos:RESTRICT(por defecto),CASCADEoSET NULL.- Pon nombre a tus restricciones con
CONSTRAINT. - Clave sustituta como
PRIMARY KEYy clave natural conUNIQUE.
En la siguiente lección, ALTER TABLE y DROP, aprenderás a cambiar tablas que ya existen: añadir columnas, restricciones y borrar lo que sobra.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.