🚀 Guía para empezar · De 0 al día a día

SQL De 0 a Producción

Por Francesc Fosas (con la ayuda de Gemini)

Guía completa de SQL con PostgreSQL: desde tu primera consulta hasta esquemas profesionales de e-commerce, con analogías, Mini-Labs y mejores prácticas.

18Secciones
3Labs
0 → ✓Nivel
01

¿Qué es SQL y por qué lo necesitas?

Imagina que tienes una agenda de contactos en una libreta de papel. Mientras tienes 20 contactos, todo va bien. Pero cuando tienes 20.000… ¿cómo buscas a alguien por ciudad? ¿Cómo eliminas todos los duplicados? Necesitas un sistema que organice, busque y manipule datos de forma eficiente. Eso es una base de datos, y SQL (Structured Query Language) es el idioma universal para hablar con ella.

SQL no es un lenguaje de programación general como Python o Java. Es un lenguaje declarativo: tú le dices qué quieres, no cómo hacerlo.

ℹ️
Nota sobre el estándar
Aunque existe un estándar internacional (SQL:2023), cada motor añade sus propias "extensiones". Es como el español: el estándar es el mismo, pero los acentos y modismos cambian entre países. Se pronuncia "ese-cu-ele" o "sequel".
🐘

PostgreSQL

Aplicaciones empresariales, datos complejos. Open Source. (Usado en esta guía).

🐬

MySQL / MariaDB

Web tradicional (WordPress, Laravel, etc.). Open Source.

📱

SQLite

Apps móviles, prototipos, testing. Open Source.

🏢

SQL Server / Oracle

Entornos corporativos, banca. Comerciales y de pago.

02

Instalación y primer contacto

# Actualizar paquetes e instalar
sudo apt update
sudo apt install postgresql postgresql-client

# Verificar y arrancar el servicio
sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo systemctl status postgresql
# Usando Homebrew
brew install postgresql@16

# Iniciar el servicio en segundo plano
brew services start postgresql@16
# Instalación en openSUSE
sudo zypper install postgresql postgresql-server

# Iniciar servicio
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Opción 1: Descargar el instalador oficial desde:
# https://www.postgresql.org/download/windows/

# Opción 2: Usar Windows Subsystem for Linux (WSL) - Recomendado para devs
wsl --install
# Luego abre WSL y sigue las instrucciones de Ubuntu/Debian
🐳
Alternativa Pro: Usar Docker (Recomendado)
La forma más limpia de tener una base de datos sin ensuciar tu sistema operativo es usando contenedores. Se levanta y se destruye en segundos. Si quieres aprender cómo hacerlo, revisa esta Guía de Docker (Sección Bases de Datos).

PostgreSQL crea un usuario del sistema llamado postgres. Vamos a usarlo para entrar y crear nuestro propio usuario.

Terminal / SQL
# Cambiar al usuario postgres del sistema y abrir consola psql
sudo -u postgres psql

-- 1. Crear un usuario (rol) con contraseña
CREATE USER dev_user WITH PASSWORD 'mi_password_segura';

-- 2. Crear una base de datos asignada a ese usuario
CREATE DATABASE mi_primera_db OWNER dev_user;

-- 3. Salir
\q

# 4. Ahora puedes conectarte directamente desde la terminal:
psql -U dev_user -d mi_primera_db -h localhost
⚠️
El error "Peer authentication failed"
Al conectar con -h localhost podrías recibir este error. Esto ocurre porque PostgreSQL intenta autenticar usando usuarios de Linux en vez de contraseñas. Para solucionarlo, edita pg_hba.conf: busca la línea local all all peer y cámbiala por local all all md5. Luego reinicia: sudo systemctl restart postgresql.
03

Los fundamentos: tablas, tipos de datos y CRUD

Piensa en una hoja de cálculo de Excel: tiene columnas y filas. En SQL, una Tabla es un conjunto de columnas (que definen el tipo de dato) y filas (los registros reales).

SQL - Crear Tabla
-- Tipos comunes: SERIAL (autoincremental), VARCHAR(n), TEXT, BOOLEAN, TIMESTAMP
-- NUMERIC(10,2) es ideal para dinero (nunca uses FLOAT para dinero)

CREATE TABLE productos (
    id          SERIAL PRIMARY KEY,        -- ID único, se genera solo
    nombre      VARCHAR(200) NOT NULL,     -- Obligatorio
    descripcion TEXT,                       -- Opcional, sin límite
    precio      NUMERIC(10,2) NOT NULL,    -- Decimal exacto
    stock       INTEGER DEFAULT 0,          -- Si no se indica, vale 0
    activo      BOOLEAN DEFAULT true,
    created_at  TIMESTAMP DEFAULT NOW()
);
SQL - CRUD Básico
-- INSERT (Crear)
INSERT INTO productos (nombre, precio, stock) VALUES
    ('Teclado mecánico', 89.99, 50),
    ('Ratón gaming', 45.50, 120);

-- SELECT (Leer)
SELECT nombre, precio FROM productos WHERE precio > 50 ORDER BY precio DESC LIMIT 3;

-- UPDATE (Modificar) - NUNCA olvides el WHERE
UPDATE productos SET precio = precio * 1.10 WHERE nombre = 'Teclado mecánico';

-- DELETE (Eliminar) - NUNCA olvides el WHERE
DELETE FROM productos WHERE activo = false;
💡
Mini-Lab 1: Tu primera tabla
Crea una tabla alumnos con id (SERIAL), nombre, email (UNIQUE), nota (NUMERIC). Inserta 3 alumnos, haz un UPDATE para subir la nota al alumno con id 1, y luego elimínalo.
04

Filtrado avanzado y funciones

SQL
-- Filtros avanzados
SELECT * FROM productos WHERE precio BETWEEN 40 AND 100;
SELECT * FROM productos WHERE nombre IN ('Ratón gaming', 'Webcam HD');
SELECT * FROM productos WHERE nombre ILIKE '%teclado%'; -- ILIKE ignora mayúsculas

-- Lógica condicional (CASE)
SELECT nombre, stock,
    CASE 
        WHEN stock = 0 THEN 'Agotado'
        WHEN stock < 10 THEN 'Bajo stock'
        ELSE 'Suficiente'
    END AS estado
FROM productos;

-- Agrupación (GROUP BY)
SELECT categoria, COUNT(*) AS total, AVG(precio) AS precio_medio
FROM productos
GROUP BY categoria
HAVING AVG(precio) > 50; -- HAVING filtra grupos, WHERE filtra filas individuales
05

Normalización: Diseñar sin dolor

Imagina un Excel gigante donde, cada vez que un cliente compra algo, escribes su nombre, su teléfono y su dirección. Si ese cliente cambia de teléfono, tendrás que buscar y actualizar sus 500 compras pasadas. Si olvidas una, tendrás datos inconsistentes. Normalizar es el proceso de romper ese Excel en tablas pequeñas y especializadas para evitar esto.

🚨
La Regla de Oro del Diseño
Si tienes que actualizar el mismo dato (ej. el email de un cliente) en más de una fila al mismo tiempo, tu base de datos está mal diseñada. Cada dato real debe existir en un único lugar.
06

Constraints: Tu última línea de defensa

Normalmente validarás que un precio sea mayor a cero en tu código Python/Node.js. Pero el código backend siempre tiene bugs, o un día alguien conectará un script directamente a tu BD. Las Constraints (Restricciones) aseguran que la base de datos sea un escudo impenetrable que rechace datos basura vengan de donde vengan.

SQL - Escudos activos
-- Tabla de pedidos con FK hacia clientes
-- CHECK: El guardián de la lógica. Evita precios o stocks imposibles.
ALTER TABLE productos ADD CONSTRAINT chk_positivo CHECK (precio > 0 AND stock >= 0);

-- Combinación única (un mismo pedido no puede tener el mismo producto en 2 líneas)
ALTER TABLE pedido_items ADD CONSTRAINT uq_item UNIQUE (pedido_id, producto_id);

-- Cascada: Si se borra un usuario, borrar sus pedidos
-- ON DELETE CASCADE: Limpieza automática. Si se borra un usuario, borra sus pedidos.
-- (¡Peligro! Úsalo con cuidado, a veces es mejor prohibir el borrado con RESTRICT)
ALTER TABLE pedidos ADD CONSTRAINT fk_usuario FOREIGN KEY (user_id) 
REFERENCES usuarios(id) ON DELETE CASCADE;
07

Relaciones (JOINs): Reconstruyendo el puzzle

En la sección 5 rompimos nuestro Excel gigante en tablas pequeñitas perfectas. Pero ahora necesitamos leer la información junta (Ej: "Dime el nombre del cliente y el total de su pedido"). Para eso usamos las Foreign Keys (el puente que conecta las tablas) y los JOINs (la instrucción de SQL para cruzarlas).

Visualizando los JOINs (Diagramas de Venn)

INNER JOIN Solo Intersección LEFT JOIN Todo A + Intersección RIGHT JOIN Todo B + Intersección FULL JOIN Absolutamente Todo
SQL - Uniendo tablas
-- Tabla de pedidos con FK hacia clientes
-- Tabla de pedidos con FK (Foreign Key) hacia clientes
CREATE TABLE pedidos (
    id          SERIAL PRIMARY KEY,
    cliente_id  INTEGER NOT NULL REFERENCES clientes(id),
    total       NUMERIC(10,2) NOT NULL
);

-- INNER JOIN: Solo clientes con pedidos (intersección)
-- INNER JOIN: Intersección estricta. Muestra SOLO los clientes que SÍ tienen pedidos.
SELECT c.nombre, p.total
FROM pedidos p
INNER JOIN clientes c ON p.cliente_id = c.id;

-- LEFT JOIN: Todos los clientes, tengan o no pedidos
-- LEFT JOIN: Incluyente. Muestra TODOS los clientes (la tabla de la izquierda),
-- e incluye sus pedidos. Si no tienen pedidos, muestra NULL en esas columnas.
SELECT c.nombre, COUNT(p.id) AS total_pedidos
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.nombre;
💡
Mini-Lab 2: Diseña una biblioteca con JOINs
Reto: Diseña un esquema para una biblioteca y practica relaciones:
  • Crea las tablas autores (id, nombre, pais), libros (id, titulo, anio, autor_id) y prestamos (id, libro_id, lector, fecha_prestamo, fecha_devolucion).
  • Inserta algunos autores, libros y préstamos inventados.
  • Haz un INNER JOIN para listar "Título del libro — Nombre del autor".
  • Haz un LEFT JOIN para encontrar autores que no tienen libros registrados.
  • Cuenta cuántos libros tiene cada autor con GROUP BY.
Solución: (intenta hacerlo antes de mirar)
SQL - Solución Lab 2
-- 1. Título + Autor (INNER JOIN)
SELECT l.titulo, a.nombre AS autor
FROM libros l
INNER JOIN autores a ON l.autor_id = a.id;

-- 2. Autores sin libros (LEFT JOIN)
SELECT a.nombre
FROM autores a
LEFT JOIN libros l ON a.id = l.autor_id
WHERE l.id IS NULL;

-- 3. Total de libros por autor
SELECT a.nombre, COUNT(l.id) AS total_libros
FROM autores a
LEFT JOIN libros l ON a.id = l.autor_id
GROUP BY a.nombre
ORDER BY total_libros DESC;
08

Subconsultas, CTEs y Vistas

Subconsultas (Subqueries)

Una subconsulta es una consulta anidada dentro de otra. Piensa en las muñecas rusas (matrioshkas): abres una y dentro hay otra más pequeña. Son muy útiles para calcular un valor sobre la marcha y usarlo inmediatamente como filtro en el WHERE.

SQL - Subqueries
-- Productos con precio superior a la media
SELECT nombre, precio FROM productos
WHERE precio > (SELECT AVG(precio) FROM productos);

CTEs (Common Table Expressions) y Vistas

Anidar muchas subconsultas acaba creando un código "espagueti" muy difícil de leer. Las CTEs (que siempre empiezan con WITH) resuelven esto actuando como variables temporales: le das un nombre a tu subconsulta arriba del todo, y luego la usas abajo como si fuera una tabla normal de tu base de datos.

SQL
-- CTEs (Common Table Expressions) con WITH
-- Hacen el código infinitamente más legible que subconsultas anidadas
WITH resumen_clientes AS (
    SELECT c.id, c.nombre, SUM(p.total) AS gasto_total
    FROM clientes c LEFT JOIN pedidos p ON c.id = p.cliente_id
    GROUP BY c.id, c.nombre
)
SELECT nombre, gasto_total FROM resumen_clientes WHERE gasto_total > 100;

-- Vistas (Views) - Consultas preguardadas en la DB
CREATE VIEW v_clientes_vip AS
SELECT * FROM resumen_clientes WHERE gasto_total > 1000;
🔭
Vistas: Ventanas virtuales de solo lectura
Una Vista (VIEW) no guarda datos en el disco duro, solo guarda la consulta. Es como una ventana preconfigurada a tu base de datos. Es la forma ideal de darle a un Analista de Datos un "resumen de ventas" sin darle acceso a la tabla original llena de contraseñas.
09

Transacciones (ACID)

Imagina que transfieres 100€ de tu cuenta a la de un amigo. Esto implica DOS operaciones SQL: restar de tu cuenta y sumar a la suya. Si el servidor se apaga por un corte de luz justo en el medio, el dinero desaparece. Las transacciones solucionan esto actuando como un contrato de "Todo o Nada" que garantizan las reglas ACID (Atomicidad, Consistencia, Aislamiento, Durabilidad).

SQL
BEGIN;  -- Inicia la transacción
    
    -- Restamos dinero a Ana
    UPDATE cuentas SET saldo = saldo - 200 WHERE nombre = 'Ana';
    
    -- Si algo explota aquí, el COMMIT final no se ejecutará
    
    -- Sumamos dinero a Luis
    UPDATE cuentas SET saldo = saldo + 200 WHERE nombre = 'Luis';

COMMIT; -- O usar ROLLBACK si capturamos un error
💡
Mini-Lab 3: Transferencia bancaria con transacciones
Reto: Simula un sistema bancario básico:
  • Crea una tabla cuentas con titular y saldo (que no permita saldo negativo).
  • Inserta a Ana con 1000€ y Luis con 500€.
  • Haz una transferencia de 200€ de Ana a Luis usando BEGIN/COMMIT.
  • Intenta transferir 2000€ (más de lo que tiene Ana) y usa ROLLBACK para cancelar.
SQL - Solución Lab 3
CREATE TABLE cuentas (
    id      SERIAL PRIMARY KEY,
    titular VARCHAR(100) NOT NULL,
    saldo   NUMERIC(10,2) NOT NULL CHECK (saldo >= 0)  -- Red de seguridad
);

INSERT INTO cuentas (titular, saldo) VALUES ('Ana', 1000.00), ('Luis', 500.00);

-- Transferencia exitosa (Ana -> Luis : 200€)
BEGIN;
    UPDATE cuentas SET saldo = saldo - 200 WHERE id = 1;
    UPDATE cuentas SET saldo = saldo + 200 WHERE id = 2;
COMMIT;

-- Transferencia fallida (Ana -> Luis : 2000€)
BEGIN;
    UPDATE cuentas SET saldo = saldo - 2000 WHERE id = 1;
    -- Falla por el CHECK (saldo >= 0). PostgreSQL aborta automáticamente.
ROLLBACK;  -- Los saldos quedan intactos
10

Índices y Rendimiento

Tu aplicación va rapidísima en tu portátil con 50 registros. Pero en producción, con 10 millones de filas, las búsquedas tardan 30 segundos y el servidor colapsa. ¿Qué ha pasado? Sin índices, la base de datos lee fila por fila desde la primera hasta la última (lo que se conoce como Full Table Scan).

Piensa en el índice alfabético de un libro. Si buscas información sobre "PostgreSQL", no lees las 500 páginas del libro enteras: vas al índice al final, buscas la 'P', y saltas directamente a la página 347. Un índice en SQL hace exactamente eso creando una estructura matemática invisible (un árbol B-Tree) superrápida.

SQL
-- B-Tree index (por defecto) para búsquedas rápidas
CREATE INDEX idx_clientes_email ON clientes(email);

-- Índice compuesto (útil cuando filtras por 2 columnas a la vez)
CREATE INDEX idx_pedidos_cliente_estado ON pedidos(cliente_id, estado);

-- Ver el plan de ejecución de la DB para saber si usa el índice
EXPLAIN ANALYZE SELECT * FROM clientes WHERE email = 'ana@email.com';
⚠️
El coste de los índices
Los índices aceleran mágicamente las lecturas (SELECT), pero ralentizan las escrituras (INSERT, UPDATE) porque cada vez que metes un dato, la base de datos debe reorganizar el árbol del índice. Regla general: no indexes todas las columnas, solo las que uses mucho en los WHERE o JOIN.
11

Permisos y Seguridad

Hasta ahora hemos trabajado como superusuario postgres. Eso es como darle las llaves maestras de todo el edificio corporativo al repartidor de pizzas. Si un atacante compromete tu aplicación web, heredará esos permisos y podrá borrar la base de datos entera. Debes aplicar el Principio de Mínimo Privilegio: cada aplicación o usuario debe tener solo los permisos estrictamente necesarios para hacer su trabajo, ni uno más.

SQL
-- Crear usuario para la app web con permisos limitados
CREATE USER app_web WITH PASSWORD 'seguro_123';
GRANT SELECT, INSERT, UPDATE ON productos, pedidos TO app_web;

-- Revocar permisos peligrosos
REVOKE DELETE ON pedidos FROM app_web;
12

De Desarrollo a Producción

En producción, tu código (Python, Java) se encarga de lanzar las queries de forma segura usando librerías y transacciones. Un par de reglas vitales:

🗃️

Migraciones (El Git de la BD)

Igual que usas Git para tu código, nunca hagas ALTER TABLE a mano en producción. Usa herramientas de migración (Flyway, Alembic) para tener un historial de versiones de tu esquema de datos que se aplique automáticamente.

💾

Backups

Automatiza dumps (pg_dump). Y recuerda: un backup que no has probado a restaurar, no es un backup.

13

Window Functions (Avanzado)

Con GROUP BY calculas totales, pero pierdes el detalle (las filas colapsan en una sola). ¿Y si quieres ver la compra de un cliente Y al mismo tiempo su gasto acumulado histórico en la misma tabla?

Las Window Functions son la magia analítica de SQL. Mantienen las filas originales intactas, pero les "pegan" al lado una columna nueva con el cálculo del grupo. Piensa en ellas como un director de maratón que ve la estadística global de la carrera, pero sin perder de vista a cada corredor individual.

SQL
-- Suma acumulada de gastos del cliente en cada pedido, manteniendo las filas de pedidos intactas
SELECT 
    cliente_id, 
    total, 
    SUM(total) OVER (PARTITION BY cliente_id ORDER BY fecha) AS acumulado
FROM pedidos;

-- Ranking del más caro al más barato por categoría
SELECT nombre, categoria, precio,
    RANK() OVER (PARTITION BY categoria ORDER BY precio DESC) AS rank
FROM productos;
14

Triggers y Funciones

Un Trigger (disparador) es una acción que la base de datos ejecuta automáticamente cuando ocurre un evento (INSERT, UPDATE o DELETE). Es como la alarma antihurto de una tienda: salta sola sin que el cajero tenga que pulsar un botón. Son el mecanismo perfecto para crear tablas de auditoría automáticas e invisibles para los programadores del backend.

⚠️
Atención: El muro de PL/pgSQL
Los triggers no usan SQL normal. En PostgreSQL usan un lenguaje de programación incrustado llamado PL/pgSQL (con variables, bucles FOR y condicionales IF). Puede parecer arameo al principio, pero permite delegar lógica crítica directamente al motor de la base de datos, haciéndola imposible de evadir por tu propia aplicación.
SQL
-- 1. Función que inserta en el log
CREATE OR REPLACE FUNCTION fn_auditar_productos() RETURNS TRIGGER AS $$
BEGIN
    IF NEW.precio <> OLD.precio THEN
        INSERT INTO log_precios (prod_id, precio_viejo, precio_nuevo)
        VALUES (OLD.id, OLD.precio, NEW.precio);
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 2. El Trigger que llama a la función en cada UPDATE
CREATE TRIGGER trg_auditar_precios
AFTER UPDATE ON productos FOR EACH ROW EXECUTE FUNCTION fn_auditar_productos();
15

Esquema Producción E-commerce

Este esquema resume las mejores prácticas de la guía, usando UUIDs, Constraints y buenas relaciones.

SQL
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE usuarios (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, -- UUID oculta a atacantes cuántos usuarios tienes
    email VARCHAR(150) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE productos (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    sku VARCHAR(50) UNIQUE NOT NULL,
    precio NUMERIC(10,2) NOT NULL CHECK (precio > 0),
    stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0)
);

CREATE TABLE pedidos (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    usuario_id UUID REFERENCES usuarios(id),
    estado VARCHAR(20) DEFAULT 'pendiente'
);

CREATE TABLE pedido_items (
    pedido_id UUID REFERENCES pedidos(id) ON DELETE CASCADE,
    producto_id UUID REFERENCES productos(id),
    cantidad INTEGER NOT NULL CHECK (cantidad > 0),
    precio_fijado NUMERIC(10,2) NOT NULL, -- Precio al momento de comprar
    PRIMARY KEY (pedido_id, producto_id)
);
16

Los 10 Pitfalls (Cuidado en Producción)

# Pitfall / Error Común Consecuencia Solución Correcta
01 UPDATE o DELETE sin WHERE Modificas o borras TODA la tabla de la base de datos irreversiblemente. Escribe primero el WHERE, o haz un SELECT de prueba antes.
02 SQL Injection Si interpolas variables (ej. "SELECT * FROM x WHERE a = " + obj.a), un usuario puede inyectar código malicioso y vaciarte las tablas. Usa siempre consultas preparadas / parámetros (? o $1).
03 Usar FLOAT para dinero Errores invisibles de redondeo en céntimos (IEEE 754) que te cuadrarán mal las cuentas y enfadarán a Hacienda. Usa siempre tipos decimales exactos como NUMERIC(10,2) o guarda los céntimos en INTEGER.
04 No hacer transacciones en pagos Si falla a la mitad, cobras a un usuario pero no le creas el pedido, o le restas stock de un producto fantasma. Envuelve procesos multi-tabla en un BEGIN; ... COMMIT;
05 Falta de Índices Una base de datos rápida en local pasará a tardar 30 segundos por query cuando tengas 1 millón de filas (Full Table Scans). Usa EXPLAIN y añade índices a las columnas usadas en WHERE, JOIN o de alta cardinalidad.
06 Contraseñas en claro Si tu BD se filtra por un ataque, las contraseñas de tus usuarios serán públicas. Demanda legal asegurada (RGPD). Guarda siempre Hashes usando algoritmos como bcrypt o argon2.
07 Exceso de Índices Las escrituras (INSERT/UPDATE) se volverán lentísimas, porque cada cambio obliga a reconstruir los índices. Solo indexa lo imprescindible. Si lees mucho y escribes poco, bien.
08 Ignorar las Constraints El código backend siempre tiene bugs. Si la BD no rechaza datos erróneos (precios negativos, nulls), tu sistema colapsará. Pon NOT NULL, UNIQUE y CHECK en las tablas para que actúen de escudo final.
09 Falta de Backups o de testearlos Ataque Ransomware o borrado accidental destruyen el proyecto. "Si el backup no ha sido restaurado, no es un backup real". Haz pg_dump automáticos (cron) y testea la restauración cada mes en un entorno seguro.
10 Transacciones abiertas colgando Abres un BEGIN, el backend falla, y nunca envías el ROLLBACK. La BD queda con "Locks" bloqueando todas las tablas, cayendo la aplicación entera. Usa gestores de conexión y control de transacciones de tu framework que cierren la conexión o hagan rollback automáticamente on-error.
17

Gran Reto Final

Construye desde cero el esquema completo de una tienda online funcional (E-commerce) y demuestra tu nivel. Deberás aplicar todo:

Resumen: Tu Hoja de Ruta SQL

Nivel Temas Secciones
🟢 PrincipianteInstalación, CRUD, tipos de datos1 — 3 + Mini-Lab 1
🟡 IntermedioFiltros, JOINs, agregaciones, GROUP BY4 — 5 + Mini-Lab 2
🔵 AvanzadoÍndices, CTEs, Vistas, Transacciones6 — 8 + Mini-Lab 3
🟣 ProducciónSeguridad, backups, migraciones, deploy9 — 10
⚫ ProfesionalWindow Functions, Triggers, Normalización, Diseño11 — 15
🔴 ConsolidaciónPitfalls, Gran Reto Final16 — 17
🚀
¿Quieres ir más allá?
Explora: Replicación (read replicas para alta disponibilidad), Particionamiento (dividir tablas enormes por fecha/rango), Full-Text Search nativo de PostgreSQL, y PgBouncer para connection pooling en aplicaciones de alta concurrencia.
18

Solución Maestra (El código que lo unifica todo)

Vuelve a la sección 15 para ver la estructura base de las tablas del E-commerce, pero aquí te presento el componente más avanzado de la solución: el procedimiento almacenado. Este bloque de código PL/pgSQL encapsula todo lo aprendido: declaración de variables, condicionales, validación de stock, actualización de inventario y registro de compra, todo ejecutándose de forma atómica en una única transacción dentro de la base de datos.

SQL
-- 5. Lógica de Negocio (Procedimiento Almacenado)
-- Procesa un pedido validando stock y calculando totales de manera atómica

CREATE OR REPLACE PROCEDURE sp_registrar_compra(
    p_usuario_id UUID,
    p_producto_id UUID,
    p_cantidad INTEGER
)
LANGUAGE plpgsql AS $$
DECLARE
    v_precio NUMERIC(10,2);
    v_pedido_id UUID;
BEGIN
    -- 1. FASE DE VALIDACIÓN: Obtener precio y validar stock
    SELECT precio INTO v_precio FROM productos_pro 
    WHERE id = p_producto_id AND stock >= p_cantidad;

    IF NOT FOUND THEN
        RAISE EXCEPTION 'Producto no encontrado o stock insuficiente';
    END IF;

    -- 2. FASE DE ACCIÓN: Descontar stock del inventario
    UPDATE productos_pro SET stock = stock - p_cantidad WHERE id = p_producto_id;

    -- 3. FASE DE REGISTRO: Crear cabecera del pedido
    INSERT INTO pedidos_pro (usuario_id, total, estado)
    VALUES (p_usuario_id, v_precio * p_cantidad, 'pagado')
    RETURNING id INTO v_pedido_id;

    -- 4. FASE DE DETALLE: Insertar los items del pedido
    INSERT INTO pedido_items (pedido_id, producto_id, cantidad, precio_unit)
    VALUES (v_pedido_id, p_producto_id, p_cantidad, v_precio);

    -- 5. FINALIZACIÓN: El COMMIT es implícito si no hubo errores
    RAISE NOTICE 'Venta registrada con éxito. Pedido ID: %', v_pedido_id;
END;
$$;