Almacenamiento interno de PostgreSQL | Nicolás Garzón
El almacenamiento interno explica por qué una query lee páginas, por qué un UPDATE crea versiones, por qué una columna grande usa TOAST y por qué cache, checkpoint y vacuum afectan rendimiento de formas distintas.
PostgreSQL organiza tablas e índices como relations respaldadas por archivos. Esos archivos se dividen en blocks o pages, normalmente de 8 KiB en builds estándar.
Texto
Copiar relation
→ segmentos de archivo
→ páginas
→ line pointers
→ tuplesLas abstracciones SQL siguen siendo la interfaz correcta. Comprender el nivel físico ayuda a diagnosticar I/O, bloat, HOT, index-only scans y WAL; no significa modificar archivos manualmente.
El data directory contiene:
Catálogos y archivos por database.
pg_wal.
Configuración y metadata de control.
Tablespaces links.
Estado de transacciones y multixacts.
No copies archivos en caliente como backup ordinario. La coherencia requiere base backup/protocolo compatible con WAL.
Una tabla lógica posee un identificador de catálogo, pero su archivo físico puede cambiar tras:
VACUUM FULL.
CLUSTER.
Algunas formas de ALTER TABLE.
REINDEX para índices.
No dependas de nombres físicos como contrato.
Relations grandes se dividen en segmentos de tamaño limitado para facilitar manejo por sistema de archivos.
Main fork: datos.
FSM: free space map.
VM: visibility map.
Init fork para unlogged relations.
Texto
Copiar page header
line pointer array →
espacio libre
← tuples desde el finalEl line pointer identifica la tuple dentro de la page. PostgreSQL puede mover bytes de una tuple dentro de la page conservando el pointer.
Contiene metadata de MVCC, flags y null bitmap. Consecuencias:
Una fila tiene overhead aunque tenga pocas columnas.
Muchas columnas nullable añaden bitmap.
Alignment puede insertar padding.
El orden de columnas puede influir en tamaño por alineación, aunque no debe optimizarse sin medir y considerar legibilidad/migraciones.
ctid identifica block y offset de una versión concreta:
SQL
Copiar SELECT ctid, * FROM products; Cambia con UPDATE y reescrituras. Puede ser útil para diagnóstico o batches muy controlados, pero no es primary key durable.
Insertar IDs ascendentes no garantiza que un SELECT sin ORDER BY salga ordenado.
Vacuum, concurrent writes, page reuse y planes cambian secuencia. Solo ORDER BY define contrato.
Una tuple debe encajar conceptualmente dentro de límites de página, pero TOAST permite externalizar/comprimir atributos variables grandes.
No significa que una fila JSONB de 20 MB sea barata; accederla puede requerir muchas chunks y memoria.
The Oversized-Attribute Storage Technique maneja valores grandes de tipos varlena como text, bytea o JSONB.
Estrategias según storage setting:
PLAIN: no compresión/externalización; tipos fijos normalmente.
MAIN: intenta compresión, prefiere mantener inline.
EXTERNAL: permite externalizar sin comprimir, útil para substring en ciertos datos.
EXTENDED: comprime y/o externaliza; default común.
SQL
Copiar SELECT attname, attstorage
FROM pg_attribute
WHERE attrelid = 'app.events' ::regclass
AND attnum > 0 ; Al seleccionar o procesar un valor externalizado, PostgreSQL puede reconstruirlo.
SQL
Copiar SELECT id FROM events; no necesita necesariamente payload.
SQL
Copiar SELECT payload FROM events; puede leer TOAST y descomprimir.
Evita SELECT * en tablas con blobs/documentos grandes.
Actualizar una columna pequeña puede reutilizar referencias a valores TOAST sin reescribir todo en algunos casos, pero cambios del documento pueden generar nuevas chunks y bloat.
JSONB gigante actualizado con jsonb_set sigue creando una nueva versión del valor; no es update in-place granular como storage documental especializado.
FSM registra páginas con espacio disponible para insertar nuevas tuples.
No es un mapa exacto por byte; permite encontrar candidatos sin escanear toda la tabla.
Vacuum actualiza FSM. Si información queda desactualizada, inserts pueden extender la relación aunque exista espacio hasta que mantenimiento lo registre.
VM tiene bits all-visible y all-frozen por página.
Todas las tuples visibles para todos los snapshots relevantes.
Index-only scan puede evitar heap.
Tuples no necesitan futuras comprobaciones de freeze ordinarias.
Una modificación limpia el bit correspondiente; vacuum lo restaura cuando procede.
Cache interna de páginas de PostgreSQL.
Texto
Copiar query solicita block
→ buscar en shared buffers
→ si no está, leer mediante OS/filesystem
→ colocar buffershared hit significa encontrado en shared buffers. shared read significa que PostgreSQL tuvo que solicitar lectura; el sistema operativo puede servir desde page cache.
PostgreSQL usa buffered I/O en muchos sistemas, por lo que existe doble cache conceptual:
Shared buffers conoce páginas y locks internos.
OS cache conserva bloques de archivo.
No configures shared_buffers igual a toda la RAM. Deja memoria para OS, conexiones, work_mem y procesos.
PostgreSQL utiliza un algoritmo clock-sweep aproximado, no LRU puro. Buffers reciben usage counts y se reemplazan según acceso.
Una sequential scan grande usa estrategias para no expulsar todo el working set de forma ingenua.
Una modificación cambia la página en memoria y genera WAL. La page queda dirty y puede escribirse después por:
Backend.
Background writer.
Checkpointer.
WAL correspondiente debe estar seguro antes de la data page según write-ahead rule.
Un checkpoint asegura que todas las páginas dirty anteriores a cierto punto queden escritas y registra un punto de recuperación.
Limitar tiempo/volumen de crash recovery.
Gestionar dirty buffers.
Costes si son frecuentes:
Picos de escritura.
Más full-page images después de cada checkpoint.
Más WAL.
Si son demasiado espaciados:
Más WAL retenido.
Recovery potencialmente mayor.
Más buffers dirty.
Un checkpoint se activa por tiempo o presión de WAL, entre otras causas.
Si max_wal_size es bajo para el workload, checkpoints requested frecuentes indican configuración o picos.
Monitorea estadísticas de checkpoints y write timing antes de cambiar.
Distribuye escritura del checkpoint a lo largo del intervalo. Un valor alto suaviza I/O, pero necesita margen antes del siguiente checkpoint.
No elimina el volumen total; cambia su forma temporal.
Escribe algunos buffers dirty para que backends encuentren buffers limpios y reduzcan escrituras propias.
No es responsable de garantizar checkpoint ni toda durabilidad.
Escribe buffers WAL periódicamente. Commit puede necesitar flush según synchronous_commit.
Data writer y WAL writer cumplen funciones distintas.
Rendimiento mejora cuando páginas necesarias están próximas y cacheables.
B-tree sobre IDs ascendentes: buena localidad de índice.
UUID aleatorio: inserts distribuidos y splits.
Correlation entre columna y heap.
CLUSTER/rewrite.
Churn y page reuse.
SQL
Copiar CLUSTER app. orders USING orders_created_idx; Reescribe tabla en el orden del índice y toma lock fuerte.
Mejor locality para rangos.
Orden se degrada con nuevas escrituras.
Necesita espacio/tiempo.
No establece contrato de salida.
Requiere repetir para mantener.
Alternativas: pg_repack, particionamiento o diseño de inserción, según necesidad.
SQL
Copiar ALTER TABLE products SET ( fillfactor = 80 ) ; SQL
Copiar CREATE INDEX . . . WITH ( fillfactor = 90 ) ; Deja espacio para updates/inserts y reduce splits/HOT failures, a cambio de mayor tamaño inicial.
El valor óptimo depende de patrón de escritura.
B-tree contiene root, internal y leaf pages. Leaf entries apuntan a TIDs del heap.
Page splits ocurren cuando no hay espacio. Inserts aleatorios distribuyen splits; deduplication puede compactar duplicados en versiones compatibles.
GIN/GiST/BRIN tienen layouts y mantenimiento diferentes.
Son estructuras reconstruibles; recovery y vacuum pueden regenerarlas según diseño. No contienen la fuente de verdad del usuario.
Permiten ubicar objetos en paths/storage diferentes:
SQL
Copiar CREATE TABLESPACE fastspace LOCATION '/mnt/fast' ;
Operación y backups más complejos.
Path debe existir en todos los nodos físicos compatibles.
No es particionamiento lógico.
Un storage perdido puede inutilizar cluster.
Managed services suelen restringirlos.
Sorts, hashes y materializaciones que exceden memoria escriben archivos temporales.
temp_files/temp_bytes en stats.
Logs con log_temp_files.
EXPLAIN temp read/write.
Un disco temporal saturado afecta todo el servidor.
SQL
Copiar CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY ,
occurred_at timestamptz NOT NULL ,
event_type text NOT NULL ,
payload jsonb NOT NULL
) ;
BRIN sobre occurred_at si tabla enorme y append-correlated.
Evitar devolver payload en listados.
GIN solo para rutas JSON consultadas.
Retención por partitions si se eliminan meses enteros.
TOAST crecerá con payload; medir tamaño y updates.
SQL
Copiar SELECT
pg_size_pretty( pg_relation_size( 'app.events' ) ) AS heap,
pg_size_pretty( pg_indexes_size( 'app.events' ) ) AS indexes,
pg_size_pretty( pg_total_relation_size( 'app.events' ) ) AS total; Total incluye índices y TOAST. Una tabla “pequeña” en heap puede tener TOAST enorme.
Extensiones como pageinspect permiten mirar páginas, pero requieren privilegios y conocimiento interno.
Úsalas para diagnóstico especializado, no en lógica de aplicación.
TOAST reduce fila principal, pero index scan aún puede ser eficiente si no seleccionas la columna.
Genera nuevas versiones y WAL; puede ser mejor normalizar campos cambiantes.
Picos de WAL o max_wal_size bajo generan requested checkpoints.
Shared hits altos no significan CPU barata; procesar millones de tuples sigue cuesta.
Commit/checkpoint pueden degradarse aunque reads estén cacheados.
Archivo no reduce automáticamente; vacuum reutiliza.
OS cache puede responder.
Deja sin memoria otros componentes.
Tiene I/O, CPU y límites.
Subestima storage y backup.
Separa heap, indexes y TOAST.
Observa buffers en plan.
Revisa temp I/O.
Mide write/checkpoint/WAL.
Busca churn y HOT ratio.
Revisa locality/correlation.
Identifica columnas grandes seleccionadas.
Evalúa storage latency.
Cambia una hipótesis.
Mide bajo carga.
Relations se dividen en pages.
Heap no está ordenado por key.
Tuples incluyen overhead MVCC.
TOAST externaliza/comprime valores grandes.
FSM encuentra espacio; VM informa visibilidad/freeze.
Shared buffers y OS cache cooperan.
Checkpoints escriben pages y afectan WAL/I/O.
El diseño físico explica costes, no sustituye SQL correcto.
¿Por qué SELECT id puede evitar leer TOAST payload?
¿Qué diferencia existe entre FSM y VM?
¿Por qué shared read no implica disco físico?
¿Qué coste tiene checkpoint frecuente?
¿Por qué CLUSTER no garantiza orden permanente?
Ver respuestas
El atributo externalizado solo se recupera cuando se necesita.
FSM registra espacio; VM registra páginas all-visible/all-frozen.
La lectura puede resolverse desde page cache del OS.
Más I/O y full-page images/WAL.
Nuevos inserts/updates reutilizan páginas y degradan el orden físico.
Write-Ahead Logging y durabilidad explica por qué WAL debe persistirse antes que las páginas y cómo habilita recovery, backup y replicación.