Relational Database

PostgreSQL

PostgreSQL es la base de datos relacional que se toma en serio la corrección. Sus tipos fuertes, transacciones reales, JSONB y un sistema de extensiones la convierten en la opción predeterminada para nuevas aplicaciones.

intermediate16 min readUpdated 16 sept 2026
schema.sql
sql
-- schema.sql
CREATE TABLE orders (
  id           bigserial PRIMARY KEY,
  user_id      bigint NOT NULL REFERENCES users (id),
  status       text NOT NULL DEFAULT 'pending',
  total_cents  integer NOT NULL CHECK (total_cents >= 0),
  created_at   timestamptz NOT NULL DEFAULT now(),
  metadata     jsonb NOT NULL DEFAULT '{}'::jsonb
);

CREATE INDEX orders_user_created_idx
  ON orders (user_id, created_at DESC);
Lanzamiento
1989
Licencia
PostgreSQL License (open source)
Modelo
Relacional + documento (JSONB)
Aislamiento predeterminado
Read Committed
Motor de almacenamiento
Heap con MVCC
Última versión mayor
17

Por que importa

Por qué Postgres sigue ganando

Estándares y corrección

Postgres sigue fielmente el estándar SQL, aplica constraints en el motor y trata la integridad de los datos como algo no negociable en lugar de una simple convención.

Un ecosistema de extensiones

PostGIS para geografía, pgvector para embeddings, pg_stat_statements para análisis de consultas. El núcleo se mantiene ligero mientras que las extensiones añaden dominios completos.

JSONB cuando lo necesitas

Un tipo JSON binario con indexación y operadores permite que los datos con forma de documento convivan con los datos relacionales sin necesidad de una segunda base de datos.

La imagen completa

Las tres ideas detrás de Postgres

Un modelo relacional tipado, transacciones que nunca mienten y un sistema de extensiones que permite que la base de datos crezca contigo.

Relaciones tipadas

Modelo

Las tablas, columnas y constraints describen la forma de tus datos, y el motor se niega a almacenar cualquier cosa que rompa las reglas.

Transacciones MVCC

Aislar

Los lectores nunca bloquean a los escritores. Cada transacción ve una instantánea consistente, con niveles de aislamiento sobre los que puedes razonar.

Conexiones en pool

Escalar

Postgres crea un proceso por conexión, por lo que un pooler como PgBouncer se coloca delante para mantener el coste de miles de clientes bajo control.

HTML5 de un vistazo

Qué incluye de fábrica

Tablas y constraints

PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK y NOT NULL aplican las reglas donde residen los datos.

Índices enriquecidos

Los índices B-tree, GIN, GiST, BRIN y hash cubren igualdad, rangos, texto completo y JSON.

EXPLAIN ANALYZE

Visualiza el plan real y los tiempos de cualquier consulta antes de intentar adivinar la solución.

MVCC

La concurrencia multi-versión otorga a cada transacción una instantánea estable sin bloqueos de lectura.

JSONB

Almacena, consulta e indexa documentos semi-estructurados junto a columnas normales.

Extensiones

CREATE EXTENSION añade capacidades que van desde la generación de UUID hasta la búsqueda geoespacial.

Modelo de datos

Una fila por pedido

Una fila representa un único pedido realizado por un usuario, con un estado mutable y un contenedor de metadatos en JSONB.

La tabla de pedidosPostgreSQL table
  • idbigserialClave primaria subrogada, generada por una secuencia
  • user_idbigintClave foránea a usuarios; el propietario del pedido
  • statustextEstado del ciclo de vida como pendiente, pagado o enviado
  • total_centsintegerMonto en unidades menores para evitar problemas de punto flotante con dinero
  • created_attimestamptzHora de inserción en UTC, almacenada con zona horaria
  • metadatajsonbExtras opcionales como códigos de cupón o información del dispositivo

Una fila representa un único pedido realizado por un usuario, con un estado mutable y un contenedor de metadatos en JSONB.

Una breve historia

Cuatro décadas haciéndolo bien

  1. 1986

    El proyecto POSTGRES de Berkeley

    El equipo de Michael Stonebraker inicia un sistema relacional para explorar la extensibilidad y los tipos avanzados.

    86
  2. 1996

    Postgres95 se convierte en PostgreSQL

    Se implementa el soporte para SQL y el proyecto cambia de nombre, abriéndose a una comunidad global.

    96
  3. 2010

    Replicación streaming y hot standby

    La replicación asíncrona integrada convierte las réplicas de lectura y el failover en una característica de primer nivel.

    10
  4. 2014

    Llega JSONB

    Un tipo JSON binario e indexable convierte a Postgres también en un almacén de documentos creíble.

    14
  5. 2020

    Columnas generadas y un ecosistema creciente

    El Postgres gestionado y extensiones como pgvector lo impulsan hacia cargas de trabajo de analítica e IA.

    20

La guia completa

PostgreSQL: Todo lo que necesitas saber

¿Qué es PostgreSQL?

PostgreSQL es una base de datos relacional de código abierto con fama de hacer las cosas básicas correctamente. Almacena los datos en tablas, aplica reglas mediante constraints, envuelve los cambios en transacciones reales y expone todo esto a través de SQL estándar. Se ha desarrollado continuamente desde la década de 1980 y está gobernada por una amplia comunidad en lugar de una sola empresa.

Esa longevidad se refleja en los detalles. Postgres tiene el sistema de tipos más rico de cualquier base de datos convencional, un mecanismo de extensiones que permite a terceros añadir capacidades completamente nuevas y un planificador de consultas que maneja desde una búsqueda puntual simple hasta una consulta analítica con window functions. Es la base de datos relacional predeterminada para nuevas aplicaciones en la mayoría de las empresas y está disponible como servicio gestionado en casi cualquier plataforma.

Si vas a elegir una sola base de datos para aprender a fondo, esta es la que más recompensa el tiempo invertido.

Por qué los equipos eligen Postgres

Tres propiedades explican la mayor parte de su popularidad.

Es correcto por defecto. Las restricciones se aplican en el motor, no en el código de la aplicación. Un CHECK no puede ser omitido por un servicio con errores, un FOREIGN KEY no puede ser ignorado por un proceso batch, y un índice UNIQUE no puede sufrir condiciones de carrera entre dos solicitudes concurrentes. La integridad de los datos se convierte en una propiedad del esquema.

Es extensible. En lugar de integrar cada funcionalidad en el núcleo, Postgres expone hooks para nuevos tipos, operadores, métodos de indexación y lenguajes procedimentales. Así es como PostGIS, pgvector, TimescaleDB y docenas de otros proyectos existen como extensiones en lugar de forks.

Habla SQL estándar. Las habilidades y las consultas son transferibles entre bases de datos, ORMs y herramientas. No tienes que aprender un dialecto propietario para empezar.

El modelo relacional, en breve

Una base de datos relacional almacena los datos en tablas compuestas por filas y columnas. Cada tabla tiene una primary key que identifica de forma única una fila, y las relaciones se expresan mediante foreign keys que apuntan a otras tablas. El objetivo es almacenar cada dato una sola vez y permitir que los joins lo reensamblen.

Consideremos los usuarios y los pedidos. En lugar de repetir el email de un cliente en cada pedido, almacenas el email una sola vez en users y haces referencia al usuario desde orders mediante user_id. Esto es la normalización, y evita la clásica anomalía en la que se actualiza una fila mientras que otras mil siguen conservando el valor antiguo.

La normalización no es una religión. La tercera forma normal es el estándar sensato, y la desnormalización deliberada —un order_count en caché, una vista materializada— es una decisión de rendimiento que tomas más adelante, basándote en mediciones reales.

Tipos de datos que realmente valen la pena

Postgres ofrece más tipos de los que la mayoría de los proyectos necesitan, pero hay un puñado que son fundamentales:

  • text — cadena de longitud variable sin límite arbitrario. Prefiérelo sobre varchar(n) a menos que el límite sea una regla de negocio real.
  • integer y bigint — números enteros. Usa bigint para cualquier cosa que pueda crecer sin límite, como los IDs de una secuencia.
  • numeric(p, s) — aritmética decimal exacta. Este es el tipo correcto para dinero, no real ni double precision.
  • timestamptz — una marca de tiempo almacenada en UTC con conciencia de la zona horaria. Prefiérelo siempre sobre timestamp para los datos de la aplicación.
  • uuid — un identificador de 128 bits, útil cuando los clientes generan IDs o cuando no quieres filtrar el conteo de registros.
  • jsonb — JSON binario que puedes indexar y consultar.
  • Arrays y tipos compuestos — text[], integer[] y tipos de fila personalizados, útiles para etiquetas y listas ordenadas pequeñas.

Vale la pena interiorizar el ejemplo del dinero: 0.1 + 0.2 no es 0.3 en punto flotante binario. Almacena los montos como unidades menores enteras (total_cents) o como numeric, nunca como float.

Creación de tablas y restricciones

Un esquema es un contrato. Cada restricción que declaras es una clase de error que evitas enviar a producción.

CREATE TABLE users (
  id         bigserial PRIMARY KEY,
  email      text NOT NULL UNIQUE,
  name       text NOT NULL CHECK (length(name) > 0),
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id          bigserial PRIMARY KEY,
  user_id     bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  status      text NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  total_cents integer NOT NULL CHECK (total_cents >= 0),
  created_at  timestamptz NOT NULL DEFAULT now(),
  metadata    jsonb NOT NULL DEFAULT '{}'::jsonb
);

Las decisiones importantes aquí son deliberadas. NOT NULL descarta toda una familia de errores por falta de datos. UNIQUE en email impone el invariante en la base de datos, que es el único lugar donde una condición de carrera no puede anularlo. La CHECK en status documenta e impone la máquina de estados. ON DELETE CASCADE decide qué sucede cuando un usuario desaparece, en lugar de dejar registros huérfanos.

Es preferible añadir las restricciones en la misma migración que crea la tabla. Añadir una NOT NULL o una clave foránea más tarde implica tener que limpiar primero cualquier fila inválida que se haya acumulado mientras la regla no existía.

Consultas con joins

La mayoría de las consultas reales combinan tablas. Un join empareja filas de dos relaciones basándose en una condición, y la elección del tipo de join decide qué sucede con las filas que no tienen coincidencia.

SELECT u.email,
       o.id,
       o.total_cents
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid'
ORDER BY o.created_at DESC
LIMIT 50;

JOIN (o INNER JOIN) mantiene únicamente los pares que coinciden. LEFT JOIN mantiene todas las filas de la izquierda y rellena la derecha con NULL cuando no hay coincidencia; es la forma estándar de preguntar “qué usuarios no tienen pedidos”:

SELECT u.email, count(o.id) AS orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.email
HAVING count(o.id) = 0;

Los agregados como count, sum, avg, min y max colapsan grupos de filas en una sola. Cada columna en el SELECT que no esté dentro de un agregado debe aparecer en el GROUP BY. La cláusula WHERE filtra las filas antes de agrupar; HAVING filtra los grupos después. Confundirlas es una fuente común de resultados confusos.

Índices y EXPLAIN ANALYZE

Un índice es una estructura ordenada que permite al planificador encontrar filas sin tener que escanear toda la tabla. El valor predeterminado es un B-tree, que sirve para predicados de igualdad y rango, y soporta ORDER BY directamente.

CREATE INDEX orders_user_created_idx
  ON orders (user_id, created_at DESC);

Un índice compuesto como este cubre consultas que filtran por user_id y ordenan por created_at. El orden de las columnas es fundamental: la columna situada más a la izquierda debe aparecer en la consulta para que el índice sea útil al filtrar. Esta es la regla del prefijo izquierdo (leftmost-prefix rule), y es la razón por la cual el diseño de índices comienza basándose en las consultas y no en las columnas.

Otros tipos de índices cubren diferentes estructuras:

  • GIN — índices invertidos para jsonb, arrays y búsqueda de texto completo (tsvector).
  • GiST — geometría, rangos y búsqueda del vecino más cercano, utilizado intensamente por PostGIS.
  • BRIN — índices diminutos sobre datos ordenados naturalmente, como tablas de timestamps de solo inserción (append-only).
  • Hash — búsquedas únicamente de igualdad; rara vez son necesarios ya que el B-tree se encarga de ellas.

Nunca adivines si se está utilizando un índice. Pregúntale al planificador:

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 10;

La salida muestra el árbol del plan, el coste estimado y —con ANALYZE— las filas y el tiempo reales.

Limit  (cost=0.43..12.94 rows=10 width=48)
       (actual time=0.031..0.079 rows=10 loops=1)
  ->  Index Scan Backward using orders_user_created_idx on orders
        (cost=0.43..521.10 rows=417 width=48)
        (actual time=0.029..0.070 rows=10 loops=1)
        Index Cond: (user_id = 42)
Planning Time: 0.184 ms
Execution Time: 0.108 ms

Léelo de adentro hacia afuera. Aquí, el Index Scan con Index Cond: (user_id = 42) confirma que el índice compuesto está cumpliendo su función. Busca un Seq Scan en una tabla grande donde esperabas un Index Scan, y busca una brecha considerable entre las filas estimadas y las reales, lo que generalmente indica estadísticas obsoletas. Ejecuta ANALYZE orders; para actualizarlas, y EXPLAIN (ANALYZE, BUFFERS) para ver cuántos datos se leyeron desde la caché frente al disco.

Transacciones y MVCC

Una transacción agrupa sentencias para que todas surtan efecto o ninguna lo haga. Postgres implementa las transacciones mediante MVCC: en lugar de bloquear filas para los lectores, mantiene múltiples versiones y asigna una instantánea (snapshot) a cada transacción.

BEGIN;
UPDATE accounts SET balance_cents = balance_cents - 5000 WHERE id = 1;
UPDATE accounts SET balance_cents = balance_cents + 5000 WHERE id = 2;
COMMIT;

Si algo falla, ROLLBACK deshace la transacción completa. El código de la aplicación debe envolver las escrituras de varios pasos en una transacción y estar preparado para reintentar la operación en caso de fallos de serialización cuando se utilicen los niveles de aislamiento más estrictos.

Los niveles de aislamiento deciden qué puede observar una transacción:

  • Read Committed (por defecto) — cada sentencia ve una instantánea actualizada; es adecuado para la mayoría de las cargas de trabajo.
  • Repeatable Read — toda la transacción ve una única instantánea; es útil cuando se leen las mismas filas repetidamente.
  • Serializable — las transacciones se comportan como si se ejecutaran una tras otra; es la garantía más fuerte y la que tiene más probabilidades de requerir reintentos.

MVCC tiene un coste: las filas actualizadas y eliminadas dejan atrás tuplas muertas. Autovacuum las recupera en segundo plano. Las transacciones de larga duración retrasan la limpieza e inflan las tablas, por lo que es recomendable mantener las transacciones cortas y monitorizar el vacuum en tablas con mucha rotación de datos.

Connection pooling con PgBouncer

Postgres gestiona cada conexión de cliente mediante un proceso independiente del sistema operativo. Esto es robusto, pero significa que las conexiones son más pesadas que en MySQL, y unos pocos cientos de clientes inactivos pueden consumir una cantidad considerable de memoria. El límite práctico es max_connections, y superarlo produce errores en lugar de una degradación gradual.

La solución es un connection pooler. PgBouncer se sitúa entre la aplicación y Postgres, mantiene un pool pequeño de conexiones reales y multiplexa múltiples conexiones de clientes sobre ellas. Ofrece tres modos:

  • Session pooling — se mantiene una conexión al servidor durante toda la sesión del cliente.
  • Transaction pooling — se mantiene una conexión al servidor solo durante una transacción; es la opción más común para aplicaciones web.
  • Statement pooling — se mantiene una conexión para una sola sentencia; es el modo más agresivo y restrictivo.

El transaction pooling rompe las funcionalidades con alcance de sesión, como SET, los advisory locks mantenidos entre sentencias y LISTEN. Utiliza SET LOCAL dentro de las transacciones y verifica que tu ORM se comporte correctamente en modo de transacción.

Independientemente de lo que elijas, implementa también un pool en el lado de la aplicación. Un pool por proceso con un máximo razonable, sumado a un pooler delante, es la arquitectura estándar. Nunca abras una conexión nueva por cada solicitud.

JSONB: cuándo usarlo

jsonb almacena JSON en un formato binario descompuesto que admite indexación y un conjunto rico de operadores. Es genuinamente útil, pero también es fácil abusar de él.

SELECT id, metadata->>'coupon' AS coupon
FROM orders
WHERE metadata @> '{"channel": "mobile"}';

El operador de contención @> puede utilizar un índice GIN:

CREATE INDEX orders_metadata_idx ON orders USING gin (metadata);

Usa JSONB para datos cuya estructura sea realmente variable: payloads de webhooks, respuestas de terceros, atributos definidos por el usuario o feature flags. No lo uses para evitar escribir una migración. Los campos que filtres, unas (join), restrinjas o agregues deben ser columnas con tipos reales. La prueba es sencilla: si te encuentras convirtiendo metadata->>'price' a un número en la mayoría de las consultas, debería haber sido un integer.

Un camino intermedio es el híbrido: campos estables como columnas y todo lo opcional en una bolsa attributes de JSONB. Esto te brinda restricciones e índices donde importan y flexibilidad donde no.

Extensiones que amplían las posibilidades

CREATE EXTENSION instala un módulo empaquetado en una base de datos. Hay algunas que vale la pena conocer por su nombre:

  • pg_stat_statements — registra el texto normalizado de las consultas junto con los tiempos y E/S; es lo primero que se debe habilitar al investigar el rendimiento.
  • PostGIS — tipos y funciones geográficas; la razón por la cual muchos equipos eligen Postgres para datos de ubicación.
  • pgvector — columnas de vectores e índices de vecinos más cercanos aproximados para embeddings y búsqueda semántica.
  • pgcrypto — funciones criptográficas como gen_random_uuid() en versiones antiguas.
  • pg_trgm — índices de trigramas para LIKE rápido y coincidencia difusa (fuzzy matching).

Habilita solo lo que utilices y recuerda que los proveedores gestionados a menudo requieren una configuración o una solicitud de soporte antes de que se pueda instalar una extensión.

Roles y permisos

Postgres separa los roles (que pueden iniciar sesión o poseer objetos) de los privileges (lo que un rol puede hacer). Otorga el privilegio mínimo necesario para que funcione.

CREATE ROLE app_user LOGIN PASSWORD 'secret';

GRANT CONNECT ON DATABASE shop TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE
  ON ALL TABLES IN SCHEMA public TO app_user;

ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;

La línea ALTER DEFAULT PRIVILEGES es importante porque se aplica a las tablas creadas posteriormente, no solo a las existentes. Evita conectarte desde la aplicación como el propietario de la base de datos o como un superusuario; una aplicación comprometida no debería poder DROP TABLE. Para las migraciones, utiliza un rol independiente con derechos DDL.

Backups y recuperación

Existen dos tipos de backups. Los backups lógicos utilizan pg_dump para generar un script portable o un archivo de una base de datos, y pg_dumpall para capturar roles y globales. Son sencillos y tolerantes a las versiones, pero más lentos de restaurar a gran escala.

pg_dump --format=custom --file=shop.dump shop
pg_restore --dbname=shop_restore shop.dump

Los backups físicos copian el directorio de datos y el write-ahead log. pg_basebackup junto con el archivado continuo de WAL permite la recuperación en un punto específico en el tiempo (point-in-time recovery), lo que te permite restaurar la base de datos a un momento exacto antes de una migración fallida. Este es el enfoque que utilizan los proveedores gestionados.

Independientemente de cuál elijas, la regla es la misma: automatízalo, almacénalo fuera del host principal y restáuralo regularmente en una base de datos de prueba. Un backup que nunca has restaurado es una esperanza, no un plan.

Búsqueda de texto completo sin servicios adicionales

Postgres tiene búsqueda de texto completo integrada, lo cual suele ser suficiente para evitar la ejecución de un clúster de búsqueda independiente. Funciona convirtiendo el texto en un tsvector de lexemas y las consultas en un tsquery, para luego compararlos.

ALTER TABLE posts
  ADD COLUMN search tsvector
    GENERATED ALWAYS AS (
      to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))
    ) STORED;

CREATE INDEX posts_search_idx ON posts USING gin (search);

SELECT id, title
FROM posts
WHERE search @@ plainto_tsquery('english', 'connection pooling')
ORDER BY ts_rank(search, plainto_tsquery('english', 'connection pooling')) DESC
LIMIT 20;

Una columna generada mantiene el vector sincronizado automáticamente, y el índice GIN hace que la coincidencia del @@ sea rápida. plainto_tsquery convierte de forma segura la entrada del usuario en una consulta, mientras que ts_rank ordena por relevancia. Puedes añadir ts_headline para resaltar las coincidencias en los resultados.

Recurre a un motor de búsqueda dedicado únicamente cuando necesites faceting, tolerancia a errores tipográficos (fuzzy search) a gran escala o ajuste de relevancia entre documentos. Para la mayoría de las aplicaciones, la versión integrada elimina una pieza móvil completa de la arquitectura.

Vistas y vistas materializadas

Una vista es una consulta almacenada que se comporta como una tabla. No almacena datos; es una forma nombrada de encapsular una estructura común y mantener los permisos consistentes.

CREATE VIEW paid_orders AS
SELECT id, user_id, total_cents, created_at
FROM orders
WHERE status = 'paid';

SELECT * FROM paid_orders WHERE created_at >= now() - interval '7 days';

Una vista materializada sí almacena el resultado, lo que hace que las agregaciones costosas sean económicas de leer, a cambio de que los datos puedan quedar obsoletos.

CREATE MATERIALIZED VIEW daily_revenue AS
SELECT date_trunc('day', created_at) AS day,
       sum(total_cents)             AS revenue_cents
FROM orders
WHERE status = 'paid'
GROUP BY 1;

REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue;

CONCURRENTLY se actualiza sin bloquear a los lectores, pero requiere un índice único en la vista. Programa las actualizaciones según tu tolerancia a los datos obsoletos y recuerda que una vista materializada es una caché: siempre puede reconstruirse a partir de las tablas base.

Cambiar esquemas de forma segura

En una base de datos en producción, el DDL adquiere bloqueos. El objetivo es evitar bloqueos ACCESS EXCLUSIVE prolongados que impidan las lecturas y escrituras mientras se reescribe una tabla.

ALTER TABLE orders ADD COLUMN channel text;
ALTER TABLE orders ALTER COLUMN channel SET DEFAULT 'web';

CREATE INDEX CONCURRENTLY orders_channel_idx ON orders (channel);

Añadir una columna nullable sin valor predeterminado es instantáneo en las versiones modernas de Postgres, y establecer un valor predeterminado solo afecta a los metadatos. CREATE INDEX CONCURRENTLY construye el índice sin mantener un bloqueo de escritura, aunque no puede ejecutarse dentro de una transacción y puede fallar, dejando un índice inválido que debe eliminarse y reintentarse. Añadir una restricción NOT NULL a una tabla grande debe hacerse por pasos: añadir una CHECK que se valide por separado y, posteriormente, convertirla.

Versiona cada cambio como una migración para que todos los entornos alcancen el mismo esquema en el mismo orden. Nunca edites una tabla de producción manualmente; olvidarás lo que hiciste.

Carga masiva con COPY

Insertar filas una a una es la forma más lenta de cargar datos. Postgres cuenta con COPY, que transmite los datos en un solo comando y puede ser un orden de magnitud más rápido que un bucle de INSERTs.

COPY orders (user_id, status, total_cents, created_at)
FROM '/data/orders.csv'
WITH (FORMAT csv, HEADER true);

COPY (SELECT id, email FROM users WHERE created_at > now() - interval '30 days')
TO '/tmp/recent-users.csv'
WITH (FORMAT csv, HEADER true);

COPY se ejecuta en el servidor, por lo que el archivo debe ser legible por el proceso de la base de datos; utiliza \copy en psql para leer desde el cliente. Para el código de la aplicación, la API copy del driver transmite las filas sin necesidad de preparar un archivo. Envuelve la carga en una transacción cuando necesites que sea una operación atómica (todo o nada), y elimina o reconstruye los índices después de cargas muy grandes, ya que mantener los índices durante una inserción masiva es el costo principal.

Las inserciones por lotes (batch inserts) son un punto medio más sencillo cuando COPY no es práctico:

INSERT INTO orders (user_id, status, total_cents)
VALUES (1, 'paid', 1999),
       (2, 'paid', 4599),
       (3, 'pending', 999);

Mejores prácticas

  • Elige text, bigint, numeric, timestamptz y jsonb deliberadamente en lugar de usar varchar y floats por defecto.
  • Declara NOT NULL, UNIQUE, CHECK y foreign keys en la misma migración que crea la tabla.
  • Diseña los índices basándote en consultas reales y confírmalos con EXPLAIN ANALYZE.
  • Mantén las transacciones cortas y elige el nivel de aislamiento más débil que siga siendo correcto.
  • Coloca un pooler como PgBouncer frente a la base de datos y utiliza pooling también en la aplicación.
  • Usa JSONB para datos genuinamente variables, no como una forma de saltarte el diseño del esquema.
  • Otorga el privilegio mínimo a los roles de la aplicación y mantén el DDL en un rol de migración.
  • Habilita pg_stat_statements, monitorea el autovacuum y restaura un backup de forma programada.
  • Versiona cada cambio de esquema como una migración para que los entornos se mantengan sincronizados.

Errores comunes

  • Almacenar dinero en double precision y perder centavos debido al redondeo.
  • Usar timestamp en lugar de timestamptz y almacenar horas locales por accidente.
  • Olvidar WHERE en un UPDATE o DELETE y afectar cada fila de la tabla.
  • Crear un índice por columna en lugar de índices compuestos que coincidan con la consulta.
  • Abrir una nueva conexión por solicitud y agotar max_connections.
  • Mantener una transacción abierta durante una llamada HTTP o una interacción del usuario.
  • Tratar NULL como si fuera igual a NULL; las comparaciones requieren IS NULL y IS NOT NULL.
  • Asumir que se está utilizando un índice sin verificar el plan de ejecución de la consulta.
  • Ejecutar consultas de la aplicación como propietario de la base de datos o superusuario.

Próximos pasos

Postgres premia la profundidad, y el siguiente paso natural es dominar el lenguaje que utiliza: lee la guía de SQL para perfeccionar los joins, las CTE y las window functions. Si estás evaluando alguna alternativa, MySQL cubre la otra base de datos relacional de código abierto dominante, MongoDB explica el modelo de documentos y Redis muestra la capa de caché que suele situarse delante de un almacenamiento relacional.

En la practica

Esquema, consulta, plan, transacción

Las cuatro cosas que más haces: definir una tabla, unirla, indexarla y cambiarla atómicamente.

schema.sql
CREATE TABLE users (
  id         bigserial PRIMARY KEY,
  email      text NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE orders (
  id          bigserial PRIMARY KEY,
  user_id     bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  status      text NOT NULL DEFAULT 'pending'
                CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  total_cents integer NOT NULL CHECK (total_cents >= 0),
  created_at  timestamptz NOT NULL DEFAULT now(),
  metadata    jsonb NOT NULL DEFAULT '{}'::jsonb
);

Modelado de datos semi-estructurados

JSONB es excelente para atributos genuinamente opcionales, pero los campos que filtras y restringes deben ir en columnas.

Preferir
CREATE TABLE products (
  id         bigserial PRIMARY KEY,
  name       text NOT NULL,
  price_cents integer NOT NULL,
  attributes jsonb NOT NULL DEFAULT '{}'::jsonb
);

CREATE INDEX products_attributes_idx
  ON products USING gin (attributes);
Evitar
CREATE TABLE products (
  id   bigserial PRIMARY KEY,
  data jsonb NOT NULL
);

-- Every query now casts text and the database
-- cannot enforce that price is a number.
SELECT (data->>'price')::int FROM products;

Indexación para la consulta que ejecutas

Un índice debe coincidir con el WHERE y ORDER BY de una consulta real. Indexar cada columna ralentiza las escrituras y no aporta nada.

Preferir
CREATE INDEX orders_user_created_idx
  ON orders (user_id, created_at DESC);
Evitar
CREATE INDEX ON orders (id);
CREATE INDEX ON orders (user_id);
CREATE INDEX ON orders (status);
CREATE INDEX ON orders (created_at);
-- four single-column indexes that a
-- composite index would cover alone

Compromisos

¿Debería Postgres ser tu base de datos predeterminada?

Postgres encaja en la gran mayoría de las aplicaciones. Vale la pena saber en qué puntos requiere más esfuerzo de tu parte.

Strengths

  • Corrección sin supervisión constante

    Los constraints, las transacciones reales y los tipos fuertes significan que la base de datos rechaza estados inválidos, evitando que los bugs de la aplicación corrompan los datos silenciosamente.

  • Una base de datos para muchas formas

    Tablas relacionales, documentos JSONB, búsqueda de texto completo e incluso embeddings vectoriales coexisten, eliminando mucha complejidad operativa.

  • Una comunidad saludable e independiente

    El desarrollo es abierto y neutral respecto a los proveedores, los lanzamientos son predecibles y existen ofertas gestionadas en cada nube principal.

Trade-offs

  • Las conexiones no son gratuitas

    Cada conexión es un proceso de backend. Sin un pooler, unos pocos cientos de clientes de aplicación pueden agotar la memoria y alcanzar el max_connections.

  • El Vacuum es una tarea real

    MVCC deja tuplas muertas. El autovacuum suele bastar, pero las cargas de trabajo con actualizaciones intensivas requieren monitoreo y ajustes ocasionales.

  • El tuning premia la experiencia

    Los valores predeterminados son sensatos, pero work_mem, shared_buffers y la configuración del planificador importan bajo carga y llevan tiempo de aprender bien.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender PostgreSQL?

Nuestro tutorial interactivo te guia a traves de PostgreSQL paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.