- La documentación oficial de Postgres es excelente, pero el PDF de Postgres 17 tiene 3,200 páginas, así que a una persona principiante le cuesta aprender solo con la documentación todo sobre diseño de esquemas, comportamiento de SQL y trampas operativas antes de usarlo en trabajo real
- Salvo que haya una razón especial, conviene normalizar los datos, y la desnormalización para mejorar el rendimiento de lectura implica asumir costos como inconsistencias y mayor complejidad de escritura
- Las palabras clave de SQL no distinguen mayúsculas de minúsculas, pero NULL se parece más a “desconocido”, así que compararlo como el
nullde un lenguaje general puede dar resultados inesperados - Con solo usar bien el pager,
\x,.psqlrc,\pset null, el autocompletado, los comandos con barra invertida y\copyenpsql, mejora mucho la legibilidad de la salida, la exploración y la exportación a CSV - Los índices, locks, transacciones y JSONB son potentes, pero si no se entienden los planes de consulta y las restricciones operativas, pueden causar degradación de rendimiento o problemas de disponibilidad
Contexto que conviene conocer antes de la enorme documentación oficial
- La documentación oficial de Postgres, según la versión actual 17, tiene 3,200 páginas si se imprime en PDF tamaño carta US letter, y 3,024 páginas en A4
- Hay mucho conocimiento práctico útil que conviene saber antes de usar Postgres, y aunque parte también aplica a otros DBMS SQL, su alcance no siempre es totalmente claro
Normaliza los datos por defecto
- La normalización es el proceso de eliminar datos duplicados o innecesarios en el esquema de una base de datos
- Si guardas
user_emaildirectamente en la tabladocuments, cuando un usuario cambie su correo habrá que actualizar todas las filas de documentos de ese usuario- En cambio, puedes hacer que cada fila de
documentsapunte a una fila de otra tabla comousersmediante una clave foráneauser_id
- En cambio, puedes hacer que cada fila de
- No hace falta memorizar todas las formas normales como la “1st normal form”, pero el proceso general de normalización suele llevar a esquemas más fáciles de mantener
- La desnormalización consiste en guardar datos duplicados para leer más rápido sin recalcular cierta información cada vez
- En una app de turnos de empleados, en lugar de calcular cada vez las horas acumuladas del año sumando toda la duración de los turnos, puedes calcularlas y guardarlas periódicamente o cuando cambie el horario trabajado
- Esos datos pueden estar dentro de Postgres o en una capa de caché como Redis
- La desnormalización casi siempre tiene un costo, y los más comunes son la posibilidad de inconsistencias de datos y el aumento en la complejidad de escritura
Consejos de “no hagas esto” del proyecto Postgres
- La wiki oficial de Postgres tiene una lista de “Don’t do this”
- No pasa nada si no entiendes todos los puntos, y si no entiendes alguno, probablemente también sea menos probable que cometas ese error
- Vale la pena recordar especialmente estos consejos
- Usar el tipo
textpara almacenar texto - Usar
timestampz/time with time zonepara almacenar marcas de tiempo - Nombrar las tablas en snake_case
- Usar el tipo
Comportamientos de SQL que suelen confundir
-
Las palabras clave de SQL no tienen que ir en mayúsculas
- Las palabras clave de SQL no distinguen mayúsculas de minúsculas
- Las siguientes consultas significan lo mismo
SELECT * FROM my_table WHERE x = 1 AND y > 2 LIMIT 10; select * from my_table where x = 1 and y > 2 limit 10; SELECT * from my_table WHERE x = 1 and y > 2 LIMIT 10;- Esta característica no es exclusiva de Postgres
-
NULL es distinto del null/nil de los lenguajes generales
- En SQL,
NULLse parece más a “desconocido” que alnullonilde un lenguaje de programación general NULL = NULLno devuelvetrue, sinoNULL- En una comparación donde uno de los lados es
NULL, la mayoría de las veces el resultado también esNULL - Para comparar con
NULL, hay que usar estas operacionesx IS NULL:truesixesNULLx IS NOT NULL:truesixno esNULLx IS NOT DISTINCT FROM y: parecido ax = y, pero trataNULLcomo un valor normalx IS DISTINCT FROM y: parecido ax != y/x <> y, pero trataNULLcomo un valor normal
- La cláusula
WHEREsolo devuelve filas cuando la condición estrueSELECT * FROM users WHERE title != 'manager'no devuelve las filas dondetitleesNULL- Eso se debe a que el resultado de
NULL != 'manager'esNULL
COALESCEdevuelve el primer valor que no seaNULLentre varios argumentos
COALESCE(NULL, 5, 10) = 5 COALESCE(2, NULL, 9) = 2 COALESCE(NULL, NULL) IS NULL - En SQL,
Cómo sacarle más provecho a psql
-
Mejorar la legibilidad de la salida
- Si al consultar una tabla con muchas columnas o valores largos la salida es difícil de leer, puede que el pager esté desactivado
- El pager del terminal permite desplazarte por textos largos o tablas de
psqldentro del viewport - Para tablas con muchas columnas, puedes activar el expanded mode con
\pset expandedo\x - Si quieres usarlo por defecto, agrega
\xa~/.psqlrcen tu directorio home
-
Hacer más clara la salida de NULL
- La configuración predeterminada no muestra claramente cuándo un valor es
NULL - En
psqlpuedes definir la cadena que se mostrará paraNULL
\pset null '[NULL]'- También puedes usar cadenas Unicode, y si quieres dejarlo como valor predeterminado, agrega el mismo comando a
~/.psqlrc
- La configuración predeterminada no muestra claramente cuándo un valor es
-
Aprovechar el autocompletado y los comandos con barra invertida
psqlsoporta autocompletado como una consola interactiva- Si escribes parte de una palabra clave o del nombre de una tabla y presionas Tab, puede completar el resto
- Algunos comandos útiles con barra invertida son
\?: lista de todos los shortcuts\d: muestra relaciones, es decir, tablas y secuencias, junto con sus propietarios\d+: agrega tamaño y algunos metadatos a\d\d table_name: muestra el esquema de la tabla, tipos de columnas, si aceptan null, valores por defecto, índices y restricciones de clave foránea\e: edita la consulta en el editor predeterminado definido en la variable de entorno$EDITOR\h SQL_KEYWORD: muestra la sintaxis de esa palabra clave SQL y un enlace a la documentación
-
Exportar a CSV y usar alias en SELECT
- Puedes guardar el resultado de una consulta en CSV con
\copy
\copy (select * from some_table) to 'my_file.csv' CSV- Para incluir los nombres de las columnas en la primera línea, agrega la opción
HEADER
\copy (select * from some_table) to 'my_file.csv' CSV HEADER\copypuede evitar los privilegios elevados que requiere la sentencia más estándarCOPY- A las columnas de salida de
SELECTse les puede poner alias conAS
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY vendor ORDER BY number_of_backpacks DESC;- En
GROUP BYyORDER BYpuedes referenciar los números de columna según el orden en que aparecen después deSELECT
SELECT vendor, COUNT(*) AS number_of_backpacks FROM backpacks GROUP BY 1 ORDER BY 2 DESC;- Esta forma abreviada es útil, pero conviene no usarla en consultas que se despliegan a producción
- Puedes guardar el resultado de una consulta en CSV con
Los índices no siempre se usan solo por agregarlos
-
Índices y planes de consulta
- Un índice es una estructura de datos que funciona como un directorio de atajos para encontrar filas de una tabla según ciertos campos
- El índice más común es el B-tree, y funciona para condiciones de igualdad exacta como
WHERE a = 3y de rango comoWHERE a > 5 - No se puede indicarle directamente a Postgres que use un índice específico
- Postgres predice si un índice será más rápido que leer toda la tabla con un sequential scan basándose en estadísticas que mantiene para cada tabla
- Si antepones
EXPLAINaSELECT ... FROM ..., puedes ver el plan de consulta de cómo Postgres ejecutará la consulta - Para leer planes de consulta, puedes consultar la guía de EXPLAIN ANALYZE de thoughtbot, la documentación de pganalyze, la documentación oficial y explain.depesz.com
-
Tablas pequeñas e índices multicolumna
- En tablas con pocas filas, como una base local de desarrollo, un índice puede no ayudar mucho
- Si hay unas 100 filas, Postgres puede decidir que un sequential scan es más rápido que usar un índice
- Postgres soporta índices multicolumna
CREATE INDEX CONCURRENTLY ON tbl (a, b);- Una condición como
WHERE a = 1 AND b = 2puede ser más rápida que tener índices separados paraayb - Eso se debe a que puede recorrer un solo B-tree y combinar eficientemente las condiciones de búsqueda
- Un índice
(a, b)también hace rápidas las consultas que filtran solo pora, casi igual que un índice exclusivo sobrea - Consultas como
WHERE b = 5pueden acelerarse, pero quizá no de la mejor manera- Como el índice está ordenado primero por
ay luego porb, tiene que pasar por todos los valores deapara encontrarb
- Como el índice está ordenado primero por
- Si necesitas consultar varias combinaciones de columnas, es común tener tanto un índice
(a, b)como otro solo sobreb - Según el caso, también puedes depender de índices individuales sobre
ayb
-
Para prefix match, usa
text_pattern_ops- Puede que guardes directorios jerárquicos con el enfoque de materialized path y necesites encontrar todos los descendientes que empiecen con un prefijo dado
SELECT * FROM directories WHERE path LIKE '/1/2/3/%'- Aunque crees un índice B-tree normal sobre la columna
path, puede que esa consulta no lo use
CREATE INDEX CONCURRENTLY ON directories (path);- Para permitir el ordenamiento por caracteres necesario en prefix match o pattern match, hay que especificar una operator class
CREATE INDEX CONCURRENTLY ON directories (path text_pattern_ops);
Problemas operativos causados por locks y transacciones
-
Los locks en Postgres
- Un lock o mutex es un mecanismo que permite que solo un cliente haga una operación riesgosa a la vez
- En una base de datos, las actualizaciones de objetos como row, table o view deben completarse totalmente o fallar totalmente, y para evitar que operaciones concurrentes dejen estados parciales, se adquieren locks sobre los objetos relacionados
- Los niveles de lock de tabla en Postgres van de menos restrictivos a más restrictivos
ACCESS SHARE:SELECTROW SHARE:SELECT ... FOR UPDATEROW EXCLUSIVE:UPDATE,DELETE,INSERTSHARE UPDATE EXCLUSIVE:CREATE INDEX CONCURRENTLYSHARE:CREATE INDEX, sinCONCURRENTLYACCESS EXCLUSIVE: muchas formas deALTER TABLE,ALTER INDEX
- En una misma tabla, las siguientes operaciones pueden ejecutarse o tienen que esperar
UPDATEduranteSELECT: sí se puedeUPDATEduranteCREATE INDEX CONCURRENTLY: sí se puedeSELECTduranteCREATE INDEX: sí se puedeSELECTduranteALTER TABLE: por lo general esperaALTER TABLEduranteSELECT: por lo general espera
- Algunas formas de
ALTER TABLEpueden requerir locks más débiles; para los detalles completos se puede consultar la documentación oficial sobre locks explícitos y esta guía de conflictos de locks por operación
-
ALTER TABLElento y colas de locks- Si
ALTER TABLEtarda mucho, también puede bloquearSELECTsobre esa misma tabla - Si se trata de una tabla crítica como
users, que todas las solicitudes de una app web consultan, las peticiones pueden quedar esperando, hacer timeout y devolver 503 - Causas comunes de un
ALTER TABLElento- agregar una columna con un default no constante
- cambiar el tipo de una columna
- agregar una restricción de unicidad
- Desde Postgres 11, ya se corrigió el problema por el que cualquier default al agregar una columna volvía lenta la operación; el caso problemático puede ser un default no constante
- Aunque
ALTER TABLEen sí sea rápido, no se ejecuta hasta conseguir el lock- Si hay un
SELECTlento ejecutándose en un dashboard interno antiguo,ALTER TABLEtendrá que esperar
- Si hay un
- Como los locks de Postgres forman una cola, las consultas posteriores sobre la misma tabla que lleguen detrás de un
ALTER TABLEen espera también pueden quedar bloqueadas - Ese mismo escenario se explora con más detalle en Migrations and exclusive locks
- Si
-
Las transacciones largas también son peligrosas
- Una transacción agrupa varias sentencias de base de datos bajo la lógica de todo o nada; comienza con
BEGINy termina conCOMMIT - Los cambios hechos dentro de una transacción no son visibles para otros clientes y solo se publican en la base de datos al hacer
COMMIT - Es ideal para operaciones como una transferencia bancaria, donde la disminución del saldo en una cuenta y el aumento en otra deben tener éxito juntos o revertirse juntos
- Si una transacción obtiene locks, los conserva hasta
COMMIT - Si después de
BEGINactualizas una row específica conUPDATEy te alejas de la terminal, otro cliente que intente hacerDELETEsobre esa row se quedará detenido hasta que la transacción haga commit - Mantener transacciones abiertas más tiempo del necesario puede bloquear consultas o actualizaciones de otros clientes
- Una transacción agrupa varias sentencias de base de datos bajo la lógica de todo o nada; comienza con
JSONB es una herramienta filosa
-
Problemas de rendimiento y esquema en JSONB
- JSONB es flexible, pero mal usado tiene desventajas importantes
- Postgres no rastrea estadísticas de columnas JSONB, así que una consulta de igualdad sobre una sola columna JSONB puede ser mucho más lenta que una consulta equivalente sobre columnas normales
- Un caso muestra un ejemplo donde JSONB es 2000 veces más lento
- En una columna JSONB puede entrar prácticamente cualquier cosa, lo cual es poderoso, pero ofrece pocas garantías sobre la estructura
- En una tabla normal puedes mirar el esquema y prever el resultado de una consulta, pero con JSONB no siempre sabes si los nombres de key están en camelCase o snake_case, o si un estado es boolean o enum
- Las propiedades de tipado estático que sí tienen los datos normales de Postgres no se aplican de la misma forma a JSONB
-
Lo incómodo de comparar tipos en JSONB
- Si quieres encontrar filas donde el campo
branddentro de la columna JSONBdatade la tablabackpacksseaJanSport, la siguiente consulta no funciona
select * from backpacks where data['brand'] = 'JanSport';- Postgres espera que el tipo del lado derecho de la comparación coincida con el del lado izquierdo, y el lado derecho debe ser un documento JSON válido
- Un documento JSON debe ser un objeto, arreglo, string, número, boolean o null, así que
JanSportpor sí solo no es JSON válido - La consulta correcta consiste en comparar con un string JSON o convertir el lado izquierdo a
textde Postgres
select * from backpacks where data['brand'] = '"JanSport"'; select * from backpacks where data['brand'] = '"JanSport"'::jsonb; select * from backpacks where data->>'brand' = 'JanSport';- El
NULLde SQL y elnullde JSONB se comportan distinto'null'::jsonb = 'null'::jsonbdevuelvetrue, peroNULL = NULLdevuelveNULL
- JSONB tiene muchos operadores y funciones dedicados, así que no es fácil memorizarlos todos de una sola vez
- En Postgres existen tanto
JSON, que guarda el valor JSON como texto, comoJSONB, que lo convierte a un formato binario eficiente - JSONB tiene ventajas como la posibilidad de indexarlo, y el formato JSON puede verse como un caso más especial
- Si quieres encontrar filas donde el campo
2 comentarios
Definitivamente tendré que leer alguna vez lo que no se debe hacer.
Opiniones de Hacker News
PostgreSQL en general distingue entre mayúsculas y minúsculas, pero escribir las palabras clave de SQL en mayúsculas suele ser un intento de mejorar la legibilidad mediante reconocimiento visual de patrones.
No es estrictamente necesario, pero si tuviera que depurar consultas de otra persona, creo que las pasaría por un prettifier para revisar rápidamente la definición sin trabarme con detalles menores de la forma de la sintaxis.
Igual que al formatear código en otros lenguajes, una estructura visual como la indentación consistente reduce el tiempo dedicado a entender las partes obvias y permite enfocarse en lo importante.
Eso sí, realmente detesto que se mezclen mayúsculas y minúsculas en identificadores como
actuallyUsingCaseInIdentifiers, y no quiero ver columnas que requieren comillas dobles para revisarlas desde la CLI.Si voy a escribir rápido una consulta temporal que nadie verá y luego descartarla, no me preocupo por las mayúsculas/minúsculas, pero el SQL que se commitea al repositorio lo escribo con los comandos en ALL CAPS.
Ahora que hay color ya no hacen falta, pero es un recuerdo viejo y no tengo material que lo respalde.
Aun así, no se deben mezclar identificadores entrecomillados y no entrecomillados, y las consultas a estructuras internas tampoco están demasiado estandarizadas, así que no tiene mucha importancia.
No conocía la sección “don’t do this” de la wiki de PostgreSQL, y es bastante útil: https://wiki.postgresql.org/wiki/Don%27t_Do_This
Por ejemplo, en esquemas nuevos parecería correcto desactivar funciones como la herencia de tablas y exigir una configuración deliberadamente complicada para volver a habilitarlas.
Mucho de lo que aparece aquí no aplica solo a PostgreSQL.
Pasa con cosas como el comportamiento extraño de
NULLy el orden de las columnas en los índices; en particular, la interacción entre NULL e índices/restricciones unique tampoco es intuitiva en MySQL.Por ejemplo, si en una tabla de usuarios
emailno permite NULL yusernamesí permite NULL, y se define una restricción unique sobre(email, username), se puede insertar varias veces el mismoemailconusernameen NULL. Esto ocurre porque NULL no es igual a otro NULL.https://www.postgresql.org/docs/devel/sql-createtable.html#S...
Los casos de uso que necesitan el comportamiento opuesto son mucho más raros.
No alcanza con decir “normaliza los datos si no hay una buena razón para no hacerlo” y dejarlo ahí.
Incluso la página enlazada por el autor enumera 11 formas normales, incluida la forma no normalizada, y la mayoría ni siquiera sabe qué son; de esas, 7 prácticamente nunca se usan.
No hay que hacer que la gente salga a perseguir formas normales más altas.
En un proyecto al que me pasé hace poco también tuve que corregir varios problemas de este tipo, y casi nunca hay motivos para duplicar datos.
El primer consejo es ejecutar VACUUM todos los días.
Cuando empezamos no sabía esto, así que nunca hicimos VACUUM en la base de datos de reddit, y un día no nos quedó otra que ejecutarlo; reddit estuvo caído casi todo un día mientras esperábamos a que terminara.
Para la escala de reddit, sorprende que no se hayan agotado primero los ID de transacción.
Me gustaría que los desarrolladores prestaran más atención a la normalización y dejaran de meter todo en columnas JSONB.
Los desarrolladores con más experiencia sabían que la respuesta correcta era no duplicar nada salvo las claves, y desnormalizar solo muy a regañadientes.
Luego aparecieron bases de datos como Mongo, que ofrecían “algo parecido a una base de datos” donde la normalización era difícil o no tenía sentido, alentando a esos juniors; como resultado, durante un tiempo florecieron diseños de bases de datos horribles y torres de basura imposibles de mantener.
Ahora el péndulo volvió y redescubrimos las ventajas de las bases de datos normalizadas, pero las columnas JSON siguen siendo una vía de escape donde pueden crecer malas prácticas.
La primera es almacenar JSON. Cuando un servidor web llama a una API de terceros, guardar la respuesta original de la API en una columna JSONB y procesarla desde ahí deja un registro auditable para depurar problemas provenientes de esa API.
La segunda es almacenar tipos suma (sum types). Que SQL no soporte tipos suma puede considerarse una de las mayores deficiencias al modelar datos en bases de datos SQL.
Hay varias soluciones alternativas, y “simplemente meterlo en una columna JSONB y validarlo en la aplicación” es una de ellas, pero ninguna es especialmente buena.
Mientras no se escriban malas consultas dentro de JSONB en lugar de promover esos valores a columnas separadas, no lo veo como un gran problema en sí mismo.
Esto se debe a que, si logran satisfacer con éxito los requisitos de persistencia, no hay una presión fuerte para pensar en un buen diseño.
Es discutible si tiene sentido construir otro DBMS encima de un DBMS, pero en cualquier caso así están las cosas hoy.
Si una columna nueva arruina el rendimiento o causa problemas, debería poder revertirse.
Si hay herramientas CLI involucradas, también hay que manejar cuánto downtime se puede tolerar, si es posible una actualización de versión sincronizada en toda la empresa o si se van a soportar tanto el esquema antiguo como el nuevo durante un tiempo.
Si la base de datos no forma parte del producto principal del equipo, puede que todo esto falte.
Escribí este texto para ayudar a principiantes: https://tomcam.github.io/postgres/
El artículo es realmente bueno, y no sabía que la documentación de PostgreSQL tuviera 3200 páginas.
Lo he estado usando desde hace un tiempo y voy aprendiendo cuando lo necesito; también me gusta bastante la documentación oficial, y me gusta leer artículos relacionados cuando necesito un tema específico.
Creo que al lector le ayudaría que el autor agregara en https://challahscript.com/what_i_wish_someone_told_me_about_... que un índice de columnas
(b, a)funciona bien al consultar solo porb.Está algo implícito cuando se habla de consultar solo por
a, pero no estaría mal hacerlo más explícito.La parte de JSON/JSONB no la miré mucho porque casi no lo uso.
Pensando en las consultas SQL ridículas que he visto en entornos reales, creo que sería bueno empezar por leer el paper de Codd y entender qué es el modelo relacional.
Tiene apenas 11 páginas, y solo con leerlo se reduciría el sufrimiento en este mundo.
Casi todo este artículo también aplica a otras bases de datos MVCC como MySQL.
Los detalles pueden variar, pero MySQL también sufre con transacciones largas, toma bloqueos de metadatos durante
ALTERy tiene problemas divertidos similares.