En esta página
Claves y restricciones
Claves y restricciones
Las restricciones (constraints) son las reglas que PostgreSQL aplica automáticamente para garantizar la integridad de tus datos. Son la diferencia entre una base de datos que contiene datos confiables y una que puede tener cualquier basura. En esta lección las exploraremos todas en profundidad.
PRIMARY KEY — La identidad de cada fila
La clave primaria identifica de forma única cada fila de una tabla. Es la restricción más fundamental:
-- Clave primaria simple con SERIAL (autoincrement)
CREATE TABLE articulos (
id SERIAL PRIMARY KEY,
titulo VARCHAR(200) NOT NULL
);
-- Equivalente explícito de SERIAL PRIMARY KEY:
CREATE SEQUENCE articulos_id_seq;
CREATE TABLE articulos2 (
id INTEGER NOT NULL DEFAULT nextval('articulos_id_seq'),
titulo VARCHAR(200) NOT NULL,
CONSTRAINT pk_articulos2 PRIMARY KEY (id)
);
-- BIGSERIAL para tablas con muchas filas (> 2.000 millones)
CREATE TABLE eventos_log (
id BIGSERIAL PRIMARY KEY,
tipo VARCHAR(50) NOT NULL,
ocurrido_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Añadir clave primaria a tabla existente
ALTER TABLE mi_tabla ADD PRIMARY KEY (columna_id);UUID como clave primaria
UUID (Universally Unique Identifier) es preferible a los enteros secuenciales en muchas situaciones:
-- UUID v4: generado aleatoriamente, globalmente único
CREATE TABLE usuarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
nombre VARCHAR(150) NOT NULL,
creado_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
INSERT INTO usuarios (email, nombre) VALUES ('[email protected]', 'Ana García');
-- id se genera automáticamente: 'a3f5c8b2-4d17-4e8c-9a01-7b6f2c3d8e9f'
-- Ventajas de UUID:
-- 1. No expone el volumen de tu tabla (el cliente no sabe cuántos usuarios tienes)
-- 2. Se puede generar en el cliente antes del INSERT (útil en sistemas distribuidos)
-- 3. Se puede usar como referencia entre bases de datos o servicios
-- 4. No tiene límite de valores (vs SERIAL: máximo 2.147 millones)
-- Desventaja: ligeramente más lento en índices B-tree (16 bytes vs 4/8 bytes)
-- Para la mayoría de aplicaciones, la diferencia es insignificanteFOREIGN KEY — Integridad referencial
Las claves foráneas garantizan que las referencias entre tablas siempre sean válidas:
CREATE TABLE categorias (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE productos (
id SERIAL PRIMARY KEY,
nombre VARCHAR(200) NOT NULL,
precio NUMERIC(10,2) NOT NULL,
categoria_id INTEGER NOT NULL
REFERENCES categorias(id) -- FK básica
ON DELETE RESTRICT -- No permite borrar categoría con productos
ON UPDATE CASCADE, -- Si cambia el id de categoría, actualiza aquí también
creado_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- FK con nombre explícito (recomendado para mensajes de error claros)
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL,
CONSTRAINT fk_pedidos_cliente
FOREIGN KEY (cliente_id)
REFERENCES clientes(id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Comportamientos ON DELETE y ON UPDATE
-- RESTRICT (predeterminado): lanza error si se intenta violar la integridad
-- No puedes borrar una categoría que tiene productos
-- CASCADE: propaga la operación a las filas relacionadas
-- Si borras un pedido, también se borran sus líneas
CREATE TABLE pedidos_lineas (
pedido_id INTEGER NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
producto_id INTEGER NOT NULL REFERENCES productos(id) ON DELETE RESTRICT,
cantidad INTEGER NOT NULL,
PRIMARY KEY (pedido_id, producto_id)
);
-- SET NULL: pone NULL en la FK cuando se borra el referenciado
-- Útil para relaciones opcionales
CREATE TABLE artículos (
id SERIAL PRIMARY KEY,
titulo VARCHAR(200) NOT NULL,
autor_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL
-- Si se borra el usuario, el artículo queda sin autor (autor_id = NULL)
);
-- SET DEFAULT: pone el valor DEFAULT de la columna
CREATE TABLE tareas (
id SERIAL PRIMARY KEY,
descripcion TEXT NOT NULL,
asignado_a INTEGER DEFAULT 1 REFERENCES usuarios(id) ON DELETE SET DEFAULT
-- Si se borra el usuario asignado, se reasigna al usuario con id=1
);
-- NO ACTION: similar a RESTRICT pero se evalúa al final de la transacción
-- (permite situaciones donde la violación es temporal dentro de una transacción)UNIQUE — Unicidad sin ser clave primaria
-- UNIQUE en una columna: no permite valores duplicados
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE, -- Un email por usuario
username VARCHAR(50) UNIQUE -- Un username único (puede ser NULL)
);
-- Una columna UNIQUE puede tener NULL (NULL no es igual a NULL en SQL)
-- Múltiples filas pueden tener NULL en una columna UNIQUE
INSERT INTO usuarios (email) VALUES ('[email protected]');
INSERT INTO usuarios (email) VALUES ('[email protected]'); -- ❌ Error: duplicate key
INSERT INTO usuarios (email, username) VALUES ('[email protected]', NULL);
INSERT INTO usuarios (email, username) VALUES ('[email protected]', NULL); -- ✅ OK
-- UNIQUE compuesto: la combinación de columnas debe ser única
CREATE TABLE favoritos (
usuario_id INTEGER NOT NULL REFERENCES usuarios(id),
producto_id INTEGER NOT NULL REFERENCES productos(id),
guardado_en TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (usuario_id, producto_id) -- Un usuario no puede favoritar el mismo producto dos veces
);
-- Añadir restricción UNIQUE a tabla existente
ALTER TABLE productos ADD CONSTRAINT unique_sku UNIQUE (sku);CHECK — Validación personalizada
CHECK permite definir cualquier condición booleana que los datos deben cumplir:
CREATE TABLE productos (
id SERIAL PRIMARY KEY,
nombre VARCHAR(200) NOT NULL,
precio NUMERIC(10,2) NOT NULL CHECK (precio >= 0),
precio_oferta NUMERIC(10,2) CHECK (precio_oferta >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
rating NUMERIC(2,1) CHECK (rating >= 1.0 AND rating <= 5.0),
descuento_pct INTEGER CHECK (descuento_pct BETWEEN 0 AND 100),
tipo VARCHAR(20) CHECK (tipo IN ('físico', 'digital', 'servicio')),
-- Check que involucra múltiples columnas (a nivel de tabla)
CONSTRAINT check_precio_oferta_menor
CHECK (precio_oferta IS NULL OR precio_oferta <= precio)
);
-- Ejemplo con fechas
CREATE TABLE reservas (
id SERIAL PRIMARY KEY,
inicio DATE NOT NULL,
fin DATE NOT NULL,
CONSTRAINT check_fechas_validas CHECK (fin > inicio) -- La fecha de fin debe ser posterior al inicio
);
-- Check con función (más expresivo)
CREATE TABLE usuarios (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
-- Validación básica de formato de email con regex
);NOT NULL — Columnas obligatorias
-- NOT NULL: el valor nunca puede ser NULL
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150) NOT NULL, -- Obligatorio
email VARCHAR(255) NOT NULL, -- Obligatorio
telefono VARCHAR(20), -- Opcional (puede ser NULL)
empresa VARCHAR(200) -- Opcional
);
-- Añadir NOT NULL a columna existente
-- Primero debes rellenar los NULLs actuales:
UPDATE clientes SET telefono = 'Sin teléfono' WHERE telefono IS NULL;
ALTER TABLE clientes ALTER COLUMN telefono SET NOT NULL;
-- Quitar NOT NULL
ALTER TABLE clientes ALTER COLUMN telefono DROP NOT NULL;DEFAULT — Valores predeterminados
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes(id),
estado VARCHAR(20) NOT NULL DEFAULT 'pendiente',
prioridad INTEGER NOT NULL DEFAULT 3 CHECK (prioridad BETWEEN 1 AND 5),
total NUMERIC(10,2) NOT NULL DEFAULT 0,
notas TEXT DEFAULT '',
creado_en TIMESTAMPTZ NOT NULL DEFAULT NOW(),
actualizado_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- DEFAULT puede ser una expresión o función
CREATE TABLE tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
valor TEXT NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
expira_en TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
usado BOOLEAN NOT NULL DEFAULT false
);
-- Cambiar el DEFAULT de una columna existente
ALTER TABLE productos ALTER COLUMN activo SET DEFAULT true;
ALTER TABLE pedidos ALTER COLUMN estado SET DEFAULT 'nuevo';
ALTER TABLE pedidos ALTER COLUMN estado DROP DEFAULT;Claves primarias compuestas y surrogadas vs naturales
-- Clave compuesta: la PK es la combinación de varias columnas
CREATE TABLE cursos_estudiantes (
estudiante_id INTEGER NOT NULL REFERENCES estudiantes(id),
curso_id INTEGER NOT NULL REFERENCES cursos(id),
matriculado_en TIMESTAMPTZ NOT NULL DEFAULT NOW(),
nota NUMERIC(4,2) CHECK (nota BETWEEN 0 AND 10),
PRIMARY KEY (estudiante_id, curso_id) -- Clave compuesta
);
-- Clave natural: usa datos del mundo real como PK
-- Ejemplo: código ISO de país
CREATE TABLE paises (
codigo_iso VARCHAR(2) PRIMARY KEY, -- 'ES', 'US', 'MX'
nombre VARCHAR(100) NOT NULL
);
-- Claves surrogadas vs naturales: debate clásico
-- Surrogada (id SERIAL/UUID): simple, estable, universalmente aplicable
-- Natural (ISBN, DNI, código): significativa pero puede cambiar
-- Recomendación práctica: usa claves surrogadas por defecto
-- Añade UNIQUE en las claves naturales para mantener la restricción de negocio
CREATE TABLE libros (
id SERIAL PRIMARY KEY, -- Surrogada
isbn VARCHAR(13) NOT NULL UNIQUE, -- Natural (también única)
titulo VARCHAR(300) NOT NULL,
autor VARCHAR(200) NOT NULL
);Ver y gestionar restricciones existentes
-- Listar todas las restricciones de una tabla
SELECT
conname AS nombre_restriccion,
contype AS tipo, -- p=PK, f=FK, u=UNIQUE, c=CHECK, n=NOT NULL
pg_get_constraintdef(oid) AS definicion
FROM pg_constraint
WHERE conrelid = 'productos'::regclass;
-- Desactivar temporalmente FK (para carga masiva de datos)
-- ¡Cuidado! Solo en entornos de carga controlada
SET session_replication_role = 'replica'; -- Desactiva triggers y FKs
-- ... hacer la carga masiva ...
SET session_replication_role = 'origin'; -- Reactivar
-- Añadir restricciones a tablas existentes
ALTER TABLE productos
ADD CONSTRAINT check_precio_positivo CHECK (precio > 0),
ADD CONSTRAINT unique_nombre_categoria UNIQUE (nombre, categoria_id);
-- Eliminar una restricción
ALTER TABLE productos DROP CONSTRAINT check_precio_positivo;
-- Renombrar una restricción
ALTER TABLE productos RENAME CONSTRAINT unique_sku TO uq_productos_sku;[!warning] Añadir CHECK o NOT NULL a tablas con datos existentes puede fallar. Si tienes una tabla con millones de filas y añades
CHECK (precio > 0), PostgreSQL valida TODOS los registros existentes antes de crear la restricción. Si alguno falla, la operación se cancela. Para tablas grandes, usaADD CONSTRAINT ... NOT VALIDprimero (no valida datos existentes) y luegoVALIDATE CONSTRAINTen un momento de baja actividad.
-- Técnica para tablas grandes: añadir restricción sin validar datos existentes
ALTER TABLE productos
ADD CONSTRAINT check_precio_positivo CHECK (precio > 0) NOT VALID;
-- Validar en un momento de menor carga (puede tardar, pero no bloquea escrituras)
ALTER TABLE productos VALIDATE CONSTRAINT check_precio_positivo;[!tip] Nombra siempre tus restricciones explícitamente. En lugar de dejar que PostgreSQL genere nombres como
productos_precio_check, usaCONSTRAINT chk_productos_precio CHECK (precio >= 0). Los nombres descriptivos hacen que los mensajes de error sean comprensibles: "ERROR: new row violates check constraint chk_productos_precio" es mucho más útil que "ERROR: new row violates check constraint products_price1".
Las restricciones son tu red de seguridad. Con un esquema bien restringido, es casi imposible insertar datos inválidos. En la siguiente lección aprenderemos sobre índices: la herramienta clave para que las consultas en tablas con millones de filas sean instantáneas.
nextSteps:
- slug: indices-y-rendimiento label: "Siguiente: Índices y rendimiento"
Inicia sesión para guardar tu progreso