Saltar al contenido
restricciones.sql · devschool

Claves y restricciones en SQL

Lección 16 de 22 · 13 min de lectura · Actualizado el

En esta lección
  1. PRIMARY KEY
  2. NOT NULL
  3. Un ejemplo completo
  4. UNIQUE
  5. DEFAULT
  6. CHECK
  7. Nombrar las restricciones con CONSTRAINT
  8. FOREIGN KEY: claves foráneas
  9. ON DELETE y ON UPDATE
  10. Integridad referencial
  11. Claves naturales y claves sustitutas
  12. Errores frecuentes
  13. Resumen

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 UNIQUE puede tener varios NULL, porque NULL no se considera igual a otro NULL. Si el dato es obligatorio, combina NOT 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;
idnombrestock
1Pies de gato0
2Arnés0
3Mochila 30 L0
4Tienda 2 plazas0

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 CHECK hasta la versión 8.0.16: los aceptaba sin error, pero no los comprobaba. Desde esa versión funcionan. Si un CHECK evalúa a NULL (por ejemplo, con precio a NULL), la fila se acepta; para impedirlo, añade NOT 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_precio se entiende; productos_chk_2 no.
  • 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ónAl borrar la fila madre…
RESTRICT / NO ACTIONSe prohíbe el borrado si tiene hijos (es lo que pasa por defecto)
CASCADESe borran también los hijos
SET NULLLos hijos se quedan con la clave foránea a NULL
SET DEFAULTLos 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;
idnombrecategoria_id
1Pies de gato1
2Arnés1
3Mochila 30 L2
4Tienda 2 plazasNULL

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;
idnombrecategoria_id
1Pies de gato10
2Arnés10
3Mochila 30 L2
4Tienda 2 plazasNULL

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_idnum_lineaproductocantidad
11Arnés1

Al borrar el pedido 3 se han borrado sus dos líneas, sin escribir ningún DELETE más.

Cuidado: CASCADE es cómodo y peligroso. Un solo DELETE puede 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 es RESTRICT: 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 id con AUTO_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 (INT frente a BIGINT, por ejemplo): MySQL no te deja crear la relación.
  • ON DELETE SET NULL con una columna NOT 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 = ON y creer que las claves funcionan.
  • Usar CASCADE en todo por comodidad y perder datos al borrar una sola fila.

Resumen

RestricciónQué impideEjemplo
PRIMARY KEYFilas repetidas o sin identificadorid INT PRIMARY KEY
NOT NULLColumnas vacíasnombre VARCHAR(100) NOT NULL
UNIQUEValores repetidos (admite NULL)email VARCHAR(255) UNIQUE
DEFAULT— (rellena si no das valor)stock INT DEFAULT 0
CHECKValores que no cumplen una condiciónCHECK (precio >= 0)
FOREIGN KEYReferencias a filas que no existenFOREIGN KEY (categoria_id) REFERENCES categorias(id)
  • ON DELETE decide qué pasa con los hijos: RESTRICT (por defecto), CASCADE o SET NULL.
  • Pon nombre a tus restricciones con CONSTRAINT.
  • Clave sustituta como PRIMARY KEY y clave natural con UNIQUE.

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

[SQL] El producto 1 tiene stock 0. ¿Qué pasa al ejecutar el UPDATE?
CREATE TABLE productos (
  id INT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0)
);
INSERT INTO productos (id, nombre) VALUES (1, 'Arnés');
UPDATE productos SET stock = stock - 1 WHERE id = 1;

[SQL] lineas_pedido tiene 3 filas: una del pedido 1 y dos del pedido 3. Su clave foránea es FOREIGN KEY (pedido_id) REFERENCES pedidos(id) ON DELETE CASCADE. ¿Cuántas filas quedan tras el DELETE?
DELETE FROM pedidos WHERE id = 3;
SELECT COUNT(*) FROM lineas_pedido;

[SQL] ¿En qué se diferencia UNIQUE de PRIMARY KEY?

[SQL] Creas tablas con claves foráneas en SQLite y puedes insertar un pedido de un cliente que no existe sin ningún error. ¿Por qué?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.