🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
En el Tema 2.4 aprendiste MySQL: la base de datos más popular del mundo para aplicaciones web. Hoy conocerás su prima más poderosa: PostgreSQL, la BD relacional open source más avanzada. Es la elección de empresas como Instagram, Spotify, Reddit, Notion y Apple para sus sistemas críticos. La buena noticia: si sabes MySQL, PostgreSQL es muy similar. Las diferencias son avanzadas, no básicas.
Cuándo usar cada una y por qué PostgreSQL gana para proyectos nuevos.
Pool de conexiones, queries, y prepared statements.
Las consultas que unen tablas y agregan datos.
BEGIN, COMMIT, ROLLBACK: cómo agrupar operaciones que deben ser atómicas.
Lo que PostgreSQL puede hacer y MySQL no (tan bien).
EXPLAIN ANALYZE, índices, y cómo evitar queries lentas.
2.8.1 PostgreSQL vs MySQL: las 7 diferencias clave
PostgreSQL y MySQL son las dos bases de datos relacionales open source más usadas del mundo. La diferencia es de FILOSOFÍA: MySQL es rápida y simple (buena para web tradicional), PostgreSQL es robusta y extensible (buena para sistemas complejos). Para proyectos nuevos en 2025, PostgreSQL es generalmente la mejor elección.
| Aspecto | MySQL | PostgreSQL |
|---|---|---|
| Año de creación | 1995 | 1989 (más viejo, más maduro) |
| Filosofía | Rápido y simple. | Robusto y extensible. |
| Tipos de datos avanzados | Limitado (JSON básico). | JSONB, ARRAY, UUID, hstore, rangos, GIS. |
| Full-text search | Básico. | Avanzado, con múltiples idiomas. |
| Concurrencia | Bloquea en escritura. | MVCC (lectura/escritura sin bloqueos). |
| Estándar SQL | Variación. | El más cercano al estándar SQL. |
| Quién lo usa | WordPress, Facebook (Histórico). | Instagram, Spotify, Reddit, Notion, Apple. |
El administrador aún no ha insertado un video para esta sección.
2.8.2 Conectar Node.js a PostgreSQL con el paquete pg
El paquete oficial de PostgreSQL para Node.js es pg (node-postgres). Te da un cliente y un pool de conexiones. El pool es importante: reutiliza conexiones en vez de abrir y cerrar una por cada query, lo que es MUCHO más rápido bajo carga.
// db.js: pool de conexiones a PostgreSQL
import pg from 'pg';
const { Pool } = pg;
export const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'mi_app',
user: 'postgres',
password: 'mi_password', // en producción: variable de entorno
max: 20, // máximo de conexiones simultáneas
idleTimeoutMillis: 30000,
});
// Hacer una query simple
const result = await pool.query('SELECT NOW()');
console.log(result.rows[0]);
// Query parametrizada (prepared statement, previene SQL injection)
const { rows } = await pool.query(
'SELECT * FROM usuarios WHERE email = $1',
['ana@mail.com']
);
// Liberar el pool al cerrar la app
process.on('SIGTERM', () => pool.end());Pool de conexiones con pg
El administrador aún no ha insertado un video para esta sección.
2.8.3 SQL avanzado: JOIN, GROUP BY, HAVING
Las consultas reales casi nunca viven en una sola tabla: usuarios con sus pedidos, pedidos con sus productos, categorías con sus posts. Para unir datos de múltiples tablas, necesitas JOIN. Para agregar, GROUP BY y HAVING. Es el SQL que usas en apps reales.
| Tipo de JOIN | Qué hace | Ejemplo |
|---|---|---|
| INNER JOIN | Solo filas que coinciden en AMBAS tablas. | Usuarios CON pedidos. |
| LEFT JOIN | Todas las filas de la izq, con coincidencias de la der (NULL si no). | Todos los usuarios, tengan o no pedidos. |
| RIGHT JOIN | Todas las filas de la der, con coincidencias de la izq. | Raro en PostgreSQL. |
| FULL OUTER JOIN | Todas las filas de ambas, NULL donde no hay match. | Para reportes completos. |
-- Ejemplo: usuarios con su número de pedidos y gasto total
SELECT
u.id, u.nombre, u.email,
COUNT(p.id) AS num_pedidos,
COALESCE(SUM(p.total), 0) AS gasto_total
FROM usuarios u
LEFT JOIN pedidos p ON p.usuario_id = u.id
WHERE u.activo = TRUE
GROUP BY u.id, u.nombre, u.email
HAVING COUNT(p.id) > 0 -- solo usuarios CON pedidos
ORDER BY gasto_total DESC
LIMIT 10;
-- Explicación:
-- LEFT JOIN: todos los usuarios, tengan o no pedidos
-- GROUP BY: agrupa por usuario
-- COUNT/SUM: agrega por grupo
-- HAVING: filtra DESPUÉS de agrupar (WHERE filtra antes)JOIN + GROUP BY + HAVING en acción
El administrador aún no ha insertado un video para esta sección.
2.8.4 Transacciones: atomicidad y consistencia
Una transacción agrupa varias operaciones SQL en una unidad atómica: o se ejecutan TODAS, o NO se ejecuta NINGUNA. Es FUNDAMENTAL para operaciones donde la consistencia es crítica: transferir dinero entre cuentas, registrar un pedido con sus items, etc. PostgreSQL tiene transacciones ACID completas (Atomicidad, Consistencia, Isolation, Durability).
// Ejemplo: transferir $100 entre cuentas (DEBE ser atómico)
import { pool } from './db.js';
async function transferir(origenId, destinoId, monto) {
const client = await pool.connect();
try {
await client.query('BEGIN'); // iniciar transacción
// 1. Descontar del origen
await client.query(
'UPDATE cuentas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1',
[monto, origenId]
);
// Si no se afectó ninguna fila, el saldo era insuficiente
const check = await client.query('SELECT saldo FROM cuentas WHERE id = $1', [origenId]);
if (check.rows[0].saldo < 0) {
throw new Error('Saldo insuficiente');
}
// 2. Acreditar al destino
await client.query(
'UPDATE cuentas SET saldo = saldo + $1 WHERE id = $2',
[monto, destinoId]
);
await client.query('COMMIT'); // confirmar: ambas se aplican
} catch (err) {
await client.query('ROLLBACK'); // revertir: ninguna se aplica
throw err;
} finally {
client.release(); // devolver la conexión al pool
}
}Transacción: BEGIN / COMMIT / ROLLBACK
El administrador aún no ha insertado un video para esta sección.
2.8.5 Tipos de datos avanzados: JSON, ARRAY, UUID
PostgreSQL tiene tipos de datos que MySQL no tiene (o no tan bien): JSON nativo con índices, arrays, UUID, rangos, tipos geométricos, y más. Esto te permite modelar datos complejos sin tener que crear tablas auxiliares. Es una de las grandes ventajas de PostgreSQL para aplicaciones modernas.
| Tipo | Para qué sirve | Ejemplo |
|---|---|---|
| JSONB | Datos JSON indexables. | { nombre: 'Ana', redes: { twitter: '@ana' } } |
| TEXT[] | Array de strings. | { 'php', 'javascript', 'python' } |
| INTEGER[] | Array de enteros. | { 1, 2, 3, 4, 5 } |
| UUID | Identificador único universal. | 550e8400-e29b-41d4-a716-446655440000 |
| INET | Dirección IP. | 192.168.1.1 |
| DATE / TIMESTAMP | Fechas con o sin zona horaria. | 2026-08-04 22:00:00+00 |
-- Crear tabla con tipos avanzados
CREATE TABLE productos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nombre TEXT NOT NULL,
precio NUMERIC(10, 2) NOT NULL,
tags TEXT[] DEFAULT '{}', -- array de strings
metadata JSONB DEFAULT '{}', -- JSON indexable
creado_en TIMESTAMPTZ DEFAULT NOW()
);
-- Insertar con JSONB y array
INSERT INTO productos (nombre, precio, tags, metadata) VALUES (
'Laptop Gamer',
25000.00,
ARRAY['tecnologia', 'gaming', 'oferta'], -- array
'{"marca": "Asus", "ram": 16, "color": "negro"}'::jsonb -- JSONB
);
-- Consultar con operadores JSONB
SELECT * FROM productos WHERE metadata->>'marca' = 'Asus'; -- ->> es 'obtener como text'
SELECT * FROM productos WHERE tags @> ARRAY['gaming']; -- @> es 'contiene'
-- Consultar con arrays
SELECT * FROM productos WHERE 'gaming' = ANY(tags);JSONB, arrays y UUID en PostgreSQL
El administrador aún no ha insertado un video para esta sección.
2.8.6 Índices y optimización de queries
Una query sin índice en una tabla de 1 millón de filas tarda segundos. Con índice, milisegundos. Los índices son la optimización #1 en bases de datos: dominarlos es la diferencia entre un junior y un senior backend.
EXPLAIN ANALYZE: tu microscopio para queries
-- ¿Por qué esta query tarda 3 segundos?
EXPLAIN ANALYZE
SELECT * FROM pedidos WHERE usuario_id = 42;
-- Posibles resultados:
-- 'Seq Scan on pedidos' -> MAL: escanea TODA la tabla
-- 'Index Scan using idx_pedidos_usuario' -> BIEN: usa el índice
-- Crear el índice si no existe:
CREATE INDEX idx_pedidos_usuario ON pedidos(usuario_id);
-- Para queries con WHERE en JSONB:
CREATE INDEX idx_productos_metadata_marca ON productos USING GIN ((metadata->>'marca'));
-- Para queries con ORDER BY frecuente:
CREATE INDEX idx_usuarios_email ON usuarios(email DESC);EXPLAIN ANALYZE y creación de índices
| Tipo de índice | Úsalo cuando... | Ejemplo |
|---|---|---|
| B-tree (default) | WHERE con =, <, >, BETWEEN, ORDER BY. | idx_usuarios_email |
| Hash | Solo con = (más rápido que B-tree para =, sin orden). | Raro, B-tree suele ser suficiente. |
| GIN (Generalized Inverted) | JSONB, arrays, full-text search. | idx_productos_tags |
| Partial | Solo indexar parte de la tabla (ej: WHERE activo=TRUE). | idx_pedidos_activos |
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
PostgreSQL se aprende escribiendo queries reales, no leyendo documentación.
Misión: Diseña la base de datos para una 'plataforma de cursos online': tablas para usuarios, cursos, lecciones, inscripciones, reseñas. Implementa: query para listar los cursos más populares (con número de inscripciones), query para ver el progreso de un usuario, y un trigger que actualice el promedio de calificaciones al insertar una reseña. Pídele a la IA que revise tu diseño. Anota en tu cuaderno: ¿qué fue lo más difícil del diseño?
Pasos sugeridos
- Diseña las 5 tablas: usuarios, cursos, lecciones, inscripciones, resenas.
- Define las relaciones con foreign keys.
- Crea el script SQL con tipos apropiados (UUID, TIMESTAMPTZ, JSONB para metadata del curso).
- Inserta datos de prueba: 3 usuarios, 3 cursos, 10 lecciones, 5 inscripciones, 3 reseñas.
- Query 1: Top 5 cursos por número de inscripciones (JOIN + GROUP BY + ORDER BY + LIMIT).
- Query 2: Progreso de un usuario: % de lecciones completadas (JOIN + cálculo).
- Trigger: al insertar una reseña, actualiza el promedio de calificaciones del curso.
- Crea los índices apropiados en las columnas de búsqueda.
- Pídele a la IA: 'Tengo este esquema de plataforma de cursos. Sugiere 2 mejoras: índices, normalización, o features avanzados. NO me des código, dime QUÉ agregar.'
- Aplica 2 mejoras. Mide tiempos con EXPLAIN ANALYZE.
- Anota: 3 cosas que aprendiste sobre diseño de bases de datos que no anticipabas.
📓 Entregable: Script SQL completo (CREATE + INSERT + queries + trigger + índices), captura de las queries ejecutadas, y media página de cuaderno con tu reflexión.
🚫 Errores típicos de razonamiento
Por qué: Si haces pool.query('SELECT * FROM usuarios WHERE email = ' + email), un atacante puede inyectar: email = "' OR '1'='1" y tu query devuelve TODOS los usuarios. SIEMPRE usa placeholders parametrizados: pool.query('SELECT * FROM usuarios WHERE email = $1', [email]). Es la única forma segura.
Por qué: Abrir y cerrar una conexión a PostgreSQL toma 5-50ms. En una API con 1000 peticiones por segundo, son 50 segundos perdidos solo en conexiones. Un POOL de conexiones reutiliza las existentes, bajando ese tiempo a 0.1ms. Es la optimización #1 en cualquier API.
Por qué: OFFSET 100000 LIMIT 20 en una tabla de 1 millón de filas tarda segundos: la BD tiene que 'saltar' las 100,000 primeras. Solución: paginación por cursor con WHERE id < último_id_visto, que usa el índice y es instantánea. OFFSET está bien hasta ~10K registros; cursor para más.
Por qué: Si haces UPDATE cuenta1 SET saldo = saldo - 100 y luego UPDATE cuenta2 SET saldo = saldo + 100 sin transacción, y el proceso se cae entre las dos, el dinero se PIERDE. Con BEGIN/COMMIT, si algo falla, ROLLBACK deshace TODO. Es FUNDAMENTAL para operaciones críticas.
Por qué: Cada índice hace las escrituras (INSERT/UPDATE/DELETE) MÁS LENTAS, porque hay que actualizar el índice. Si creas índices en columnas que casi no usas en WHERE, estás sacrificando rendimiento de escritura sin ganar nada en lectura. La regla: indexa solo las columnas que uses en WHERE/ORDER BY frecuentemente.
🧪 Laboratorio práctico
PostgreSQL se aprende diseñando bases de datos reales, no leyendo sobre SQL.
Laboratorio: API de gestión de cursos con Express + PostgreSQL
Objetivo: Construir una API completa de gestión de cursos con Express y PostgreSQL. La API debe permitir: CRUD de cursos, inscripción de usuarios, listado de cursos populares, y búsqueda por tags. Practicarás: conexión con pg, prepared statements, queries avanzadas, y manejo de errores.
Pasos
- Instala PostgreSQL localmente o usa un servicio gratuito (Supabase, Neon, Railway).
- Crea la BD 'plataforma_cursos' y las tablas: usuarios, cursos, lecciones, inscripciones.
- Inicializa el proyecto Node: 'api-cursos-pg'. npm init -y. type:module.
- Instala: npm install express pg. Dev: -D nodemon.
- Crea db.js con el pool de conexiones.
- Crea schema.sql con el CREATE TABLE de todas las tablas + datos de prueba.
- Crea routes/cursos.js con las rutas: GET /, GET /:id, POST /, PUT /:id, DELETE /:id, GET /populares.
- Crea routes/inscripciones.js: POST /cursos/:id/inscribirse, GET /usuarios/:id/cursos.
- Usa prepared statements con $1, $2 en TODAS las queries.
- Implementa validación: si falta nombre o precio, devuelve 400.
- Agrega búsqueda: GET /cursos?tag=javascript filtra por tag usando ANY(tags).
- Inicia con npm run dev. Prueba con curl todas las rutas.
- Mide tiempos: agrega 10,000 cursos de prueba y mide el tiempo de GET /.
- Agrega índices en columnas de búsqueda (nombre, tags). Vuelve a medir.
- Haz commit: 'feat: API de cursos con Express + PostgreSQL, prepared statements e índices'.
📓 Entregable: Capturas de las rutas funcionando con curl, código de los 4 archivos JS, schema.sql, comparación de tiempos antes/después de índices, y commit en Git.