9. Particionado y tablas grandes
Secciones
Ajustes de lectura
Ninguna voz para este idioma en este navegador.
Más voces
Código y términos técnicos en inglés
Desactivado: la voz de la página lo lee todo, sin pausa entre dos voces.
Ninguna voz para este idioma en este navegador.
Más voces
Más voces: Microsoft Edge ofrece voces naturales en línea, y Windows (Configuración › Hora e idioma › Voz), macOS y Android instalan otras.
El script de la lección es sql/09-partitioning.sql, y los ejercicios están en sql/09-exercises.sql. check.sh compara su salida con expected/09-partitioning.txt y expected/09-exercises.txt. Ambos cargan ci.step_history, la tabla de la lección 5 con 440 800 pasos a lo largo de 200 días.
| SQL Server | PostgreSQL |
|---|---|
| función de partición y esquema de partición | PARTITION BY RANGE, LIST o HASH sobre la tabla, una tabla por partición |
valores límite RANGE LEFT o RANGE RIGHT |
FOR VALUES FROM (…) TO (…): el límite inferior incluido, el superior excluido |
| grupos de archivos | tablespaces, rara vez necesarios; cada partición es su propio conjunto de archivos |
| índices alineados | un índice sobre la tabla particionada, creado en cada partición |
ALTER TABLE … SWITCH PARTITION |
ALTER TABLE … ATTACH PARTITION y DETACH PARTITION |
$PARTITION.function(value) |
tableoid::regclass, la partición de la que se leyó una fila |
| hasta 15 000 particiones | sin límite fijo; el tiempo de planificación y la memoria crecen con las particiones que conserva una consulta |
Una tabla particionada
Sección titulada «Una tabla particionada»SQL Server particiona una tabla mediante dos objetos separados, una función de partición y un esquema. El particionado declarativo de PostgreSQL pone el método y la clave en la tabla, y cada partición es una tabla en sí misma, con sus límites. El primer intento, con la misma clave primaria que ci.step_history (líneas 8-18):
-- Una tabla particionada no tiene filas propias: cada fila va a la partición cuyos límites contienen su claveCREATE TABLE ci.step_log ( id bigint NOT NULL, workflow_name text NOT NULL, job_name text NOT NULL, step_name text NOT NULL, conclusion ci.conclusion, started_at timestamptz NOT NULL, duration interval NOT NULL, PRIMARY KEY (id)) PARTITION BY RANGE (started_at);ERROR: unique constraint on partitioned table must include all partitioning columnsDETAIL: PRIMARY KEY constraint on table "step_log" lacks column "started_at" which is part of the partition key.No existe ningún índice que abarque todas las particiones: cada partición tiene el suyo, que solo puede comprobar la unicidad entre sus propias filas. La regla de la documentación es que «the constraint’s columns must include all of the partition key columns». Dos filas con el mismo id en dos meses distintos se aceptarían ambas, así que PostgreSQL rechaza la clave. Con started_at dentro, la clave se comprueba partición a partición y sigue siendo válida para toda la tabla (líneas 20-46):
-- Una clave primaria debe incluir la clave de partición: cada partición comprueba la unicidad en sus propias filasCREATE TABLE ci.step_log ( id bigint NOT NULL, workflow_name text NOT NULL, job_name text NOT NULL, step_name text NOT NULL, conclusion ci.conclusion, started_at timestamptz NOT NULL, duration interval NOT NULL, PRIMARY KEY (id, started_at)) PARTITION BY RANGE (started_at);
-- Una partición por mes, de febrero a septiembre de 2026, escritas por una consulta y ejecutadas por \gexecSELECT format('CREATE TABLE ci.step_log_%s PARTITION OF ci.step_log FOR VALUES FROM (%L) TO (%L)', to_char(m, 'YYYY_MM'), m, m + interval '1 month')FROM generate_series(timestamptz '2026-02-01', '2026-09-01', interval '1 month') AS mORDER BY m\gexec
INSERT INTO ci.step_logSELECT id, workflow_name, job_name, step_name, conclusion, started_at, duration FROM ci.step_history;VACUUM ANALYZE ci.step_log;
SELECT tableoid::regclass AS partition, count(*) AS rows, min(started_at)::date AS first_day, max(started_at)::date AS last_dayFROM ci.step_logGROUP BY tableoidORDER BY first_day;CREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLEINSERT 0 440800VACUUM partition | rows | first_day | last_day---------------------+-------+------------+------------ ci.step_log_2026_02 | 5079 | 2026-02-26 | 2026-02-28 ci.step_log_2026_03 | 68324 | 2026-03-01 | 2026-03-31 ci.step_log_2026_04 | 66120 | 2026-04-01 | 2026-04-30 ci.step_log_2026_05 | 68324 | 2026-05-01 | 2026-05-31 ci.step_log_2026_06 | 66120 | 2026-06-01 | 2026-06-30 ci.step_log_2026_07 | 68324 | 2026-07-01 | 2026-07-31 ci.step_log_2026_08 | 68324 | 2026-08-01 | 2026-08-31 ci.step_log_2026_09 | 30185 | 2026-09-01 | 2026-09-14(8 rows)- La consulta escribe ocho instrucciones
CREATE TABLE … PARTITION OF, una por mes, y el\gexecdepsqlejecuta cada fila del resultado como una instrucción: las nueve líneasCREATE TABLEson la tabla y sus ocho particiones. FROMincluye su límite yTOlo excluye, así que2026-03-01 00:00pertenece solo a marzo.tableoid::regclassda el nombre de la partición en la que está guardada cada fila.- Febrero solo tiene tres días, ya que el historial empieza el 26 de febrero.
Una fila que cae fuera de todas las particiones se rechaza (líneas 48-49):
-- Una fila fuera de los límites de todas las particiones no tiene adónde irINSERT INTO ci.step_log VALUES (0, 'Deploy to GitHub Pages', 'deploy', 'Checkout', 'success', '2026-10-01 12:00+00', '2 seconds');ERROR: no partition of relation "step_log" found for rowDETAIL: Partition key of the failing row contains (started_at) = (2026-10-01 12:00:00+00).Poda de particiones
Sección titulada «Poda de particiones»Con una condición sobre la clave de partición, el planificador deja fuera las particiones cuyos límites no pueden coincidir. pg_temp.plan es el EXPLAIN (ANALYZE, BUFFERS) de la lección 5 sin costes ni tiempos (líneas 51-58):
-- Poda de particiones: una condición sobre la clave de partición deja fuera las particiones que no pueden coincidirSELECT * FROM pg_temp.plan($$ SELECT count(*) FROM ci.step_log WHERE started_at >= '2026-09-14'$$);-- Sin condición sobre la clave, se leen todas las particionesSELECT * FROM pg_temp.plan($$ SELECT count(*) FROM ci.step_log WHERE step_name = 'Compile-fail doctests'$$); QUERY PLAN-------------------------------------------------------------------------------------------------------------- Aggregate (actual rows=1.00 loops=1) Buffers: shared hit+read=121 -> Index Only Scan using step_log_2026_09_pkey on step_log_2026_09 step_log (actual rows=1533.00 loops=1) Index Cond: (started_at >= '2026-09-14 00:00:00+00'::timestamp with time zone) Heap Fetches: 0 Index Searches: 1 Buffers: shared hit+read=121(7 rows)
QUERY PLAN----------------------------------------------------------------------------------- Aggregate (actual rows=1.00 loops=1) Buffers: shared hit+read=7453 -> Append (actual rows=9200.00 loops=1) Buffers: shared hit+read=7453 -> Seq Scan on step_log_2026_02 step_log_1 (actual rows=114.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 4965 Buffers: shared hit+read=86 -> Seq Scan on step_log_2026_03 step_log_2 (actual rows=1426.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 66898 Buffers: shared hit+read=1155 -> Seq Scan on step_log_2026_04 step_log_3 (actual rows=1380.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 64740 Buffers: shared hit+read=1118 -> Seq Scan on step_log_2026_05 step_log_4 (actual rows=1426.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 66898 Buffers: shared hit+read=1155 -> Seq Scan on step_log_2026_06 step_log_5 (actual rows=1380.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 64740 Buffers: shared hit+read=1118 -> Seq Scan on step_log_2026_07 step_log_6 (actual rows=1426.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 66898 Buffers: shared hit+read=1155 -> Seq Scan on step_log_2026_08 step_log_7 (actual rows=1426.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 66898 Buffers: shared hit+read=1155 -> Seq Scan on step_log_2026_09 step_log_8 (actual rows=622.00 loops=1) Filter: (step_name = 'Compile-fail doctests'::text) Rows Removed by Filter: 29563 Buffers: shared hit+read=511(36 rows)- La primera consulta lee una partición, la de septiembre, a través del índice de su clave primaria, cuya segunda columna es
started_at: 121 páginas. - La segunda no tiene condición sobre
started_at: un nodoAppendlee las ocho particiones, 7453 páginas. El particionado no acelera una consulta que no filtra por la clave; un índice sobrestep_namesí lo haría.
Con un parámetro, el valor se desconoce cuando se construye el plan. Un plan genérico conserva todas las particiones, y el ejecutor elimina las que el valor excluye cuando se ejecuta la consulta (líneas 60-65):
-- Un parámetro solo se conoce en la ejecución: el plan genérico poda entonces, e indica cuántas particiones se saltóPREPARE failures_since (timestamptz) AS SELECT count(*) FROM ci.step_log WHERE started_at >= $1 AND conclusion = 'failure';SET plan_cache_mode = force_generic_plan;EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF) EXECUTE failures_since('2026-09-01');RESET plan_cache_mode;PREPARESET QUERY PLAN--------------------------------------------------------------------------------------- Aggregate (actual rows=1.00 loops=1) -> Append (actual rows=301.00 loops=1) Subplans Removed: 7 -> Seq Scan on step_log_2026_09 step_log_1 (actual rows=301.00 loops=1) Filter: ((started_at >= $1) AND ((conclusion)::text = 'failure'::text)) Rows Removed by Filter: 29884(6 rows)
RESETSubplans Removed: 7 es esa poda en la ejecución. plan_cache_mode fuerza el plan genérico, al que una sentencia preparada puede pasar tras sus cinco primeras ejecuciones, tanto si se preparó en SQL como si lo hicieron Npgsql y pgjdbc (lección 4). enable_partition_pruning, activado por defecto, controla ambos tipos de poda.
Índices y filas que cambian de partición
Sección titulada «Índices y filas que cambian de partición»Un índice creado sobre la tabla particionada es un índice particionado: PostgreSQL crea un índice en cada partición y lo vincula al padre (líneas 67-72):
-- Un índice creado sobre la tabla particionada se crea en cada partición, y en las particiones añadidas despuésCREATE INDEX step_log_step_name ON ci.step_log (step_name);SELECT c.relname AS index, i.inhparent::regclass AS parentFROM pg_inherits AS i JOIN pg_class AS c ON c.oid = i.inhrelidWHERE i.inhparent = 'ci.step_log_step_name'::regclassORDER BY c.relname;CREATE INDEX index | parent--------------------------------+----------------------- step_log_2026_02_step_name_idx | ci.step_log_step_name step_log_2026_03_step_name_idx | ci.step_log_step_name step_log_2026_04_step_name_idx | ci.step_log_step_name step_log_2026_05_step_name_idx | ci.step_log_step_name step_log_2026_06_step_name_idx | ci.step_log_step_name step_log_2026_07_step_name_idx | ci.step_log_step_name step_log_2026_08_step_name_idx | ci.step_log_step_name step_log_2026_09_step_name_idx | ci.step_log_step_name(8 rows)Una partición creada más tarde recibe su propia copia del índice. Crearlo sobre una tabla grande bloquea la tabla; la documentación describe cómo construir primero el índice de cada partición con CONCURRENTLY, y después crear el índice del padre ON ONLY sobre el padre y vincularlos.
Un UPDATE que cambia la clave de partición mueve la fila: PostgreSQL la borra de una partición y la inserta en la otra (líneas 74-77):
-- Un UPDATE de la clave de partición mueve la fila a otra particiónUPDATE ci.step_log SET started_at = started_at - interval '1 month'WHERE id = (SELECT min(id) FROM ci.step_log WHERE started_at >= '2026-09-01')RETURNING tableoid::regclass AS now_in, started_at; now_in | started_at---------------------+------------------------ ci.step_log_2026_08 | 2026-08-01 01:37:05+00(1 row)
UPDATE 1Retención: desvincular y luego eliminar
Sección titulada «Retención: desvincular y luego eliminar»La razón habitual de las particiones mensuales es quitar un mes de una vez. Un DELETE de febrero deja tantas versiones de fila muertas que limpiar como filas ha borrado, como en la lección 6. Quitar la partición toma un bloqueo y borra archivos (líneas 79-87):
-- Retención: quitar un mes con DELETE deja filas muertas para vacuum; eliminar su partición borra un archivoCREATE EXTENSION pgstattuple;BEGIN;DELETE FROM ci.step_log WHERE started_at < '2026-03-01';SELECT pg_size_pretty(table_len) AS size, dead_tuple_count FROM pgstattuple('ci.step_log_2026_02');ROLLBACK;ALTER TABLE ci.step_log DETACH PARTITION ci.step_log_2026_02;SELECT count(*) AS rows_left FROM ci.step_log;DROP TABLE ci.step_log_2026_02;CREATE EXTENSIONBEGINDELETE 5079 size | dead_tuple_count--------+------------------ 688 kB | 5079(1 row)
ROLLBACKALTER TABLE rows_left----------- 435721(1 row)
DROP TABLEpgstattuplecuenta 5079 tuplas muertas en el archivo de 688 kB de febrero, revertido aquí.DETACH PARTITIONconvierte la partición en una tabla normal, que se puede archivar conpg_dump(lección 11) antes delDROP TABLE.DETACH PARTITION … CONCURRENTLYtoma un bloqueo más débil sobre el padre, pero «cannot be run in a transaction block and is not allowed if the partitioned table contains a default partition».
Particiones por defecto, y vincular una tabla ya cargada
Sección titulada «Particiones por defecto, y vincular una tabla ya cargada»Una partición por defecto recibe las filas que ninguna otra partición acepta: la fila de octubre rechazada más arriba entra. Después impide crear la partición a la que pertenece esa fila (líneas 89-93):
-- Una partición por defecto recibe las filas que ninguna otra partición aceptaCREATE TABLE ci.step_log_default PARTITION OF ci.step_log DEFAULT;INSERT INTO ci.step_log VALUES (0, 'Deploy to GitHub Pages', 'deploy', 'Checkout', 'success', '2026-10-01 12:00+00', '2 seconds');-- ... y después bloquea una partición nueva a la que pertenecen sus filasCREATE TABLE ci.step_log_2026_10 PARTITION OF ci.step_log FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');CREATE TABLEINSERT 0 1ERROR: updated partition constraint for default partition "step_log_default" would be violated by some rowAñadir una partición también recorre la partición por defecto, para comprobar que ninguna de sus filas pertenece a la nueva. Una partición por defecto es una red de seguridad, que debería quedarse vacía.
La costumbre de SQL Server de cargar una tabla de preparación y luego hacerle SWITCH se convierte en ATTACH PARTITION (líneas 95-107):
-- Cargar un mes aparte y luego vincularlo: una restricción CHECK con los mismos límites ahorra el recorrido que los validaDELETE FROM ci.step_log_default;CREATE TABLE ci.step_log_2026_10 (LIKE ci.step_log INCLUDING DEFAULTS INCLUDING CONSTRAINTS);INSERT INTO ci.step_log_2026_10SELECT id + 1000000, workflow_name, job_name, step_name, conclusion, started_at + interval '1 month', durationFROM ci.step_log_2026_09;ALTER TABLE ci.step_log_2026_10 ADD CONSTRAINT in_october CHECK (started_at >= '2026-10-01' AND started_at < '2026-11-01');ALTER TABLE ci.step_log ATTACH PARTITION ci.step_log_2026_10 FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');SELECT relname AS partition, pg_get_expr(relpartbound, oid) AS boundsFROM pg_classWHERE oid IN (SELECT inhrelid FROM pg_inherits WHERE inhparent = 'ci.step_log'::regclass)ORDER BY relname;DELETE 1CREATE TABLEINSERT 0 30184ALTER TABLEALTER TABLE partition | bounds------------------+-------------------------------------------------------------------------- step_log_2026_03 | FOR VALUES FROM ('2026-03-01 00:00:00+00') TO ('2026-04-01 00:00:00+00') step_log_2026_04 | FOR VALUES FROM ('2026-04-01 00:00:00+00') TO ('2026-05-01 00:00:00+00') step_log_2026_05 | FOR VALUES FROM ('2026-05-01 00:00:00+00') TO ('2026-06-01 00:00:00+00') step_log_2026_06 | FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00') step_log_2026_07 | FOR VALUES FROM ('2026-07-01 00:00:00+00') TO ('2026-08-01 00:00:00+00') step_log_2026_08 | FOR VALUES FROM ('2026-08-01 00:00:00+00') TO ('2026-09-01 00:00:00+00') step_log_2026_09 | FOR VALUES FROM ('2026-09-01 00:00:00+00') TO ('2026-10-01 00:00:00+00') step_log_2026_10 | FOR VALUES FROM ('2026-10-01 00:00:00+00') TO ('2026-11-01 00:00:00+00') step_log_default | DEFAULT(9 rows)- Octubre se carga en una tabla fuera de la tabla particionada, una copia de septiembre un mes más tarde, donde se puede comprobar antes de que nadie la vea.
ATTACH PARTITIONrecorre la tabla para comprobar sus límites, con un bloqueoACCESS EXCLUSIVEsobre ella. La documentación recomienda «creating aCHECKconstraint matching the expected partition constraint on the table prior to attaching it»: PostgreSQL confía entonces en la restricción y se salta el recorrido. La restricción se puede eliminar después.- Sobre el padre,
ATTACH PARTITIONtoma un bloqueoSHARE UPDATE EXCLUSIVE, más débil que el bloqueoACCESS EXCLUSIVEdeCREATE TABLE … PARTITION OF. pg_get_expr(relpartbound, oid)imprime los límites de cada partición, en UTC en esta sesión.
Lista y hash
Sección titulada «Lista y hash»PARTITION BY LIST enumera los valores de cada partición; PARTITION BY HASH reparte las filas entre un número fijo de particiones según el hash de la clave (líneas 109-125):
-- Particionado por lista y por hashCREATE TABLE ci.step_outcomes (LIKE ci.step_log) PARTITION BY LIST (conclusion);CREATE TABLE ci.step_outcomes_success PARTITION OF ci.step_outcomes FOR VALUES IN ('success');CREATE TABLE ci.step_outcomes_other PARTITION OF ci.step_outcomes DEFAULT;CREATE TABLE ci.step_buckets (LIKE ci.step_log) PARTITION BY HASH (job_name);SELECT format('CREATE TABLE ci.step_buckets_%s PARTITION OF ci.step_buckets FOR VALUES WITH (MODULUS 4, REMAINDER %s)', r, r)FROM generate_series(0, 3) AS rORDER BY r\gexecINSERT INTO ci.step_outcomes SELECT * FROM ci.step_log;INSERT INTO ci.step_buckets SELECT * FROM ci.step_log;SELECT tableoid::regclass AS partition, count(*) AS rows, count(DISTINCT job_name) AS jobsFROM ci.step_outcomes GROUP BY tableoidUNION ALLSELECT tableoid::regclass, count(*), count(DISTINCT job_name)FROM ci.step_buckets GROUP BY tableoidORDER BY 1;CREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLEINSERT 0 465905INSERT 0 465905 partition | rows | jobs--------------------------+--------+------ ci.step_outcomes_success | 441875 | 64 ci.step_outcomes_other | 24030 | 17 ci.step_buckets_0 | 83288 | 18 ci.step_buckets_1 | 113779 | 15 ci.step_buckets_2 | 175176 | 13 ci.step_buckets_3 | 93662 | 18(6 rows)- La partición
successcontiene el 95 % de las filas: el particionado por lista sigue a los datos, estén equilibrados o no. - El particionado por hash de 64 nombres de job distintos deja una partición con el doble de filas que otra. Un hash reparte claves, no filas: unos pocos jobs con mucha actividad pesan más que muchos jobs poco frecuentes. Las particiones hash no ayudan a la poda con rangos ni a la retención; dividen una tabla que no tiene un rango natural, para el mantenimiento o el trabajo en paralelo.
En Aurora
Sección titulada «En Aurora»Por verificar: nada de esta sección se ejecutó en AWS. El particionado declarativo es el de PostgreSQL, y Aurora no lo cambia. Lo que cambia son las herramientas a su alrededor.
- pg_partman y pg_cron. Las particiones del mes que viene tienen que existir antes de que empiece. pg_partman las crea por adelantado y elimina las antiguas, y pg_cron ejecuta su mantenimiento según una programación. La tabla de extensiones de Aurora PostgreSQL 18 da pg_partman 5.4.3 en 18.4 (5.2.4 en 18.3) y pg_cron 1.6.7; la página de pg_partman de AWS dice que «is supported on Aurora PostgreSQL versions 12.6 and higher».
- Límites. No encontré ninguna cuota de particiones ni de número de tablas en las cuotas y límites de Aurora. La misma página da un tamaño máximo de tabla de 32 TiB para Aurora PostgreSQL, y recomienda «table design best practices, such as partitioning of large tables»: cada partición es una tabla, así que el límite se aplica por partición.
- Blue/Green Deployments. Replican con replicación lógica (lección 11), y las consideraciones de AWS indican que «creating new partitions on partitioned tables isn’t supported during blue/green deployments for Aurora», y que «the pg_partman extension must be disabled in the blue environment».
- Zero-ETL hacia Redshift. Para las integraciones zero-ETL, «the table partitions will be replicated to Amazon Redshift. However, the partitioned table itself isn’t replicated».
Puntos clave
Sección titulada «Puntos clave»- Una tabla particionada es un padre sin filas y una tabla por partición;
FROMes inclusivo,TOexclusivo. - Las claves primarias y las restricciones únicas deben incluir la clave de partición.
- La poda necesita una condición sobre la clave; con parámetros, ocurre en la ejecución (
Subplans Removed). - Los índices creados sobre el padre se crean en cada partición, presente y futura.
- La retención es
DETACHyDROP, noDELETE: sin filas muertas, sin vacuum. - Una partición por defecto recoge las filas perdidas, y obliga a recorrerla al añadir particiones.
- Cargar aparte, añadir una restricción
CHECKequivalente, y luegoATTACH: el patrónSWITCHde SQL Server. - Las particiones hash reparten claves, no filas.
Ejercicios
Sección titulada «Ejercicios»Las soluciones están en sql/09-exercises.sql.
- Particiona una copia de
ci.runspor workflow: una partición paraDeploy to GitHub Pages, otra para los tres workflows de ejemplos de los cursos, y una partición por defecto. ¿Cuántas ejecuciones contiene cada una, y qué particiones lee una consulta deRust course examples?
Solución
-- Ejercicio 1: ejecuciones particionadas por workflow, con su propia partición para los despliegues y una por defecto para el resto,-- y un plan que solo lee la partición de 'Rust course examples'CREATE TABLE ci.runs_by_workflow (LIKE ci.runs) PARTITION BY LIST (workflow_name);CREATE TABLE ci.runs_deploy PARTITION OF ci.runs_by_workflow FOR VALUES IN ('Deploy to GitHub Pages');CREATE TABLE ci.runs_examples PARTITION OF ci.runs_by_workflow FOR VALUES IN ('Rust course examples', 'Java course examples', 'WSL containers examples');CREATE TABLE ci.runs_other PARTITION OF ci.runs_by_workflow DEFAULT;INSERT INTO ci.runs_by_workflow SELECT * FROM ci.runs;SELECT tableoid::regclass AS partition, count(*) AS runs FROM ci.runs_by_workflow GROUP BY tableoid ORDER BY tableoid::regclass::text;SELECT * FROM pg_temp.plan($$ SELECT count(*) FROM ci.runs_by_workflow WHERE workflow_name = 'Rust course examples'$$);CREATE TABLECREATE TABLECREATE TABLECREATE TABLEINSERT 0 125 partition | runs------------------+------ ci.runs_deploy | 65 ci.runs_examples | 27 ci.runs_other | 33(3 rows)
QUERY PLAN------------------------------------------------------------------------------ Aggregate (actual rows=1.00 loops=1) Buffers: shared hit+read=1 -> Seq Scan on runs_examples runs_by_workflow (actual rows=18.00 loops=1) Filter: (workflow_name = 'Rust course examples'::text) Rows Removed by Filter: 9 Buffers: shared hit+read=1(6 rows)LIKE ci.runs copia las columnas y sus restricciones NOT NULL, no la clave primaria. La poda conserva runs_examples, la partición cuya lista contiene el valor; la partición por defecto también queda fuera, ya que el valor está en la lista de otra partición.
- Particiona los pasos de septiembre en dos niveles: por mes y, dentro del mes, por conclusión. Muestra el árbol con
pg_partition_tree, y el plan de una consulta de los fallos desde el 14 de septiembre.
Solución
-- Ejercicio 2: dos niveles, meses y luego conclusión, y el árbol que devuelve pg_partition_treeCREATE TABLE ci.step_months (LIKE ci.step_history) PARTITION BY RANGE (started_at);CREATE TABLE ci.step_months_2026_09 PARTITION OF ci.step_months FOR VALUES FROM ('2026-09-01') TO ('2026-10-01') PARTITION BY LIST (conclusion);CREATE TABLE ci.step_months_2026_09_success PARTITION OF ci.step_months_2026_09 FOR VALUES IN ('success');CREATE TABLE ci.step_months_2026_09_other PARTITION OF ci.step_months_2026_09 DEFAULT;INSERT INTO ci.step_months SELECT * FROM ci.step_history WHERE started_at >= '2026-09-01';SELECT relid, parentrelid, isleaf, level FROM pg_partition_tree('ci.step_months') ORDER BY level, relid::text;SELECT * FROM pg_temp.plan($$ SELECT count(*) FROM ci.step_months WHERE started_at >= '2026-09-14' AND conclusion = 'failure'$$);CREATE TABLECREATE TABLECREATE TABLECREATE TABLEINSERT 0 30185 relid | parentrelid | isleaf | level--------------------------------+------------------------+--------+------- ci.step_months | | f | 0 ci.step_months_2026_09 | ci.step_months | f | 1 ci.step_months_2026_09_other | ci.step_months_2026_09 | t | 2 ci.step_months_2026_09_success | ci.step_months_2026_09 | t | 2(4 rows)
QUERY PLAN--------------------------------------------------------------------------------------------------------------------------------- Aggregate (actual rows=1.00 loops=1) Buffers: shared hit+read=36 -> Seq Scan on step_months_2026_09_other step_months (actual rows=15.00 loops=1) Filter: ((started_at >= '2026-09-14 00:00:00+00'::timestamp with time zone) AND ((conclusion)::text = 'failure'::text)) Rows Removed by Filter: 1512 Buffers: shared hit+read=36(6 rows)Una partición declarada con su propio PARTITION BY está a su vez particionada: el nivel 1 tampoco tiene filas. Ambas condiciones podan, una por nivel, y la consulta solo lee la partición other de septiembre.
- Escribe un procedimiento que desvincule y elimine las particiones mensuales de una tabla que terminan en una fecha dada o antes, y úsalo para conservar de junio a septiembre.
Solución
-- Ejercicio 3: un procedimiento que desvincula y elimina las particiones mensuales que terminan antes de una fechaCREATE TABLE ci.step_log (LIKE ci.step_history) PARTITION BY RANGE (started_at);SELECT format('CREATE TABLE ci.step_log_%s PARTITION OF ci.step_log FOR VALUES FROM (%L) TO (%L)', to_char(m, 'YYYY_MM'), m, m + interval '1 month')FROM generate_series(timestamptz '2026-02-01', '2026-09-01', interval '1 month') AS mORDER BY m\gexec
CREATE PROCEDURE ci.drop_partitions_before(parent regclass, cutoff timestamptz)LANGUAGE plpgsql AS $$DECLARE part regclass;BEGIN FOR part IN SELECT c.oid::regclass FROM pg_inherits AS i JOIN pg_class AS c ON c.oid = i.inhrelid -- el límite superior, leído de la definición de la partición: FOR VALUES FROM ('…') TO ('…') WHERE i.inhparent = parent AND substring(pg_get_expr(c.relpartbound, c.oid) FROM $re$TO \('([^']+)'\)$re$)::timestamptz <= cutoff ORDER BY c.relname LOOP -- primero el nombre: una vez eliminada la tabla, su regclass se imprime como un OID sin más RAISE NOTICE 'dropping %', part; EXECUTE format('ALTER TABLE %s DETACH PARTITION %s', parent, part); EXECUTE format('DROP TABLE %s', part); END LOOP;END$$;
CALL ci.drop_partitions_before('ci.step_log', '2026-06-01');SELECT relid FROM pg_partition_tree('ci.step_log') WHERE isleaf ORDER BY relid::text;CREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE TABLECREATE PROCEDURENOTICE: dropping ci.step_log_2026_02NOTICE: dropping ci.step_log_2026_03NOTICE: dropping ci.step_log_2026_04NOTICE: dropping ci.step_log_2026_05CALL relid--------------------- ci.step_log_2026_06 ci.step_log_2026_07 ci.step_log_2026_08 ci.step_log_2026_09(4 rows)El límite superior vuelve del catálogo como texto, que se lee con una expresión regular y se convierte a timestamptz. Un regclass se imprime como un nombre mientras la tabla existe; después de DROP TABLE, el mismo valor se imprime como un número, y por eso el aviso va primero. Es el trabajo que los ajustes de retención de pg_partman hacen según una programación.
Fuentes
Sección titulada «Fuentes»- Documentación de PostgreSQL 18: particionado de tablas,
CREATE TABLE,ALTER TABLE, ajustes de planificación de consultas,psql,pgstattuple, funciones de información del sistema, funciones de información de particiones, notas de versión de PostgreSQL 18 - SQL Server: tablas e índices particionados,
CREATE PARTITION FUNCTION,ALTER TABLE … SWITCH - AWS: versiones de las extensiones, pg_partman en Aurora, cuotas y límites, consideraciones de blue/green, integraciones zero-ETL, todas consultadas el 2026-09-16
- pg_partman, pg_cron