🐘 2.8 · PostgreSQL: la base de datos relacional más avanzada del open source

⏱ 6h 00min ⚡ 100 XP 🏅 Profesional Junior 📖 Backend FullStack
"PostgreSQL no es solo una BD: es el motor de datos que usa Instagram, Spotify y Reddit."

🎯 Objetivo del tema

Al terminar este tema serás capaz de: Al terminar este tema serás capaz de usar PostgreSQL con Node.js (vía el paquete pg): conectarte, hacer queries, usar prepared statements, hacer JOINs entre tablas, usar transacciones, y explotar las ventajas de PostgreSQL sobre MySQL (tipos avanzados, JSON nativo, full-text search). Es la BD que el mercado moderno prefiere para proyectos nuevos.
🎬
Video del instructor

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.

1. PostgreSQL vs MySQL: las 7 diferencias clave

Cuándo usar cada una y por qué PostgreSQL gana para proyectos nuevos.

2. Conectar Node.js a PostgreSQL con el paquete pg

Pool de conexiones, queries, y prepared statements.

3. SQL avanzado: JOIN, GROUP BY, HAVING

Las consultas que unen tablas y agregan datos.

4. Transacciones: atomicidad y consistencia

BEGIN, COMMIT, ROLLBACK: cómo agrupar operaciones que deben ser atómicas.

5. Tipos de datos avanzados: JSON, ARRAY, UUID

Lo que PostgreSQL puede hacer y MySQL no (tan bien).

6. Índices y optimización de queries

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.

📷 Imagen referencial: Diagrama de relaciones PostgreSQL: 4 tablas (clientes, pedidos, productos, order_items) con foreign keys y relaciones uno-a-muchos y muchos-a-muchos.
AspectoMySQLPostgreSQL
Año de creación19951989 (más viejo, más maduro)
FilosofíaRápido y simple.Robusto y extensible.
Tipos de datos avanzadosLimitado (JSON básico).JSONB, ARRAY, UUID, hstore, rangos, GIS.
Full-text searchBásico.Avanzado, con múltiples idiomas.
ConcurrenciaBloquea en escritura.MVCC (lectura/escritura sin bloqueos).
Estándar SQLVariación.El más cercano al estándar SQL.
Quién lo usaWordPress, Facebook (Histórico).Instagram, Spotify, Reddit, Notion, Apple.
⭐ PostgreSQL = mejor para el futuro: Para el mercado laboral 2024-2025: PostgreSQL está creciendo más rápido que MySQL en ofertas de empleo, startups, y nuevos proyectos. MySQL sigue siendo masivo por WordPress y legacy. Si vas a aprender UNA BD nueva además de MySQL, que sea PostgreSQL.
🎬
Video del instructor

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

⭐ $1 $2 $3, no ?: USIEMPRE $1, $2, $3 (no ?) como placeholders. La sintaxis $1 es de PostgreSQL (con $). MySQL/MariaDB usan ? o :nombre. Es la diferencia más visible. Y SIEMPRE pool.query() con un segundo argumento (array de parámetros) en vez de concatenar: NUNCA 'SELECT * FROM usuarios WHERE email = ' + email.
🎬
Video del instructor

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 JOINQué haceEjemplo
INNER JOINSolo filas que coinciden en AMBAS tablas.Usuarios CON pedidos.
LEFT JOINTodas las filas de la izq, con coincidencias de la der (NULL si no).Todos los usuarios, tengan o no pedidos.
RIGHT JOINTodas las filas de la der, con coincidencias de la izq.Raro en PostgreSQL.
FULL OUTER JOINTodas 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

⭐ WHERE antes, HAVING después: La diferencia entre WHERE y HAVING: WHERE filtra ANTES de agrupar, HAVING filtra DESPUÉS. Si quieres 'usuarios con más de 5 pedidos', es HAVING COUNT(...) > 5, no WHERE. Confundirlos es uno de los errores más comunes en SQL.
🎬
Video del instructor

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

⭐ Transacciones solo cuando son necesarias: NUNCA uses transacciones para queries simples de lectura (SELECT). Usa transacciones solo cuando tienes 2+ operaciones que DEBEN ser atómicas (transferir dinero, crear pedido+items, etc.). El uso innecesario de transacciones bloquea otras queries y mata el rendimiento. Es como usar un candado en la puerta cuando no hay nada valioso adentro.
🎬
Video del instructor

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.

TipoPara qué sirveEjemplo
JSONBDatos JSON indexables.{ nombre: 'Ana', redes: { twitter: '@ana' } }
TEXT[]Array de strings.{ 'php', 'javascript', 'python' }
INTEGER[]Array de enteros.{ 1, 2, 3, 4, 5 }
UUIDIdentificador único universal.550e8400-e29b-41d4-a716-446655440000
INETDirección IP.192.168.1.1
DATE / TIMESTAMPFechas 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

⭐ JSONB > JSON: JSONB NO es lo mismo que JSON. JSONB es la versión BINARIA: más rápida de consultar, indexable con GIN, y elimina espacios duplicados. Si vas a hacer queries sobre campos JSON (ej: WHERE metadata->>'marca' = 'Asus'), USA JSONB. Si solo guardas y lees completo, da igual. Es un detalle que mucha gente pasa por alto.
🎬
Video del instructor

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
HashSolo 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
PartialSolo indexar parte de la tabla (ej: WHERE activo=TRUE).idx_pedidos_activos
⭐ Menos indexes = escrituras más rápidas: NO indexes de más. Cada índice adicional hace las escrituras (INSERT/UPDATE/DELETE) MÁS LENTAS, porque hay que actualizar el índice también. La regla: indexa solo las columnas que uses en WHERE y ORDER BY frecuentemente. Si tienes 50 columnas y 5 indexes, probablemente tienes demasiados.
🎬
Video del instructor

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

  1. Diseña las 5 tablas: usuarios, cursos, lecciones, inscripciones, resenas.
  2. Define las relaciones con foreign keys.
  3. Crea el script SQL con tipos apropiados (UUID, TIMESTAMPTZ, JSONB para metadata del curso).
  4. Inserta datos de prueba: 3 usuarios, 3 cursos, 10 lecciones, 5 inscripciones, 3 reseñas.
  5. Query 1: Top 5 cursos por número de inscripciones (JOIN + GROUP BY + ORDER BY + LIMIT).
  6. Query 2: Progreso de un usuario: % de lecciones completadas (JOIN + cálculo).
  7. Trigger: al insertar una reseña, actualiza el promedio de calificaciones del curso.
  8. Crea los índices apropiados en las columnas de búsqueda.
  9. 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.'
  10. Aplica 2 mejoras. Mide tiempos con EXPLAIN ANALYZE.
  11. 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

Error 1: Concatenar variables en queries SQL (SQL injection).
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.
Error 2: No usar pool de conexiones y abrir una nueva por cada query.
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.
Error 3: Usar OFFSET para paginación en tablas grandes.
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.
Error 4: No usar transacciones cuando las operaciones deben ser atómicas.
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.
Error 5: Crear índices en columnas que no los necesitan.
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

  1. Instala PostgreSQL localmente o usa un servicio gratuito (Supabase, Neon, Railway).
  2. Crea la BD 'plataforma_cursos' y las tablas: usuarios, cursos, lecciones, inscripciones.
  3. Inicializa el proyecto Node: 'api-cursos-pg'. npm init -y. type:module.
  4. Instala: npm install express pg. Dev: -D nodemon.
  5. Crea db.js con el pool de conexiones.
  6. Crea schema.sql con el CREATE TABLE de todas las tablas + datos de prueba.
  7. Crea routes/cursos.js con las rutas: GET /, GET /:id, POST /, PUT /:id, DELETE /:id, GET /populares.
  8. Crea routes/inscripciones.js: POST /cursos/:id/inscribirse, GET /usuarios/:id/cursos.
  9. Usa prepared statements con $1, $2 en TODAS las queries.
  10. Implementa validación: si falta nombre o precio, devuelve 400.
  11. Agrega búsqueda: GET /cursos?tag=javascript filtra por tag usando ANY(tags).
  12. Inicia con npm run dev. Prueba con curl todas las rutas.
  13. Mide tiempos: agrega 10,000 cursos de prueba y mide el tiempo de GET /.
  14. Agrega índices en columnas de búsqueda (nombre, tags). Vuelve a medir.
  15. 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.

📓 Tu cuaderno: Anota pseudocódigo, diagramas, errores que encontraste y respuestas a "explica sin código". La escritura manual refuerza tu razonamiento. Tu profesor puede pedirte que subas fotos de páginas específicas.