Errores de PostgreSQL Que Probablemente Estas Cometiendo (Y Como Solucionarlos)
Una guia completa para evitar los errores mas comunes de PostgreSQL que pueden comprometer el rendimiento, la integridad de datos y la mantenibilidad.
PostgreSQL es una de las bases de datos relacionales más poderosas y confiables disponibles hoy. Ofrece un rango extraordinario de características, desde indexación avanzada hasta búsqueda de texto completo y soporte JSON. Pero con ese poder viene complejidad, y hasta desarrolladores experimentados caen en trampas que pueden degradar silenciosamente el rendimiento, corromper datos o crear pesadillas de mantenimiento a largo plazo.
Esta guía cubre los anti-patrones más comunes de PostgreSQL, explica por qué son dañinos y te muestra el enfoque correcto para cada uno. Ya sea que estés construyendo una nueva aplicación o manteniendo una existente, auditar tu base de código contra esta lista vale completamente la pena.
Puntos Clave
- Tipos de Datos: Elegir el tipo de dato incorrecto (ej.
char(n),money,serial,timestamp without time zone) puede llevar a bugs sutiles y problemas de integridad de datos. Prefieretext,numeric,identity columnsytimestamptz. - Queries: Ciertas construcciones SQL como
NOT INcon columnas nullables yBETWEENpara rangos de timestamp producen resultados sorprendentes. UsaNOT EXISTSy comparaciones de rango explícitas en su lugar. - Diseño de Schema: Evita PostgreSQL Rules y Table Inheritance. Usa triggers y foreign keys o particionamiento nativo como alternativas.
- Seguridad: Nunca uses autenticación
trustsobre TCP/IP. Siempre usascram-sha-256para autenticación basada en contraseña.
Codificación de Base de Datos
El Problema
Cuando creas una base de datos PostgreSQL, puedes especificar la codificación de caracteres. Una opción es SQL_ASCII, que efectivamente es “ninguna codificación en absoluto”. No valida ni convierte datos de caracteres de ninguna manera. Esto significa que tu base de datos aceptará y almacenará felizmente una mezcla de UTF-8, Latin-1, Windows-1252 y cualquier otra codificación, a menudo dentro de la misma columna. Una vez que tienes codificaciones mezcladas en tus datos, no hay forma confiable de ordenar, buscar o comparar texto correctamente. Funciones como lower(), upper() y coincidencia de expresiones regulares producirán resultados incorrectos o errores directos.
Qué Hacer en Su Lugar
Siempre crea tus bases de datos con codificación UTF-8:
CREATE DATABASE myapp
ENCODING 'UTF8'
LC_COLLATE 'en_US.UTF-8'
LC_CTYPE 'en_US.UTF-8'
TEMPLATE template0;
UTF-8 puede representar cada carácter en el estándar Unicode, maneja datos multilingües correctamente, y es el estándar de facto para aplicaciones modernas.
La Excepción
No hay esencialmente ninguna razón válida para usar SQL_ASCII en un proyecto nuevo. El único escenario donde podrías encontrarlo es al migrar una base de datos legacy que ya contiene datos de codificación mezclada, e incluso entonces tu objetivo debería ser limpiar los datos y convertir a UTF-8 tan pronto como sea posible.
Uso de Herramientas
El Problema
Muchos desarrolladores se conectan a PostgreSQL usando psql -W o psql --password. La flag -W fuerza a psql a solicitar una contraseña antes de intentar conectarse. El problema es que PostgreSQL puede que ni siquiera requiera una contraseña para tu conexión (dependiendo de la configuración de pg_hba.conf). Si no la requiere, escribes tu contraseña para nada. Si la requiere, psql te solicitará automáticamente de todos modos. Usar -W no proporciona ningún beneficio y puede crear confusión sobre si la autenticación está realmente configurada.
Qué Hacer en Su Lugar
Simplemente ejecuta psql sin la flag -W:
psql -h localhost -U myuser -d mydb
Si se requiere una contraseña, psql te lo solicitará. Si no se requiere, te conectas directamente. De esta manera siempre obtienes feedback preciso sobre tu configuración de autenticación.
La Excepción
La flag -W puede ahorrar un round-trip al servidor si sabes que se requerirá una contraseña y quieres evitar el intento de conexión inicial. En la práctica, esto es insignificante. Si necesitas autenticación no interactiva, usa un archivo .pgpass o la variable de entorno PGPASSWORD (aunque esta última es desaconsejada por razones de seguridad).
Reescritura de Queries con Rules
El Problema
PostgreSQL Rules (el sistema CREATE RULE) reescriben queries antes de que se ejecuten. Aunque esto puede sonar conveniente, las rules son notoriamente difíciles de razonar. Interactúan mal con otras características, pueden cambiar silenciosamente el comportamiento de tus queries y producen resultados confusos cuando se combinan con cláusulas RETURNING, CTEs o triggers. La propia documentación de PostgreSQL desaconseja su uso en la mayoría de los escenarios.
Por ejemplo, una rule que redirige inserts a una tabla diferente puede silenciosamente descartar datos si la rule no tiene en cuenta todas las columnas, y depurar esto es extremadamente difícil porque la query que realmente se ejecuta es diferente de la que escribiste.
Qué Hacer en Su Lugar
Usa triggers para cualquier lógica que necesite ejecutarse automáticamente en respuesta a cambios de datos:
CREATE OR REPLACE FUNCTION log_update()
RETURNS trigger AS $$
BEGIN
INSERT INTO audit_log (table_name, record_id, changed_at)
VALUES (TG_TABLE_NAME, NEW.id, now());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_log_update
AFTER UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION log_update();
Los triggers son explícitos, bien entendidos y se componen correctamente con el resto de características de PostgreSQL.
La Excepción
El único lugar donde las rules todavía se usan comúnmente es para vistas actualizables, aunque incluso ahí, los triggers INSTEAD OF en vistas son el enfoque moderno preferido.
Table Inheritance
El Problema
PostgreSQL soporta herencia de tablas, donde una tabla hija hereda columnas de una tabla padre. Aunque esto suena como un ajuste natural para modelado orientado a objetos, tiene limitaciones significativas:
- Las restricciones únicas y foreign keys no abarcan la jerarquía de herencia.
- Un
SELECTen la tabla padre incluye filas de todos los hijos, lo cual puede ser sorprendente. - Los índices en el padre no cubren las tablas hijas.
- Muchas herramientas ORM no soportan bien la herencia.
Estas limitaciones significan que la herencia de tablas frecuentemente lleva a problemas de integridad de datos que solo se descubren en producción.
Qué Hacer en Su Lugar
Usa diseño relacional estándar con foreign keys:
CREATE TABLE vehicles (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
make text NOT NULL,
model text NOT NULL,
year integer NOT NULL
);
CREATE TABLE trucks (
vehicle_id integer PRIMARY KEY REFERENCES vehicles(id),
payload_capacity numeric NOT NULL,
bed_length numeric NOT NULL
);
Si tu objetivo es particionar tablas grandes para rendimiento, usa el particionamiento declarativo nativo de PostgreSQL, que se introdujo en la versión 10 y mejoró significativamente en versiones posteriores:
CREATE TABLE events (
id integer GENERATED ALWAYS AS IDENTITY,
event_time timestamptz NOT NULL,
payload jsonb
) PARTITION BY RANGE (event_time);
CREATE TABLE events_2024 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE events_2025 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
La Excepción
La herencia de tablas todavía se usa internamente por el sistema de particionamiento, y algunas aplicaciones legacy dependen de ella. Para nuevos diseños, no hay razón convincente para usar herencia directamente.
NOT IN con Subqueries
El Problema
NOT IN es una de las construcciones más peligrosas en SQL cuando se usa con una subquery, debido a cómo maneja valores NULL. Si la subquery retorna incluso un solo NULL, toda la condición NOT IN se evalúa a NULL (que es falsy), y se retornan cero filas.
-- Esto retorna CERO filas si bar.x contiene cualquier valor NULL
SELECT * FROM foo WHERE col NOT IN (SELECT x FROM bar);
Para ilustrar por qué, considera:
SELECT * FROM foo WHERE col NOT IN (1, NULL);
Para cada fila, PostgreSQL evalúa: col != 1 AND col != NULL. Como col != NULL es siempre NULL (desconocido), la expresión completa es NULL, y ninguna fila pasa el filtro. Este comportamiento es correcto según el estándar SQL, pero sorprende a casi todos los que lo encuentran.
Qué Hacer en Su Lugar
Usa NOT EXISTS, que maneja valores NULL correctamente:
SELECT *
FROM foo
WHERE NOT EXISTS (
SELECT 1 FROM bar WHERE foo.col = bar.x
);
NOT EXISTS retorna TRUE cuando la subquery no produce filas, sin importar valores NULL. También es frecuentemente más rápido porque el optimizador puede dejar de evaluar la subquery tan pronto como encuentra una coincidencia.
La Excepción
NOT IN es seguro cuando se usa con una lista hardcodeada de valores non-null (ej. WHERE status NOT IN ('active', 'pending')). Pero si hay alguna posibilidad de que los valores puedan ser NULL, o si la lista viene de una subquery, siempre usa NOT EXISTS.
Nombres de Tablas y Columnas en Mayúsculas
El Problema
PostgreSQL convierte identificadores sin comillas a minúsculas. Si creas una tabla con nombres en mayúsculas o mixed-case, debes citarlos en todas partes:
CREATE TABLE "Users" ("FirstName" text, "LastName" text);
-- Cada query debe usar comillas dobles
SELECT "FirstName" FROM "Users" WHERE "LastName" = 'Smith';
Esto es tedioso, propenso a errores y hace cada query más difícil de leer y escribir. Olvidar un solo conjunto de comillas produce un error “column does not exist” que puede ser desconcertante si no sabes sobre el folding de identificadores.
Qué Hacer en Su Lugar
Usa lowercase_with_underscores para todos los identificadores:
CREATE TABLE users (
first_name text,
last_name text
);
-- Limpio, simple, sin necesidad de comillas
SELECT first_name FROM users WHERE last_name = 'Smith';
Esta convención es idiomática en PostgreSQL, funciona sin comillas y se alinea con cómo la mayoría de herramientas y bibliotecas de PostgreSQL esperan que los nombres estén formateados.
La Excepción
Si estás interfazando con un sistema que requiere casing específico (ej. una aplicación legacy que espera nombres de columna en CamelCase), puedes verte forzado a usar identificadores citados. Minimiza esto tanto como sea posible.
BETWEEN para Timestamps
El Problema
BETWEEN es inclusivo en ambos extremos. Cuando se usa con timestamps, esto crea bugs sutiles de off-by-one:
-- MAL: Esto incluye la medianoche del 8 de junio pero excluye el resto de ese día
SELECT * FROM events
WHERE event_time BETWEEN '2018-06-01' AND '2018-06-08';
El límite superior '2018-06-08' se convierte a '2018-06-08 00:00:00', así que eventos el 8 de junio después de medianoche se excluyen. Si cambias el límite superior a '2018-06-08 23:59:59', todavía pierdes eventos en el último segundo del día. Usar '2018-06-08 23:59:59.999999' es frágil y depende de la precisión del timestamp.
Qué Hacer en Su Lugar
Usa comparaciones de rango explícitas con un intervalo semi-abierto:
-- BIEN: incluye todo el 1 de junio hasta todo el 7 de junio
SELECT * FROM events
WHERE event_time >= '2018-06-01'
AND event_time < '2018-06-08';
El intervalo semi-abierto (>= inicio, < fin) es inequívoco, funciona correctamente sin importar la precisión del timestamp, y es el enfoque estándar en la mayoría de aplicaciones de bases de datos.
La Excepción
BETWEEN está bien para tipos discretos como integer o date (no timestamp), donde no hay valores fraccionarios entre los límites.
Almacenamiento de Fecha y Hora
Usa timestamptz, No timestamp
El Problema: timestamp (también conocido como timestamp without time zone) almacena una fecha y hora sin información de zona horaria. Esto significa que PostgreSQL no tiene idea de si '2024-03-15 14:30:00' está en UTC, Eastern Time o Tokyo Time. Si tus servidores de aplicación están en diferentes zonas horarias, o si la zona horaria de un servidor cambia, tus datos se vuelven silenciosamente incorrectos.
Qué Hacer en Su Lugar: Siempre usa timestamptz (timestamp with time zone):
CREATE TABLE orders (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now(),
shipped_at timestamptz
);
timestamptz almacena el valor en UTC internamente y convierte desde/hacia la zona horaria de la sesión en entrada y salida. Esto asegura comportamiento consistente sin importar la configuración de zona horaria del servidor o cliente.
No Almacenes UTC en Columnas timestamp
El Problema: Un workaround común es “siempre almacenar UTC” en una columna timestamp (without timezone). El problema es que PostgreSQL no sabe que el valor es UTC. Cualquier función consciente de zona horaria (como AT TIME ZONE) tratará el valor según la configuración de zona horaria de la sesión, produciendo resultados incorrectos. También estás dependiendo de que cada desarrollador y cada aplicación almacene consistentemente UTC, sin enforcement de la base de datos.
Qué Hacer en Su Lugar: Usa timestamptz. Almacena UTC internamente y maneja conversiones automáticamente.
Nunca Uses timetz
El Problema: timetz (time with time zone) almacena una hora del día con un offset de zona horaria. Esto casi nunca es útil porque un offset de zona horaria sin una fecha no tiene sentido. Las reglas de horario de verano dependen de la fecha, así que un offset de zona horaria para “3:00 PM” es ambiguo sin saber qué día es. La documentación de PostgreSQL explícitamente establece que timetz existe solo para cumplimiento con el estándar SQL y no debería usarse.
Qué Hacer en Su Lugar: Usa timestamptz si necesitas almacenar un punto en el tiempo. Si genuinamente necesitas solo una hora del día (ej. “la tienda abre a las 9:00 AM”), usa time (without timezone) y maneja conversiones de zona horaria en tu aplicación.
No Uses CURRENT_TIME
El Problema: CURRENT_TIME retorna un valor timetz, que como se discutió arriba casi nunca es lo que quieres.
Qué Hacer en Su Lugar: Usa CURRENT_TIMESTAMP, now() o clock_timestamp():
-- Hora de inicio de transacción (mismo valor durante toda la transacción)
SELECT now();
SELECT CURRENT_TIMESTAMP;
-- Hora del reloj de pared (cambia dentro de una transacción)
SELECT clock_timestamp();
No Uses timestamp(0) o timestamptz(0)
El Problema: El modificador de precisión (0) redondea timestamps al segundo más cercano. Esto suena inofensivo, pero el redondeo puede empujar un timestamp hacia adelante al siguiente segundo, minuto, hora o incluso día. Un valor de '2024-01-01 23:59:59.7' se redondea a '2024-01-02 00:00:00', que es un día completamente diferente.
Qué Hacer en Su Lugar: Almacena timestamps de precisión completa y redondea en tu aplicación o queries cuando sea necesario:
-- Almacena precisión completa
CREATE TABLE logs (
created_at timestamptz NOT NULL DEFAULT now()
);
-- Redondea al mostrar
SELECT date_trunc('second', created_at) FROM logs;
No Uses Offsets +/-HH:mm como Nombres de Zona Horaria
El Problema: Usar offsets fijos como '+05:30' o '-04:00' como identificadores de zona horaria ignora el horario de verano y otros cambios históricos de zona horaria. Representan un offset fijo en un punto en el tiempo, no una zona horaria geográfica con su conjunto completo de reglas.
Qué Hacer en Su Lugar: Usa nombres de zona horaria IANA:
-- MAL
SET timezone = '+05:30';
-- BIEN
SET timezone = 'Asia/Kolkata';
Los nombres de zona horaria IANA (ej. 'America/New_York', 'Europe/London', 'Asia/Tokyo') codifican la historia completa de cambios de offset UTC, incluyendo transiciones de horario de verano.
Almacenamiento de Texto
Nunca Uses char(n)
El Problema: char(n) rellena valores con espacios trailing para llenar la longitud declarada. Esto desperdicia almacenamiento, causa comportamiento de comparación confuso (los espacios trailing pueden o no ser significativos dependiendo de la operación), y no es más rápido que text o varchar. PostgreSQL almacena char(n) y text de la misma manera internamente, así que no hay beneficio de rendimiento.
-- char(10) almacena 'hello ' (rellenado con 5 espacios)
-- Esto desperdicia espacio y crea sorpresas de comparación
SELECT 'hello'::char(10) = 'hello '; -- true
SELECT length('hello'::char(10)); -- 5 (espacios trailing eliminados por length())
Qué Hacer en Su Lugar
Usa text para todas las columnas de string:
CREATE TABLE customers (
name text NOT NULL,
email text NOT NULL
);
No Uses char(n) Incluso para Identificadores de Longitud Fija
El Problema: Los desarrolladores frecuentemente alcanzan char(3) para cosas como códigos de país o códigos de moneda, razonando que la longitud es siempre exactamente 3. Pero char(n) no valida la longitud, rellena valores más cortos con espacios. Si alguien inserta 'US' en una columna char(3), almacena 'US ' sin queja.
Qué Hacer en Su Lugar: Usa text con una restricción CHECK que enforce tanto el formato como la longitud:
CREATE TABLE countries (
code text CHECK (code ~ '^[A-Z]{3}$'),
name text NOT NULL
);
Esto es estrictamente mejor: rechaza valores inválidos (muy corto, muy largo, caracteres incorrectos) mientras char(3) silenciosamente rellenaría o truncaría.
Piensa Antes de Usar varchar(n)
El Problema: varchar(n) es mejor que char(n) porque no rellena, pero el límite de longitud es frecuentemente arbitrario y causa problemas después. Cuando defines varchar(255), no estás ganando ninguna ventaja de rendimiento sobre text. PostgreSQL los maneja idénticamente en términos de almacenamiento y rendimiento. Pero estás creando una restricción que puede necesitar cambiarse después, y alterar la longitud de columna requiere una reescritura de tabla en versiones antiguas de PostgreSQL.
Qué Hacer en Su Lugar: Usa text con restricciones CHECK cuando necesites validación:
CREATE TABLE users (
username text NOT NULL CHECK (length(username) BETWEEN 3 AND 50),
bio text CHECK (length(bio) <= 10000)
);
Las restricciones CHECK pueden agregarse o removerse sin reescribir la tabla, y pueden enforce reglas más complejas que un simple límite de longitud.
La Excepción
Si estás construyendo una base de datos que debe cumplir con un estándar o especificación específica que manda varchar(n) (ej. ciertos estándares de datos financieros o de salud), entonces úsalo. Pero para desarrollo de aplicaciones general, text con restricciones es la mejor opción.
Otros Tipos de Datos
No Uses money
El Problema: El tipo money en PostgreSQL tiene varios problemas serios:
- Su precisión y formateo dependen de la configuración locale
lc_monetary, que puede diferir entre servidores o sesiones. - Transferir datos
moneyentre bases de datos con diferentes configuraciones locale puede cambiar silenciosamente valores. - Tiene precisión limitada (fijo a los lugares decimales de moneda del locale).
- Aritmética con
moneypuede producir resultados sorprendentes.
Qué Hacer en Su Lugar
Usa numeric con precisión explícita para montos monetarios:
CREATE TABLE transactions (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
amount numeric(19,4) NOT NULL,
currency text NOT NULL CHECK (currency ~ '^[A-Z]{3}$'),
created_at timestamptz NOT NULL DEFAULT now()
);
numeric(19,4) te da hasta 15 dígitos antes del punto decimal y 4 dígitos después, lo cual es suficiente para virtualmente cualquier aplicación financiera. Al almacenar el código de moneda separadamente, haces el sistema independiente del locale.
La Excepción
Si estás construyendo un prototipo rápido o herramienta interna donde nunca transferirás datos entre sistemas y el locale nunca cambiará, money podría ahorrarte algunas teclas. Pero para cualquier cosa que importe, usa numeric.
Usa Identity Columns en Lugar de serial
El Problema: serial y bigserial son pseudo-tipos específicos de PostgreSQL que crean una secuencia y establecen un valor default, pero tienen varias desventajas:
- La secuencia no está verdaderamente “owned” por la columna de una manera que prevenga que otras tablas la usen.
- Los permisos en la secuencia deben gestionarse separadamente.
serialno previene inserts manuales de valores arbitrarios, lo que puede causar conflictos de secuencia.serialno cumple con el estándar SQL.
Qué Hacer en Su Lugar
Usa identity columns, que se introdujeron en PostgreSQL 10:
CREATE TABLE users (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username text NOT NULL,
email text NOT NULL
);
GENERATED ALWAYS AS IDENTITY ata la secuencia fuertemente a la columna, previene inserción manual de valores por default (debes usar explícitamente OVERRIDING SYSTEM VALUE para evitarlo), y sigue el estándar SQL. Usa GENERATED BY DEFAULT AS IDENTITY si necesitas permitir inserts manuales ocasionalmente.
La Excepción
Si estás manteniendo una base de código legacy que ya usa serial extensivamente, no hay necesidad urgente de migrar tablas existentes. Pero para todas las tablas nuevas, usa identity columns.
Autenticación
Nunca Uses Autenticación trust Sobre TCP/IP
El Problema: El método de autenticación trust en pg_hba.conf permite a cualquiera conectarse sin contraseña. Cuando se configura para conexiones TCP/IP (entradas host), esto significa que cualquiera que pueda alcanzar el puerto PostgreSQL en la red puede conectarse como cualquier usuario, incluyendo el superusuario, y hacer lo que quiera: leer todos los datos, modificarlos, borrar bases de datos o incluso ejecutar comandos del sistema operativo.
# PELIGROSO: Cualquiera en la red puede conectarse como cualquier usuario
host all all 0.0.0.0/0 trust
Esto es el equivalente en base de datos de dejar tu puerta principal abierta con un cartel que dice “toma lo que quieras.”
Qué Hacer en Su Lugar
Usa autenticación scram-sha-256 para todas las conexiones TCP/IP:
# pg_hba.conf
host all all 0.0.0.0/0 scram-sha-256
Establece el método de encriptación de contraseña en postgresql.conf:
password_encryption = scram-sha-256
Luego establece contraseñas fuertes para todos los usuarios de base de datos:
ALTER USER myuser WITH PASSWORD 'a-strong-randomly-generated-password';
scram-sha-256 es el método de autenticación basada en contraseña más seguro disponible en PostgreSQL. Previene sniffing de contraseñas y ataques de replay.
La Excepción
La autenticación trust es aceptable para conexiones locales (Unix socket) en una máquina de desarrollo donde eres el único usuario. Nunca debería usarse para conexiones TCP/IP en ningún ambiente, incluyendo desarrollo, porque otros procesos o usuarios en la red podrían explotarlo.
Resumen
PostgreSQL te da una cantidad enorme de poder, pero también te da suficiente cuerda para crear problemas serios si no tienes cuidado. Los anti-patrones cubiertos en esta guía no son hipotéticos: aparecen regularmente en sistemas de producción y causan problemas reales de integridad de datos, problemas de rendimiento y vulnerabilidades de seguridad.
Aquí hay un checklist rápido que puedes usar al revisar tu configuración de PostgreSQL:
- Codificación: UTF-8, no SQL_ASCII.
- Tipos de tiempo:
timestamptz, notimestamp. Nuncatimetz. - Tipos de texto:
textcon restriccionesCHECK, nochar(n)ovarchar(n)arbitrario. - Dinero numérico:
numeric(19,4), nomoney. - Primary keys: Identity columns, no
serial. - Queries:
NOT EXISTSen lugar deNOT INcon subqueries. Rangos explícitos en lugar deBETWEENpara timestamps. - Schema: Triggers en lugar de Rules. Foreign keys en lugar de Table Inheritance.
- Autenticación:
scram-sha-256sobre TCP/IP, nuncatrust.
Arreglar estos problemas es usualmente directo, pero encontrarlos requiere saber qué buscar. Marca esta guía y refiere a ella al diseñar nuevas tablas, escribir queries o revisar pull requests. Tu yo futuro y tu equipo te lo agradecerán.