postgres declarative partitioning - ghdrako/doc_snipets GitHub Wiki
- https://postgresql.itversity.com/05_partitioning_tables_and_indexes/03_list_partitioning.html
- https://hevodata.com/learn/postgresql-partitions/
- https://www.postgresql.org/docs/current/ddl-partitioning.html
- https://github.com/pgpartman/pg_partman
- https://blog.anayrat.info/en/2021/09/01/partitioning-use-cases-with-postgresql/
PostgreSQL does not support join between partition-leafs and non-partition tables before merging.
- Partitioning to manage retention - Due to the MVCC model, massive deletion leads to bloat in the tables.Deleting complete partition will be fast and the tables will not be bloated.
- Partitioning to control index bloat - na zakończonej partycji można wykonać reindex i jest optymalny. Ciągłe zmiany w indeksie powodują jego nieoptymalnosc chociaż z każda wersja indeks b-tree jest lepiej zoptymalizowany.
Scenariusz: logi / zamówienia / zdarzenia (dane rosnące w czasie) - gdzie większość zmian dotyczy najnowszych danych
przyklad
--Na początku miesiąca tworzymy nowa partycje:
CREATE TABLE events_2025_03 PARTITION OF events
FOR VALUES FROM ('2025-03-01') TO ('2025-04-01');
-- Pod koniec miesiąca → porządkujemy starą partycję
REINDEX TABLE events_2025_02;
CLUSTER events_2025_02 USING events_created_at_idx;
Zyskujemy zbalansowany minimalny indeks i dane skorelowane z indeksem wiec zapytania historyczne sa szybsze.
Uwaga!!! CLUSTER blokuje tabelę wyłącznie do odczytu na czas działania, przepisuje całą tabelę (duże I/O), jest ciężką operacją. Dlatego w praktyce robi się to tylko na zamkniętych partycjach (archiwalnych), poza godzinami szczytu, albo w maintenance window.
- Partitioning for low cardinality - partycjonowania jako alternatywy dla indeksu (zwłaszcza częściowego) na kolumnie o bardzo niskiej kardynalności i bardzo „skośnym” rozkładzie danych (skrew).
orders (
order_id bigint,
state text, -- np. 'new', 'payment_in_progress', 'delivering', 'delivered'
created_at timestamptz,
...
);
CREATE INDEX ON orders (order_id)
WHERE state IN ('payment_in_progress','delivering');
Po kilku latach 99% wierszy ma state = 'delivered' 1% to stany „aktywne” (payment_in_progress, delivering itd.). Z czasem coraz więcej zamówień przechodzi do delivered każdy taki update usuwa wpis z indeksu częściowego zostawia „martwą przestrzeń” (bloat) w stronach indeksu. Aby tego uniknac zamiast indeksować state, partyjonujesz tabelę po state:
CREATE TABLE orders (
order_id bigint,
state text,
created_at timestamptz,
...
) PARTITION BY LIST (state);
CREATE TABLE orders_delivered
PARTITION OF orders FOR VALUES IN ('delivered');
CREATE TABLE orders_in_progress
PARTITION OF orders
FOR VALUES IN ('payment_in_progress','delivering');
orders (tabela logiczna)
│
├── orders_delivered → ~99% danych
└── orders_in_progress → ~1% danych
Dzieki temu nie musimy przeszukiwać indeks,skakać po wielu blokach,a potem wracać do tabeli. Tutaj skanujemy tylko małą partycję orders_in_progress, która: zawiera tylko aktywne zamówienia,jest bardzo mała w porównaniu do całej tabeli.
Problemy - przenoszenie wierszy miedzy partycjami:
UPDATE orders
SET state = 'delivered'
WHERE order_id = 123;
Update on orders
-> Result
-> Append
-> Delete on orders_in_progress
-> Insert on orders_delivered
co realizowane jest jako
DELETE FROM orders_in_progress WHERE order_id = 123;
INSERT INTO orders_delivered VALUES (...);
Mimo tego dziala to dobrze gdy wiersze wedruja tylko w jedna strone z malej do duzej partycji i to na stale.
Rozwiazanie hybrydowe
orders
├─ orders_2025_01 (RANGE by created_at)
│ ├─ orders_2025_01_in_progress (LIST)
│ └─ orders_2025_01_delivered (LIST)
Korzysci to dobre zarządzanie retencją, plus korzyści z podziału po state.
- Partitioning to get more accurate statistics - With partitioning, we could individualy per partition colect statistics(default_statistic_target), which allows us to increase the accuracy and beter plan. We can collect statistic on hot partition more frequntly.
- partitionwise join & partitionwise aggregate
- Storage tiering - store a part of the table on a different storage For example recent data on a fast tablespace on NVMe SSD
The PostgreSQL document “Table Partitioning”[371] has a rule of thumb based on the server instance memory and table size, about when to partition: The size of the table should exceed the physical memory of the database server.
Other recomendation - They recommend partitioning when a table exceeds 100 GB in size.
PostgreSQL supports three types of declarative partitioning:
- Range partitioning
- List partitioning
- Hash partitioning(postgres 11)

We can create global primary key constraint for partitioned table that is passed down to all partitions. The pk must include all partitioned columns.
Index created on partitioned table will be created automatically for all partitions. Such indexes are still physically separated indexes but cat't be removed for specyfic partition.
Unique index support global uniquess but must include partition key.
If you have a primary key for the table, the partition column should be a part of the primary key. If you choose the primary key not as a part of the partition column, create the primary key on the child table.

One limitation of PostgreSQL declarative partitioning, when compared to some other databases like Oracle, is the impossibility to create global indexes. This includes the unique index that is necessary to enforce a primary key.
On partitioned tables, all primary keys, unique constraints and unique indexes must contain the partition expression. That is because indexes on partitioned tables are implemented by individual indexes on each partition, and there is no way to enforce uniqueness across different indexes.
we can create unique id index for every partition, but it does not guarantee unique across whole partitioned table
- Dodać klucz partycjonowania do UNIQUE
CREATE TABLE orders (
order_id bigint,
created_at date,
customer_id bigint,
PRIMARY KEY (order_id, created_at)
) PARTITION BY RANGE (created_at);
- Wada: klucz logiczny zmienia się z
order_idna(order_id, created_at). Czyli nie gwarantuje unikalnosciorder_id
- Partycjonowanie po kolumnach UNIQUE
- kolumny wymagające globalnej unikalności stają się partition key
CREATE TABLE payments (
customer_id bigint,
ext_payment_id text,
amount numeric
)
PARTITION BY HASH (customer_id, ext_payment_id);
ALTER TABLE payments
ADD CONSTRAINT uq_payment
UNIQUE (customer_id, ext_payment_id);
Daje prawdziwa globalna unikalność ale zmieniasz strategię partycjonowania - często tracisz sens biznesowy partycji. To jest najbliższy odpowiednik Oracle Global Unique Index w PostgreSQL. Daje prawdziwa globalna unikalnosc biznesowa i do tego celu jest polecane. 3. Oddzielna tabela „unikalności”
CREATE TABLE order_keys (
external_id text PRIMARY KEY
);
CREATE TABLE orders (
id bigint,
external_id text,
created_at date
) PARTITION BY RANGE(created_at);
-- a nawet lepiej tabela z kluczem obcym wymuszajacym zgodnosc external_id
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY,
external_id text NOT NULL,
created_at timestamptz NOT NULL,
FOREIGN KEY (external_order_id)
REFERENCES order_registry(external_id)
)
PARTITION BY RANGE(created_at);
Transaction:
BEGIN;
INSERT INTO order_keys(external_id)
VALUES ('ABC-123');
INSERT INTO orders(...)
VALUES (...);
COMMIT;
prawdziwa globalna unikalność z zachowaniem partycjonowania np po czasie
Jeszcze lepsza wersja: surrogate key
CREATE TABLE order_registry (
registry_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_order_id text UNIQUE NOT NULL
);
a potem: `` orders.registry_id bigint REFERENCES order_registry(registry_id)
FK po bigint: mniejsze indexy; lepszy cache locality; szybsze joiny
Dodanie triggersa dla insertu:
CREATE OR REPLACE FUNCTION ensure_unique_order() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN INSERT INTO order_registry(external_order_id) VALUES (NEW.external_order_id);
RETURN NEW;
EXCEPTION WHEN unique_violation THEN RAISE EXCEPTION 'external_order_id already exists: %', NEW.external_order_id; END; $$;
CREATE TRIGGER trg_orders_unique BEFORE INSERT ON orders FOR EACH ROW EXECUTE FUNCTION ensure_unique_order();
4. Trigger sprawdzający wszystkie partycje
CREATE TRIGGER check_unique BEFORE INSERT
Trigger robi:
SELECT 1 FROM parent_table WHERE external_id = NEW.external_id LIMIT 1;
Uwaga rozwiazanie z trigerem ma problem przy jednoczesnych tranzakcjach: https://www.cybertec-postgresql.com/en/triggers-to-enforce-constraints/#what-is-wrong-with-our-trigger-constraint
To enforce uniqueness across the entire partitioned table, we can use a PostgreSQL function and trigger.
CREATE TABLE partitioned_example ( id UUID, col1 VARCHAR(64), date TIMESTAMP(3), PRIMARY KEY (id, date) ) PARTITION BY RANGE (date); ERROR: unique constraint on partitioned table must include all partitioning columns DETAIL: PRIMARY KEY constraint on table "partitioned_example" lacks column "date" which is part of the partition key.
id UUID,
col1 VARCHAR(64),
date TIMESTAMP(3),
PRIMARY KEY (id, date)
) PARTITION BY RANGE (date);
CREATE TABLE partitioned_example_20240901_to_20240930 PARTITION OF partitioned_example FOR VALUES FROM ('2024-09-01') TO ('2024-10-01');
CREATE OR REPLACE FUNCTION check_unique_id() RETURNS TRIGGER AS $$ BEGIN IF EXISTS ( SELECT 1 FROM partitioned_example WHERE id = NEW.id ) THEN RAISE EXCEPTION 'check_unique_id: Duplicate id value: %', NEW.id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql;
CREATE TRIGGER enforce_unique_id BEFORE INSERT ON partitioned_example FOR EACH ROW EXECUTE FUNCTION check_unique_id();
CREATE OR REPLACE FUNCTION ensure_unique_order() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN INSERT INTO order_registry(external_order_id) VALUES (NEW.external_order_id) ON CONFLICT DO NOTHING;
IF NOT FOUND THEN
RAISE EXCEPTION
'external_order_id already exists: %',
NEW.external_order_id;
END IF;
RETURN NEW;
END; $$;
5. UUID / sequence jako globalnie unikalny identyfikator
id uuid DEFAULT gen_random_uuid() --or id bigint GENERATED ALWAYS AS IDENTITY
Sekwencje PostgreSQL są globalne dla wszystkich partycji. Rozwiazuje techniczną unikalność ID ale nie biznesowa
6. Reorganizacja modelu danych
* split tabeli:
* tabela partycjonowana po PK
* osobna tabela dla unique columns
* Czyli coś podobnego do #3, ale bardziej formalne i relacyjne.
#### Vacuum
* https://www.percona.com/blog/postgresql-vacuuming-to-optimize-database-performance-and-reclaim-space
#### Parallel processing
* https://pganalyze.com/blog/5mins-postgres-partition-wise-joins-aggregates-query-performance
* https://www.postgresql.fastware.com/postgresql-insider-prf-prt-mec
By setting `enable_partition_pruning=on`,`enable_partitionwise_join=on`, and `enable_partitionwise_aggregate=on` either at the **session or system level**, you can leverage the **parallel processing** capabilities of PostgreSQL.
## Declarative partitioning
### Create partition table
#### List partitioning
1. Sanity check
DROP TABLE IF EXISTS part_tags cascade;
2. create our parent table
CREATE TABLE part_tags ( pk INTEGER NOT NULL DEFAULT nextval('part_tags_pk_seq') , level INTEGER NOT NULL DEFAULT 0, tag VARCHAR (255) NOT NULL, primary key (pk,level) ) PARTITION BY LIST (level);
Note is that the field used to partition the data must be part of the primary key.
3. Define the child tables
CREATE TABLE part_tags_level_0 PARTITION OF part_tags FOR VALUES IN (0); CREATE TABLE part_tags_level_1 PARTITION OF part_tags FOR VALUES IN (1); CREATE TABLE part_tags_level_2 PARTITION OF part_tags FOR VALUES IN (2); CREATE TABLE part_tags_level_3 PARTITION OF part_tags FOR VALUES IN (3);
4.
CREATE INDEX part_tags_tag on part_tags using GIN (tag gin_trgm_ops);
5.
\d part_tags; \d part_tags_level_0; select * from part_tags; select * from part_tags_level_0; select * from part_tags_level_1;
### Range partitioning
* `MINVALUE/MAXVALUE`
* wyzsza wartosc wyłaczona
CREATE TABLE t2(c1 integer, c2 text) PARTITION BY RANGE (c1); CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM (1) TO (100); -- <1,100) CREATE TABLE t2_2 PARTITION OF t2 FOR VALUES FROM (100) TO (MAXVALUE); -- <100, inf)
INSERT INTO t2 VALUES (0); ERROR: no PARTITION OF relation "t2" found for row DETAIL: Partition key of the failing row contains (c1) = (0). INSERT INTO t2 VALUES (10, 'dix'); INSERT INTO t2 VALUES (100, 'cent'); INSERT INTO t2 VALUES (10000, 'dix mille'); SELECT * FROM t2 ; c1 | c2 -------+---------- 10 | dix 100 | cent 10000 | dix mille (3 lignes) SELECT * FROM t2_2 ; c1 | c2 -------+---------- 100 | cent 10000 | dix mille (2 lignes)
Columna ` tableoid` pokazuje partycje do ktorej trafil wiersz:
SELECT ctid, tableoid::regclass, * FROM t2 ; ctid | tableoid | c1 | c2 -------+----------+-------+---------- (0,1) | t2_1 | 10 | dix (0,1) | t2_2 | 100 | cent (0,2) | t2_2 | 10000 | dix mille
Górne granice przedziałów są wyłączone! Wartość 100 trafi więc do drugiego przedziału.
CREATE TABLE erp.payments_p ( id bigint GENERATED ALWAYS AS IDENTITY, tstamp timestamp with time zone NOT NULL, amount numeric NOT NULL, invoice bigint NOT NULL ) PARTITION BY RANGE (tstamp);
DO $$ BEGIN FOR i IN 0..28 LOOP EXECUTE format('CREATE TABLE erp.%s PARTITION OF erp.payments_p FOR VALUES FROM (''%s'') TO (''%s'')', 'payments_p_' || extract('year' FROM date_trunc('month', now()) - (i * INTERVAL '1 month')) || '_' || extract('month' FROM date_trunc('month', now()) - (i * INTERVAL '1 month')), date_trunc('month', now()) - (i * INTERVAL '1 month'), date_trunc('month', now()) + ((1 - i) * INTERVAL '1 month')); END LOOP; END; $$; DO
The script’s loop starts 28 months before the current month and creates
each month’s partition like so:
CREATE TABLE erp.payments_p__ PARTITION OF erp.payments_p FOR VALUES FROM () TO ()
Example 1
DROP TABLE IF EXISTS part_tags cascade;
CREATE TABLE part_tags ( pk INTEGER NOT NULL DEFAULT nextval('part_tags_pk_seq'), ins_date date not null default now()::date, tag VARCHAR (255) NOT NULL, level INTEGER NOT NULL DEFAULT 0, primary key (pk,ins_date) ) PARTITION BY RANGE (ins_date);
CREATE TABLE part_tags_date_01_2020 PARTITION OF part_tags FOR VALUES FROM ('2020-01-01') TO ('2020-01-31'); CREATE TABLE part_tags_date_02_2020 PARTITION OF part_tags FOR VALUES FROM ('2020-02-01') TO ('2020-02-28'); CREATE TABLE part_tags_date_03_2020 PARTITION OF part_tags FOR VALUES FROM ('2020-03-01') TO ('2020-03-31'); CREATE TABLE part_tags_date_04_2020 PARTITION OF part_tags FOR VALUES FROM ('2020-04-01') TO ('2020-04-30') CREATE INDEX part_tags_tag on part_tags using GIN (tag gin_trgm_ops);
\d part_tags; \d part_tags_date_01_2020; select * from part_tags; select * from part_tags_date_01_2020; select * from part_tags_date_02_2020; select * from part_tags_date_03_2020;
Example 2
1. Create parent payments table:
CREATE TABLE partitioned_payments( id serial not null, created_at timestamp NOT NULL, updated_at timestamp NOT NULL, amount double precision NOT NULL, total double precision NOT NULL, description json DEFAULT NULL, tax double precision NOT NULL, customer_id integer NOT NULL, shipping_address text DEFAULT NULL, status varchar(255) DEFAULT NULL ) PARTITION BY RANGE (created_at);
2. Create partitions. It makes sense to create a few more partitions in advance::
CREATE TABLE payments_2020_1_1_7 PARTITION OF partitioned_payments FOR VALUES FROM ('2020-01-01') TO ('2020-01-07 23:59:59');
CREATE TABLE payments_2020_1_8_15 PARTITION OF partitioned_payments FOR VALUES FROM ('2020-01-08') TO ('2020-01-15 23:59:59');
CREATE TABLE payments_2020_1_16_23 PARTITION OF partitioned_payments FOR VALUES FROM ('2020-01-16') TO ('2020-01-23 23:59:59');
and automateadd New partition in cron
3. Create indexes:
// for partition key columns CREATE INDEX payments_2020_1_1_7_created_at_idx ON payments_2020_1_1_7(created_at); CREATE INDEX payments_2020_1_8_15_created_at_idx ON payments_2020_1_8_15(created_at); CREATE INDEX payments_2020_1_16_23_created_at_idx ON payments_2020_1_16_23(created_at);
// for id column CREATE INDEX payments_2020_1_1_7_id_idx ON payments_2020_1_1_7(id); CREATE INDEX payments_2020_1_8_15_id_idx ON payments_2020_1_8_15(id); CREATE INDEX payments_2020_1_16_23_id_idx ON payments_2020_1_16_23(id);
4. Schedule for partition creation. As our last partition will have rows till 23 of Jan, 2020 it means that on 23 of Jan we should create a new partitions for new rows having created_at greater then 23 of Jan, 2020
0 0 23 * * /project_dir/create_payments_partitions.sh
where create_payments_partitions.sh:
#!/bin/sh
year=$(date +%Y)
month=$(date +%m)
next_year=$(date -d "+1 months" +%Y)
next_month=$(date -d "+1 months" +%m)
month_last_day=$(date -d "date +%Y%m01 +1 month -1 day" +%d)
sql=$(sed "s/|year|/$year/g" ./declarative_new_partitions_template.sql | sed "s/|month|/$month/g" | sed "s/|next_month|/$next_month/g" | sed "s/|next_year|/$next_year/g" | sed "s/|month_last_day|/$month_last_day/g" )
docker exec -it postgres-13.10 psql -U postgres partitioning_demo -c "$sql"
and declarative_new_partitions_template.sql :
CREATE TABLE payments_|year|_|month|24|month_last_day| PARTITION OF partitioned_payments FOR VALUES FROM ('|year|-|month|-24') TO ('|year|-|month|-|month_last_day| 23:59:59');
CREATE TABLE payments_|next_year|_|next_month|_1_7 PARTITION OF partitioned_payments FOR VALUES FROM ('|next_year|-|next_month|-01') TO ('|next_year|-|next_month|-07 23:59:59');
CREATE TABLE payments_|next_year|_|next_month|_8_15 PARTITION OF partitioned_payments FOR VALUES FROM ('|next_year|-|next_month|-08') TO ('|next_year|-|next_month|-15 23:59:59');
CREATE TABLE payments_|next_year|_|next_month|_16_23 PARTITION OF partitioned_payments FOR VALUES FROM ('|next_year|-|next_month|-16') TO ('|next_year|-|next_month|-23 23:59:59');
And you can test
INSERT INTO partitioned_payments (created_at, updated_at, amount, total, description, tax, customer_id, shipping_address, status) SELECT date('2020-01-' || random_between(1,7)), NOW() - '1 year'::INTERVAL * ROUND(RANDOM() * 100), ROUND(random() * 1000)::double precision , ROUND(random() * 1000)::double precision , '{}', ROUND(random() * 1000)::double precision, ROUND(RANDOM() * 1000)::INTEGER, SUBSTRING(md5(random()::text), 0, 25), SUBSTRING(md5(random()::text), 0, 100) FROM generate_series(1, 20);
and check the explain for the select:
EXPLAIN (analyze,timing off) SELECT * FROM partitioned_payments WHERE created_at BETWEEN '2020-01-01' AND '2020-01-07' ORDER BY id asc LIMIT 10 OFFSET 100;
In the execution plan we can see:
Scan on partitioned_payments... ... Index Scan using payments_2020_1_1_7_created_at_idx on payments_2020_1_1_7 partitioned_payments (cost=0.00..8.27 rows=1 width=628)
So only partitioned_payments and payments_2020_1_1_7 tables are being accessed to find the rows.
Example 3
CREATE TABLE data ( payload integer ) PARTITION BY RANGE (payload); CREATE TABLE negatives PARTITION OF data FOR VALUES FROM (MINVALUE) TO (0); CREATE TABLE positives PARTITION OF data FOR VALUES FROM (0) TO (MAXVALUE); INSERT INTO data VALUES (5); SELECT * FROM positives; --move between partition UPDATE data SET payload = -10 WHERE payload = 5 RETURNING *; SELECT * FROM negatives; DROP TABLE negatives CREATE TABLE p_def PARTITION OF data DEFAULT;
Example 4
CREATE TABLE log ( log_id integer NOT NULL GENERATED ALWAYS AS IDENTITY, log_date date not null, log_text char(10), constraint pk_log primary key(log_id,log_date) ) PARTITION BY RANGE (log_date); ------- Creation of Parent Table CREATE TABLE log2023_m1 PARTITION OF log FOR VALUES FROM ('2023-01-01') TO ('2023-01-31'); --- Creation of Child table CREATE TABLE log2023_m2 PARTITION OF log FOR VALUES FROM ('2023-02-01') TO ('2023-03-01'); CREATE TABLE log_default PARTITION OF log DEFAULT ; --Default INSERT INTO log(log_date,log_text) values('2023-01-01','log010123'); INSERT INTO log(log_date,log_text) values('2023-04-01','log010123'); ALTER TABLE log DETACH PARTITION log2023_m1; ---- Detach Partition ALTER TABLE log ATTACH PARTITION log2023_m1 -----Attach Partition FOR VALUES FROM ('2023-01-01') TO ('2023-02-01'); SELECT tableoid, tableoid::regclass, * FROM log; tableoid | tableoid | log_id | log_date | log_text ----------+-------------+--------+------------+------------ 16439 | log2023_m1 | 1 | 2023-01-01 | log010123 16449 | log_default | 2 | 2023-04-01 | log010123 (2 rows) UPDATE log set log_date='2023-08-01'; SELECT tableoid, tableoid::regclass, * FROM log;
UPDATE log set log_date='2023-07-01'; SELECT tableoid, tableoid::regclass, * FROM log;
#### Hash partitioning
Aby równomiernie rozkladac dane po partycjach aklucz partycjonowania nie jest oczywisty: – Haszowanie wartości według partycji – wskazać moduł i resztę – klucz partycjonowania jedno- lub wielokolumnowy.
CREATE TABLE t3(c1 integer, c2 text) PARTITION BY HASH (c1); CREATE TABLE t3_a PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 0); CREATE TABLE t3_b PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 1); CREATE TABLE t3_c PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 2);
INSERT INTO t3 SELECT generate_series(1, 1000000); SELECT relname, count(*) FROM t3 JOIN pg_class ON t3.tableoid=pg_class.oid GROUP BY 1; relname | count ---------+------- t3_1 | 333263 t3_2 | 333497 t3_3 | 333240
1. Create parent table:
CREATE TABLE partitioned_payments_hash( id serial not null, created_at timestamp NOT NULL, updated_at timestamp NOT NULL, amount double precision NOT NULL, total double precision NOT NULL, description json DEFAULT NULL, tax double precision NOT NULL, customer_id integer NOT NULL, shipping_address text DEFAULT NULL, status varchar(255) DEFAULT NULL ) PARTITION BY HASH (customer_id);
2. Create partitions:
The goal is to have 12 partitions, so we use the hash value and do a modulo 12. The remainder will identify the partition
CREATE TABLE payments_1 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 0);
CREATE TABLE payments_2 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 1);
CREATE TABLE payments_3 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 2);
CREATE TABLE payments_4 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 3);
CREATE TABLE payments_5 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 4);
CREATE TABLE payments_6 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 5);
CREATE TABLE payments_7 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 6);
CREATE TABLE payments_8 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 7);
CREATE TABLE payments_9 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 8);
CREATE TABLE payments_10 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 9);
CREATE TABLE payments_11 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 10);
CREATE TABLE payments_12 PARTITION OF partitioned_payments_hash FOR VALUES WITH (modulus 12, remainder 11);
3. Create indexes for partition key column in each partition:
CREATE INDEX payments_1_idx ON payments_1 (customer_id); CREATE INDEX payments_2_idx ON payments_2 (customer_id); CREATE INDEX payments_3_idx ON payments_3 (customer_id); CREATE INDEX payments_4_idx ON payments_4 (customer_id); CREATE INDEX payments_5_idx ON payments_5 (customer_id); CREATE INDEX payments_6_idx ON payments_6 (customer_id); CREATE INDEX payments_7_idx ON payments_7 (customer_id); CREATE INDEX payments_8_idx ON payments_8 (customer_id); CREATE INDEX payments_9_idx ON payments_9 (customer_id); CREATE INDEX payments_10_idx ON payments_10 (customer_id); CREATE INDEX payments_11_idx ON payments_11 (customer_id); CREATE INDEX payments_12_idx ON payments_12 (customer_id);
Now you can insert rows:
INSERT INTO partitioned_payments_hash (created_at, updated_at, amount, total, description, tax, customer_id, shipping_address, status) SELECT date('2023-02-' || random_between(1,28)), NOW() - '1 year'::INTERVAL * ROUND(RANDOM() * 100), ROUND(random() * 1000)::double precision , ROUND(random() * 1000)::double precision , '{}', ROUND(random() * 1000)::double precision, ROUND(RANDOM() * 100000)::INTEGER, SUBSTRING(md5(random()::text), 0, 25), SUBSTRING(md5(random()::text), 0, 255) FROM generate_series(1, 10000);
test the query performance:
Bitmap Heap Scan on payments_5 partitioned_payments_hash (cost=4.29..10.12 rows=2 width=109) Recheck Cond: (customer_id = 47209) -> Bitmap Index Scan on payments_5_idx (cost=0.00..4.29 rows=2 width=0) Index Cond: (customer_id = 47209) (4 rows)
#### List with Range Subpartition
first, you create a table that is partitioned by a list; next,
you create child tables that serve as partitions of the parent table; and finally,
you create range subpartitions within those list-partitioned.
DROP TABLE if exists global; CREATE TABLE global ( countryid integer, Region varchar, country varchar, date_cr date, rank integer) PARTITION BY list (country); -- Parent Table --- Child Table with List values CREATE TABLE country_list PARTITION OF global FOR VALUES IN ('China','Europe','USA') PARTITION BY RANGE(date_cr); ----Child table based on list with range CREATE TABLE country_jan_2021 PARTITION OF country_list FOR VALUES FROM ('2021-01-01 00:00:00') TO ('2021-01-31 23:59:59'); INSERT INTO global values(1,'East','USA','2021-01-01 00:00:00',100); INSERT INTO global values(2,'East','China','2021-01-01 00:00:00',90);
#### Range with Hash Subpartition
Drop table if exists global1 cascade; CREATE TABLE global1 ( countryid integer, Region varchar, country varchar, date_cr date, amount integer ) PARTITION BY RANGE (date_cr); -- Parent table partitioned on range \echo Child table based on range and partitioned by hash CREATE TABLE country_dates_2020 PARTITION OF global1 FOR VALUES FROM ('2020-01-01 00:00:00') TO ('2020-12-31 23:59:59') PARTITION BY HASH (amount); CREATE TABLE hash_p0_2020 PARTITION OF country_dates_2020 -- Child table of hash FOR VALUES WITH (modulus 3, remainder 1); INSERT INTO global1 values(1113,'East','China','2020-01-01 00:00:00',3000); INSERT INTO global1 values(11121,'East','japan','2020-01-01 00:00:00',30000); INSERT INTO global1 values(1115,'East','USA','2020-02-01 00:00:00',3000); SET enable_partition_pruning = on; explain SELECT * FROM global1 where date_cr='2020-01-01' AND country='China' and amount > 300; SELECT relname as partition_table, pg_get_expr(relpartbound, oid) as partition_range FROM pg_class WHERE relispartition and relkind = 'r'; SELECT tableoid::regclass, * FROM global1; SELECT * FROM pg_partition_tree('global1');
#### Range with List and Hash Partition
Drop table if exists global1 cascade; CREATE TABLE global1 ( countryid integer, Region varchar, country varchar, date_cr date, amount integer ) PARTITION BY range (date_cr); CREATE TABLE country_dates_2020 PARTITION OF global1 FOR VALUES FROM ('2020-01-01 00:00:00') TO ('2020-12-31 23:59:59'); CREATE TABLE hash_p0_2020 PARTITION OF country_dates_2020 FOR VALUES WITH (modulus 3, remainder 1); CREATE TABLE country_list_2023 PARTITION OF global1 FOR VALUES FROM ('2019-01-01 00:00:00') TO ('2019-12-31 23:59:59') PARTITION BY LIST(country); CREATE INDEX idx_id_country1 on global1(countryid); CREATE INDEX idx_country1 on global1(country); INSERT INTO global1 values (1113,'East','China','2020-01-01 00:00:00',3000); INSERT INTO global1 values(1112,'East','usa','2021-01-01 00:00:00',300); INSERT INTO global1 values(11121,'East','japan','2020-01-01 00:00:00',30000); SET enable_partition_pruning = on; SELECT * FROM global1;
#### List Partition with Range and Hash Subpartition
DROP TABLE IF EXISTS global1 CASCADE; -- Create the main table with RANGE partitioning on date_cr CREATE TABLE global1 ( countryid integer, region varchar, country varchar, date_cr date, amount integer ) PARTITION BY RANGE (date_cr); CREATE TABLE country_dates_2020 PARTITION OF global1 FOR VALUES FROM ('2020-01-01') TO ('2021-01-01'); CREATE TABLE country_dates_2020_p0 PARTITION OF country_dates_2020 FOR VALUES WITH (MODULUS 3, REMAINDER 0) PARTITION BY LIST (country); CREATE TABLE country_dates_2020_p0_list1 PARTITION OF country_dates_2020_p0 FOR VALUES IN ('USA', 'JAPAN', 'CHINA'); CREATE TABLE country_dates_2020_p0_list2 PARTITION OF country_dates_2020_p0 FOR VALUES IN ('UK', 'FRANCE', 'GERMANY'); CREATE TABLE country_dates_2020_p0_list3 PARTITION OF country_dates_2020_p0 FOR VALUES IN ('INDIA', 'BRAZIL', 'AUSTRALIA'); INSERT INTO global1 (countryid, region, country, date_cr, amount) VALUES (0, 'North America', 'USA', '2020-05-15', 1000), (3, 'Asia', 'JAPAN', '2020-06-10', 2000), (6, 'Europe', 'UK', '2020-08-25', 1800); SELECT * FROM global1;
#### Partycjonowanie wielokolumnowe Multicolumn partition
CREATE TABLESPACE ts0 LOCATION '/tablespaces/ts0'; CREATE TABLESPACE ts1 LOCATION '/tablespaces/ts1'; CREATE TABLESPACE ts2 LOCATION '/tablespaces/ts2'; CREATE TABLESPACE ts3 LOCATION '/tablespaces/ts3';
CREATE TABLE t2(c1 integer, c2 text, c3 date not null) PARTITION BY RANGE (c1, c3); CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM (1,'2017-08-10') TO (100, '2017-08-11') TABLESPACE ts1; CREATE TABLE t2_2 PARTITION OF t2 FOR VALUES FROM (100,'2017-08-11') TO (200, '2017-08-12') TABLESPACE ts2; CREATE TABLE t2_0 PARTITION OF t2 FOR VALUES FROM (MINVALUE, MINVALUE) TO (1,'2017-08-10') TABLESPACE ts0; CREATE TABLE t2_3 PARTITION OF t2 FOR VALUES FROM (200,'2017-08-12') TO (MAXVALUE, MAXVALUE) TABLESPACE ts3;
INSERT INTO t2 VALUES (1, 'test', '2017-08-10'); INSERT INTO t2 VALUES (150, 'test2', '2017-08-11');
ANALYZE t2; SELECT relname,relispartition,relkind,reltuples FROM pg_class WHERE relname LIKE 't2%'; relname | relispartition | relkind | reltuples ---------+----------------+---------+----------- t2 | f | p | 0 t2_0 | t | r | 2 t2_1 | t | r | 1 t2_2 | t | r | 1 t2_3 | t | r | 0
* relname: nazwa relacji (tabela główna i partycje)
* relispartition: czy jest partycją (t = true, f = false)
* relkind: typ relacji (p = partitioned table, r = ordinary table)
* reltuples: szacowana liczba wierszy
#### Subpartycje
Partycja jest pelnoprawna tabela ktora mozna dalej partycjonowac
CREATE TABLE objets (id int, statut int, annee int, t text) PARTITION BY LIST (statut) ; CREATE TABLE objets_123 PARTITION OF objets FOR VALUES IN (1, 2, 3) PARTITION BY LIST (annee) ; CREATE TABLE objets_123_2023 PARTITION OF objets_123 FOR VALUES IN (2023) ; CREATE TABLE objets_123_2024 PARTITION OF objets_123 FOR VALUES IN (2024) ; CREATE TABLE objets_45 PARTITION OF objets FOR VALUES IN (4,5) ;
Nie ma obowiązku poddzielania przy użyciu tej samej techniki (list, range, hash...) co podział na wyższym poziomie. Nie ma potrzeby dostarczania pierwszego klucza partycjonowania, aby poddziały były bezpośrednio dostępne.
#### Partycja domyslna
Przy partycjonowaniu typu list lub range - wszystkie dane, które nie mieszczą się w zdefiniowanych partycjach, trafią do partii domyślnej.
CREATE TABLE t1(c1 integer, c2 text) PARTITION BY LIST (c1); CREATE TABLE t1_a PARTITION OF t1 FOR VALUES IN (1, 2, 3); CREATE TABLE t1_b PARTITION OF t1 FOR VALUES IN (4, 5); INSERT INTO t1 VALUES (0); ERROR: no PARTITION OF relation "t1" found for row DETAIL: Partition key of the failing row contains (c1) = (0). INSERT INTO t1 VALUES (6); ERROR: no PARTITION OF relation "t1" found for row DETAIL: Partition key of the failing row contains (c1) = (6).-- partition par défaut
CREATE TABLE t1_defaut PARTITION OF t1 DEFAULT ;-- on réessaie l'insertion
INSERT INTO t1 VALUES (0); INSERT INTO t1 VALUES (6);
SELECT tableoid::regclass, * FROM t1;
tableoid | c1 | c2
------------+----+----
t1_a | 1 |
t1_a | 2 |
t1_b | 5 |
t1_defaut | 0 |
t1_defaut | 6 |
Podział na partycje według haszowania nie może posiadać domyślnej tabeli, ponieważ dane są nieuchronnie kierowane do jednej lub drugiej partycji.
### Maintenance partition
#### List partition
use the ``pg_partition_tree`` function to get the list of partition names in the table:
SELECT p.relname AS partition_name,
pg_get_expr(p.relpartbound, p.oid) AS partition_bound,
pg_get_partkeydef(p.oid) AS partition_key
FROM pg_partition_tree('sales') AS t
JOIN pg_class AS p ON p.oid = t.relid;
#### Attach a new partition
ALTER TABLE … ATTACH PARTITION … FOR VALUES … ;
alter table stock attach partition stock_default default;
* tabela musi istniec wczesniej
* przy dodawaniu jako partycja weryfikacja czy dane spelniaja ograniczenia dla tej partycji chyba ze istnieje na tabeli takie ograniczenie CHECK
* jesli defaultowa partycja zawiera juz dane ktore sa w tej tabeli to dodanie zakonczy sie bledem
* odpiac partycje domyslna
* dodac tabele jako partycje
* usunac z domyslnej dane ktore sa w nowej
* przypiac oczyszczona domyslna
Dodanie tabeli jako partycji do partycjonowanej tabeli jest możliwe, ale wymaga upewnienia się, że warunek partycjonowania jest ważny dla całej dołączonej tabeli oraz że partycja domyślna nie zawiera danych, które powinny znajdować się w tej nowej partycji. To skutkuje pełnym przeszukaniem dołączonej tabeli oraz, jeśli istnieje, partycji domyślnej, co będzie tym bardziej wolne, im są większe. Może to być bardzo kosztowne w obliczeniach, ale największym problemem jest czas trwania blokady na partycjonowanej tabeli podczas tej całej operacji. Dlatego zaleca się dodanie odpowiedniego warunku CHECK przed DOŁĄCZENIEM: czas trwania blokady będzie znacznie krótszy. Jeśli w tej nowej partycji znajdują się już wiersze w partycji domyślnej, należy przeprowadzić dodatkowe operacje, aby je przenieść. To nie jest automatyczne.
CREATE TABLE t1 (c1 integer, filler char (10)) PARTITION BY LIST (c1); CREATE TABLE t1_123 PARTITION OF t1 FOR VALUES IN (1, 2, 3); CREATE TABLE t1_45 PARTITION OF t1 FOR VALUES IN (4, 5); CREATE TABLE t1_default PARTITION OF t1 DEFAULT ;
INSERT INTO t1 SELECT 1+mod(i,5) FROM generate_series (1,5000000) i;
SELECT tableoid::regclass, c1, count(*) FROM t1 GROUP BY 1,2 ORDER BY c1 ;
CREATE TABLE t1_6 (LIKE t1 INCLUDING ALL) ; INSERT INTO t1_6 SELECT 6 FROM generate_series (1,1000000);
\dt+ t1* \timing on -- tabela przy dolaczeniu jest skanowana - co trwa dlugo ALTER TABLE t1 ATTACH PARTITION t1_6 FOR VALUES IN (6) ; -- po dolaczeniu pojawi sie nowe ograniczenie na tabeli \d+ t1_6 -- po odlaczeniu ograniczenie znika ALTER TABLE t1 DETACH PARTITION t1_6 ; \d+ t1_6 -- recznie nakladamy to samo ograniczenie co powyzej-- (co jest długotrwałe, ale nie stanowi blokady na t1) ALTER TABLE t1_6 ADD CONSTRAINT t1_6_ck CHECK(c1 IS NOT NULL AND c1 = 6) ; \d+ t1_6-- l' -- zalaczenie tabeli z odpowiednim CHECK jest prawie natychmiastowe ALTER TABLE t1 ATTACH PARTITION t1_6 FOR VALUES IN (6) ; \timing off
-- wprowadzmy omylkowo wiersze z warosci 7 klucza partycjonujacego ktora trafia do domyslnej partycji INSERT INTO t1 SELECT 7 FROM generate_series (1,100); -- dodanie partycji dla 7 nie powiedzie sie CREATE TABLE t1_7 PARTITION OF t1 FOR VALUES IN (7);
-- Aby to poprawić, w ramach transakcji, -- przenosi się dane z domyślnej partii -- do nowej tabeli, która jest następnie dołączana.
CREATE TABLE t1_7 (LIKE t1 INCLUDING ALL) ; ALTER TABLE t1_7 ADD CONSTRAINT t1_7_ck CHECK(c1 IS NOT NULL AND c1 = 7) ; BEGIN ; INSERT INTO t1_7 SELECT * FROM t1_default WHERE c1=7 ; DELETE FROM t1_default WHERE c1=7 ; ALTER TABLE t1 ATTACH PARTITION t1_7 FOR VALUES IN (7) ; COMMIT ;
CREATE TABLE part_tags_date_05_2020 PARTITION OF part_tags FOR VALUES FROM ('2020-05-01') TO ('2020-05-30');
#### Detach an existing partition
* https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-DETACH-PARTITION
ALTER TABLE … DETACH PARTITION …
Operacja jest szybka. Z opcja 'CONCURRENTLY` (v14+) nie wymaga exclusive lock.
Odłączenie partycji jest znacznie szybsze niż jej podłączenie. W rzeczywistości nie ma potrzeby przeprowadzania weryfikacji danych z partycji. Odłączona partycja staje się zwykłą tabelą. Zachowuje indeksy, ograniczenia itp., które mogła odziedziczyć po oryginalnej tabeli partycjonowanej. Jednak nadal konieczne jest uzyskanie wyłącznego zablokowania na tabeli partycjonowanej, co może zająć czas, jeśli trwają jakieś transakcje. Opcja `CONCURRENTLY` (od PostgreSQL 14) łagodzi problem, mimo kilku ograniczeń, w tym: brak użycia w transakcji, niekompatybilność z obecnością partycji domyślnej, oraz konieczność użycia polecenia `FINALIZE`, jeśli polecenie nie powiodło się lub zostało przerwane.
ALTER TABLE part_tags DETACH PARTITION part_tags_date_05_2020 ;
ALTER TABLE t_data DETACH PARTITION payments_3; ALTER TABLE t_data ATTACH PARTITION payments_3 FOR VALUES WITH (MODULUS 12, REMAINDER 3);
#### Usuniecie partycji
DROP TABLE nom_partition;
Podział będący tabelą, usunięcie tabeli odpowiada usunięciu podziału, a oczywiście danych, które ona zawiera. Nie ma potrzeby wcześniej jej odłączać. Operacja jest prosta i szybka, lecz wymaga wyłącznej blokady. Często dzieli się w czasie, aby skorzystać z tej możliwości podczas usuwania starych danych, a także aby znacznie zmniejszyć czas ich przechowywania, ale także zapisy w dziennikach.
#### Funkcje do zarzadzania partycjami
* psql: `\dP`
* `pg_partition_tree ('logs')`: kompletna lista partycji
* `pg_partition_root ('logs_2019')`: tablica głowna korzen partycji
* `pg_partition_ancestors ('logs_201901')`: tablica/partycja nadrzedna dla danej partycji
-- Tabla partycjonowana CREATE TABLE logs (dreception timestamptz, contenu text) PARTITION BY RANGE(dreception); -- Partition 2018, elle-même partitionnée CREATE TABLE logs_2018 PARTITION OF logs FOR VALUES FROM ('2018-01-01') TO ('2019-01-01') PARTITION BY range(dreception); -- Sous-partitions 2018 CREATE TABLE logs_201801 PARTITION OF logs_2018 FOR VALUES FROM ('2018-01-01') TO ('2018-02-01'); CREATE TABLE logs_201802 PARTITION OF logs_2018 FOR VALUES FROM ('2018-02-01') TO ('2018-03-01'); …-- Idem en 2019 CREATE TABLE logs_2019 PARTITION OF logs FOR VALUES FROM ('2019-01-01') TO ('2020-01-01') PARTITION BY range(dreception); CREATE TABLE logs_201901 PARTITION OF logs_2019 FOR VALUES FROM ('2019-01-01') TO ('2019-02-01');
logs_2018 logs
logs_201901 logs_2019 logs
SELECT * FROM pg_partition_tree('logs'); relid | parentrelid | isleaf | level ---------------+-------------+--------+------- logs | | f | 0 logs_2018 | logs | f | 1 logs_2019 | logs | f | 1 logs_201801 | logs_2018 | t | 2 logs_201802 | logs_2018 | t | 2 logs_201901 | logs_2019 | t | 2
Drzewo partycji gdzie level to poziom zaglebienia w partycjonowaniu
psql: `\d` wyświetli wszystkie tabele, w tym partycje, które mogą szybko zaśmiecić widok.
psql: `\dP` wyświetla tylko tabele i indeksy podzielone na partycje: =# \dP
=# \dP
Liste des relations partitionnées
Schéma | Nom | Propriétaire | Type
--------+--------+--------------+---------------------
public | logs | postgres | table partitionnée
public | t2 | postgres | table partitionnée
public | bigtabl| postgres | index partitionné
Tabela systemowa [`pg_partitioned_table`](https://www.postgresql.org/docs/current/catalog‑pg‑partitioned‑table.html) umożliwia bardziej złożone zapytania. Pole [`pg_class.relpartbound`](https://www.postgresql.org/docs/current/catalog-pg-class.html) zawiera definicje kluczy partycjonowania.
Aby ukryć partycje w niektórych narzędziach, może być interesujące zadeklarowanie partycji w innym schemacie niż główny schemat tabeli. W środowisku "multi-tenant" z wieloma schematami i partycjami o tych samych nazwach, każda w swoim schemacie, ustawienie search_path umożliwia niejawne wybranie partycji, co ułatwia życie deweloperowi lub pozwala "kłamać" aplikacji.
#### Klucz podstawowy i klucz partycjonowania
Partycjonowanie narzuca istotne ograniczenie na modelowanie: klucz podziału musi bezwzględnie być częścią klucza głównego (a także unikalnych ograniczeń i indeksów). W rzeczywistości PostgreSQL nie utrzymuje globalnego indeksu obejmującego wszystkie partycje. Może więc zapewnić unikalności pola tylko w ramach każdej partycji.
W wielu przypadkach nie powinno to stanowić problemu, zwłaszcza jeśli podzielimy na podstawie klucza głównego. W innych przypadkach może być to bardziej uciążliwe. Jeśli prawdziwy klucz główny jest identyfikatorem zarządzanym przez bazę w momencie wstawiania (serial lub IDENTITY), ryzyko jest ograniczone. Jednakże w przypadku identyfikatorów generowanych po stronie aplikacji istnieje ryzyko wprowadzenia duplikatu. W sytuacji, gdy wartości klucza partycjonowania nie są prostą stałą (na przykład daty zamiast jednego roku), problem można złagodzić, dodając ograniczenie unikalności bezpośrednio na każdej partycji, zapewniając unikalność rzeczywistego klucza głównego przynajmniej w obrębie partycji.
Ogólnym rozwiązaniem jest stworzenie innej, niepartycjonowanej tabeli z prawdziwym kluczem podstawowym oraz ograniczeniem do tej tabeli z tabeli partycjonowanej. Koncepcyjnie, jest to równoważne niepartycjonowaniu dużej tabeli, ale 'wyjęciu' danych do podtabeli partycjonowanej z nałożonym ograniczeniem. Czyli zeby wstawic wiersz do tabeli partycjonowanej trzeba wstawic taki klucz do niepartycjonowanej a tam unikalnosc jest pilnowana przez pk. Do partycjonowanej nie da sie wstawic bo ma klucz obcy na pk tabeli niepartycjonowanej
-- nie moza utworzyc w tabeli partycjonowanej pk ktore nie zawiera kolun partycjonujacych CREATE TABLE factures_p (id bigint PRIMARY KEY, d timestamptz, id_client int NOT NULL, montant_c int NOT NULL DEFAULT 0) PARTITION BY RANGE (d); ERROR: unique constraint on partitioned table must include all partitioning columns DÉTAIL : PRIMARY KEY constraint on table "factures_p" lacks column "d" which is part of the partition key.
-- dodajac do pk kolumny partycjonujace unikalnosc zapewniamy tylko w ramach partycji
CREATE TABLE factures_p (id bigint NOT NULL, d timestamptz NOT NULL, id_client int, montant_c int, PRIMARY KEY (id, d) ) PARTITION BY RANGE (d);
CREATE TABLE factures_p_202310 PARTITION OF factures_p FOR VALUES FROM ('2023-10-01') TO ('2023-11-01'); CREATE TABLE factures_p_202311 PARTITION OF factures_p FOR VALUES FROM ('2023-11-01') TO ('2023-12-01');
ALTER TABLE factures_p_202310 ADD CONSTRAINT factures_p_202310_uq UNIQUE (id); -- mozna unikalnosc dodac w partycji ALTER TABLE factures_p_202311 ADD CONSTRAINT factures_p_202311_uq UNIQUE (id); -- Dodanie paru wierszy z id 1-5 do tabeli partycjonowanej INSERT INTO factures_p (id, d, id_client) SELECT i, '2023-10-26'::timestamptz+i*interval '2 days', 42 FROM generate_series (1,5) i; BEGIN ; -- Ten duplikat id=3 jest akceptowany bo dt= 2023-11-01 a ten wyzej mial 2023-10-26 czyli trafia do osobnych partycji INSERT INTO factures_p (id, d, id_client) SELECT 3, '2023-11-01'::timestamptz-interval '1s', 42 ; -- Weryfikacja duubli id=3 SELECT tableoid::regclass, id, d FROM factures_p ORDER BY id ; ROLLBACK ; -- Utworzenie tej tabeli pozwala zachowac unikalnosc we wszystkich partycjach CREATE TABLE factures_ref ( id bigint NOT NULL PRIMARY KEY, d timestamptz NOT NULL, UNIQUE (id,d) -- niezbedne ograniczenie unikalnosci id ktore to zapewnia ) ; INSERT INTO factures_ref SELECT id,d FROM factures_p ; -- utworzenie klucza obcego w tabeli partycjonowanej do tabeli niepartycjonowanej ALTER TABLE factures_p ADD CONSTRAINT factures_p_id_fk FOREIGN KEY (id, d) REFERENCES factures_ref (id,d); -- Kazda nowa wartosc id wprowadzamy do obu tabel -- Duplikat zostanie prawidłowo odrzucony : WITH ins AS ( INSERT INTO factures_p (id, d, id_client) SELECT 3, '2023-11-01'::timestamptz-interval '1s', 42 RETURNING id,d ) INSERT INTO factures_ref SELECT id, d FROM ins ;
ERROR: duplicate key value violates unique constraint "factures_ref_pkey" DÉTAIL : Key (id)=(3) already exists.
#### Indeksowanie
Indeksy są propagowane z tabeli macierzystej do partycji: każdy indeks utworzony w tabeli partycjonowanej zostanie automatycznie utworzony w istniejących partycjach. Każda nowa partycja będzie miała indeksy tabeli partycjonowanej. Usunięcie indeksu odbywa się na tabeli partycjonowanej i dotyczy wszystkich partycji. Nie jest możliwe usunięcie takiego indeksu tylko z jednej partycji.
Ręczne zarządzanie indeksami na niektórych partycjach jest możliwe. Na przykład, można nie mieć potrzeby tworzenia niektórych indeksów tylko na partycjach z danymi bieżącymi, a nie tworzyć ich na partycjach z danymi archiwalnymi.
Klucz podstawowy(PK) lub unikalny(unique) może istnieć na podzielonej tabeli (ale musi zawierać wszystkie kolumny klucza partycjonowania); oraz klucz obcy z tabeli partycjonowanej do tabeli normalnej.
Od PostgreSQL 12 możliwe jest tworzenie klucza obcego do partycjonowanej tabeli w taki sam sposób jak między dwiema normalnymi tabelami. Na przykład, jeśli tabele sprzedaży i linie_sprzedaży są obie partycjonowane.
ALTER TABLE lignes_ventes ADD CONSTRAINT lignes_ventes_ventes_fk FOREIGN KEY (vente_id) REFERENCES ventes (vente_id) ;
Wersje 10 i 11 mają ograniczenia dotyczące tych funkcji, które często można obejść, tworząc indeksy i ograniczenia ręcznie na każdej partycji.
#### archive data
pg_dump -c -Fc \ --table rideshare.trip_positions_202309 \ $PGSLICE_URL > trip_positions_202309.dump
* delete table
drop table rideshare.trip_positions_202309;
#### Attach an already existing table to the parent table
ALTER TABLE part_tags ATTACH PARTITION part_tags_already_exists FOR VALUES FROM ('1970-01-01') TO ('2019-12-31');
#### Partition pruning
* https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITION-PRUNING
* https://www.postgresql.fastware.com/postgresql-insider-prf-prt-mec
It's use only in declarative partitioning. Set parameter ``partition pruning`` to ``enable`` (default). If WHERE clause contain partition key optimizer using table partition bounds to remove partition from scaning. This bounds exist only in declarative partition.
select * from pg_setting where name = 'enable_partition_pruning';
enable_partition_pruning = off/on
Partition pruning can be performed not only during the **planning** of a
given query, but also during its **execution**. This is useful as it can
allow more partitions to be pruned when clauses contain expressions
whose values are not known at query planning time; for example,
parameters defined in a PREPARE statement, using a value obtained from
a subquery or using a parameterized value on the inner side of a
nested loop join.
W przypadku, gdy klucz partycjonowania zależy od wyniku obliczenia, podzapytania lub złączenia, PostgreSQL przewiduje plan dotyczący wszystkich partycji, ale podczas wykonywania odrzuci wywołania partycji, które nie są dotknięte. Poniżej tylko pgbench_accounts_8 jest zapytane (i w zależności od powtórzenia zapytania może to być inna partycja):
EXPLAIN (ANALYZE,COSTS OFF) SELECT * FROM pgbench_accounts WHERE aid = (SELECT (random()*1000000)::int ) ;
Poważnym ograniczeniem partycjonowania jest to, że czas planowania szybko rośnie wraz z liczbą partycji, nawet tych małych. Rzeczywiście, każda partycja dodaje swoje statystyki i często kilka indeksów do tabel systemowych. Na przykład, w najbardziej niekorzystnym przypadku sesji, która się uruchamia.
-- Tabela niepartycjonowana EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT * FROM pgbench_accounts WHERE aid = 123 LIMIT 1 ;
Limit (actual time=0.021..0.022 rows=1 loops=1) Buffers: shared hit=4-> Index Scan using pgbench_accounts_pkey on pgbench_accounts (actual time=0.021..0.021 rows=1 loops=1) Index Cond: (aid = 123) Buffers: shared hit=4 Planning: Buffers: shared hit=70 Planning Time: 0.358 ms Execution Time: 0.063 ms -- tabela 100 partycji EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT * FROM pgbench_accounts WHERE aid = 123 LIMIT 1 ;
Limit (actual time=0.015..0.016 rows=1 loops=1) Buffers: shared hit=3-> Index Scan using pgbench_accounts_1_pkey on pgbench_accounts_1 pgbench_accounts (actual time=0.015..0.015 rows=1 loops=1) Index Cond: (aid = 123) Buffers: shared hit=3 Planning: Buffers: shared hit=423 Planning Time: 1.030 ms Execution Time: 0.061 ms
W sekcji planowania ilosc blokow do zbuforowania Buffers: shared hit=423 dla partycjonowanej i Buffers: shared hit=70 dla niepartycjonowanej wiaze sie z systemowymi tabelami i statystykami ktore trzeba zapytac przy planowaniu.
Ogólnie przyjmuje się, że nie należy przekraczać 100 partycji, jeśli nie chce się aby czas planowania bardzo wpływal na kótkie tranzakcje. Jednak najnowsze wersje PostgreSQL są w tym zakresie lepsze.
Ten problem z planowaniem jest mniej uciążliwy dla długich zapytań (analitycznych). Aby obejść to ograniczenie, można bezpośrednio używać partycji, jeśli programista (lub generator kodu...) łatwo może znaleźć ich nazwę, oprócz zawsze dostarczania klucza. Bezpośrednie zapytanie do partycji jest bowiem tak samo szybkie w planowaniu jak zapytanie do monolitycznej tabeli.
EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT * FROM pgbench_accounts_1 WHERE aid = 123 LIMIT 1 ;
Limit (actual time=0.006..0.007 rows=1 loops=1) Buffers: shared hit=3-> Index Scan using pgbench_accounts_1_pkey on pgbench_accounts_1 (actual time=0.006..0.006 rows=1 loops=1) Index Cond: (aid = 123) Buffers: shared hit=3 Planning Time: 0.046 ms Execution Time: 0.016 ms
Bezpośrednie korzystanie z partycji jest szczególnie oszczędne, jeśli ich liczba jest duża, ale tracimy wtedy aspekt „przezroczystości” partycjonowania, co zwiększa złożoność kodu aplikacji.
W bardziej skomplikowanych przypadkach, szczególnie w przypadku łączenia z partycjonowanymi tabelami, czas planowania może eksplodować. Na przykład, w przypadku poniższego zapytania, gdzie partycjonowana tabela jest łączona sama z sobą, plan na tabeli niepartycjonowanej, gorący cache sesji, zwraca
EXPLAIN (BUFFERS, COSTS OFF, SUMMARY ON) SELECT * FROM pgbench_accounts a INNER JOIN pgbench_accounts b USING (aid) WHERE a.bid = 55 ;
Gather Workers Planned: 4-> Nested Loop-> Parallel Seq Scan on pgbench_accounts a Filter: (bid = 55)-> Index Scan using pgbench_accounts_pkey on pgbench_accounts b Index Cond: (aid = a.aid) Planning: Buffers: shared hit=16 Planning Time: 0.168 ms
Z setką partytur czas planowania jest tutaj pomnożony przez 50:
Gather Workers Planned: 4-> Parallel Hash Join Hash Cond: (b.aid = a.aid)-> Parallel Append-> Parallel Seq Scan on pgbench_accounts_1 b_1 -> Parallel Seq Scan on pgbench_accounts_2 b_2 …-> Parallel Seq Scan on pgbench_accounts_99 b_99-> Parallel Seq Scan on pgbench_accounts_100 b_100-> Parallel Hash-> Parallel Append-> Parallel Seq Scan on pgbench_accounts_1 a_1 Filter: (bid = 55)-> Parallel Seq Scan on pgbench_accounts_2 a_2 Filter: (bid = 55) …-> Parallel Seq Scan on pgbench_accounts_99 a_99 Filter: (bid = 55)-> Parallel Seq Scan on pgbench_accounts_100 a_100 Filter: (bid = 55) Planning Time: 5.513 ms
Ten plan zapytania nie jest idealny: pobiera całą tabelę pgbench_accounts i łączy ją z całą drugą tabelą. Byłoby inteligentniej wykonać łączenie partycja po partycji, skoro klucz łączenia jest jednocześnie kluczem partycjonowania.
Aby PostgreSQL próbował wykonać takie łączenie między odpowiadającymi sobie partycjami, należy włączyć specjalny parametr:
SET enable_partitionwise_join TO on;
Po jego aktywacji, łączenia będą wykonywane między odpowiednimi partycjami, a nie na całych tabelach.
Gather Workers Planned: 4 -> Parallel Append -> Parallel Hash Join Hash Cond: (a_55.aid = b_55.aid) -> Parallel Seq Scan on pgbench_accounts_55 a_55 Filter: (bid = 55)-> Parallel Hash-> Parallel Seq Scan on pgbench_accounts_55 b_55 …
-> Nested Loop-> Parallel Seq Scan on pgbench_accounts_100 a_100 Filter: (bid = 55)-> Index Scan using pgbench_accounts_100_pkey on pgbench_accounts_100 b_100 Planning: Index Cond: (aid = a_100.aid) Buffers: shared hit=1200 Planning Time: 12.449 ms
Dla wielu partycji gdy trzeba je laczyc ze soba czes wykonania zapytan bedzie mniejszy ale wzrosnie czas planowania.
Inna parametru musi być aktywowany, jeśli należy przeprowadzić agregacje na wielu partycjach.
SET enable_partitionwise_aggregate TO on;
`enable_partitionwise_aggregate` i `enable_partitionwise_join` są domyślnie wyłączone ze względu na ich koszt planowania w przypadku małych zapytań, ale ich włączenie często jest opłacalne. Można to ustalać na poziomie zapytania za pomocą `SET`.
### Default partition
use a default partition where all the values that are not reflected in the mapping of the child tables will be inserted.
CREATE TABLE part_tags_default PARTITION OF part_tags default;
### Indexing
* https://pgdash.io/blog/partition-postgres-11.html
Postgres 11+ If define the indexes for the parent table. These indexes will automatically be propagated to child tablesa.
* https://stackoverflow.com/a/70958260
When creating indexes on partitioned tables, there are caveats to be aware of. Creating indexes CONCURRENTLY on the parent table is not supported as of PostgreSQL 16. To work around this limitation, use CREATE INDEX CONCURRENTLY to create the index manually on each child partition. Once that’s done, run CREATE INDEX on the parent.
Because the parent doesn’t contain data rows and because the indexes were created on all children prior to this statement, this technique avoids locking on children for index creation and minimizes the lock duration when creating the corresponding index on the parent.
### Moving row between partitions
* https://pgdash.io/blog/partition-postgres-11.html
PostgreSQL 11+ is able to move a row from one partition to the other. However, keep in mind that moving data between partitions may not be the best idea in general.
When you create an index on the parent table, indexes are created on partitioned tables
postgres=# CREATE INDEX idx_emp_sales on employee(sale_amount); CREATE INDEX postgres=# \di List of relations Schema | Name | Type | Owner | Table --------+------------------------------+-------------------+----------+-------------- public | emp_feb_2023_sale_amount_idx | index | postgres | emp_feb_2023 public | emp_jan_2023_sale_amount_idx | index | postgres | emp_jan_2023 public | emp_mar_2023_sale_amount_idx | index | postgres | emp_mar_2023 (9 rows)
### Default partition
CREATE TABLE p_def PARTITION OF data DEFAULT;
All the data that doesn’t fit anywhere will end up in this default partition, which ensures that creating the right partition can never be forgotten.
### Table partition information
The function `pg_partition_tree` displays the parent and child partition levels in the database.
postgres=# select * from pg_partition_tree('log'); relid | parentrelid | isleaf | level -------------+-------------+--------+------- log | | f | 0 log_q1_2024 | log | t | 1 log_q2_2024 | log | t | 1
The partition range details are obtained from the `pg_get_expr` function.
SELECT relname as partition_table, pg_get_expr(relpartbound, oid) as partition_range FROM pg_class WHERE relispartition AND relkind = 'r'; partition_table | partition_range -----------------+-------------------------------------------------- log_q2_2024 | FOR VALUES FROM ('2024-04-01') TO ('2024-06-30') log_q1_2024 | FOR VALUES FROM ('2024-01-01') TO ('2024-03-31') The partition table object id(oid) is converted to tablename by the ::regclass typecase operator. SELECT tableoid::regclass, * FROM log; tableoid | log_id | logtext | log_date -------------+--------+--------------------------------+------------ log_q1_2024 | 200 | test | 2024-03-20 log_q2_2024 | 100 | test | 2024-06-21
### Management partitions
Operacje administracyjne zyskują znacznie na możliwości podzielenia operacji na tyle kroków, ile jest partycji. Dane 'zimne' mogą być przenoszone do innej przestrzeni tabel na tańsze dyski, partycja po partycji, co jest niemożliwe w przypadku monolitycznej tabeli:
ALTER TABLE pgbench_accounts_8 SET TABLESPACE hdd
Autovacuum i autoanalyze działają normalnie i niezależnie na każdej partycji, tak jak w przypadku klasycznych tabel. Dzięki temu mogą być wyzwalane częściej na aktywnych partycjach. W porównaniu do dużej monolitycznej tabeli, rzadziej zachodzi potrzeba regulacji autovacuum. Polecenia `ANALYZE` i `VACUUM` mogą być wykonywane na jednej partycji, ale również na tabeli partycjonowanej, w takim przypadku polecenie zostanie przekazane kaskadowo na partycje (opcja `VERBOSE` pozwala to sprawdzić). Statystyki będą obliczane według partycji, więc będą bardziej dokładne. Odbudowa tabeli partycjonowanej za pomocą `VACUUM FULL` zazwyczaj będzie się odbywać partycja po partycji. Partycjonowanie umożliwia zatem rozwiązanie przypadków, w których blokada na tabeli monolitycznej byłaby zbyt długa lub całkowita przestrzeń dyskowa byłaby niewystarczająca.
Należy jednak zauważyć te specyfikacje dotyczące tabel partycjonowanych.
* Począwszy od PostgreSQL 14, REINDEX na tabeli partycjonowanej automatycznie reindeksuje wszystkie partycje. W poprzednich wersjach trzeba reindeksować partycję po partycji.
* Autovacuum nie tworzy statystyk dla całej tabeli partycjonowanej automatycznie, lecz jedynie partycja po partycji. Aby uzyskać statystyki dla całej jeśli tabela jest partycjonowana, trzeba to wykonać ręcznie
ANALYZE table_partitionnée ;
Dzięki partycjonowaniu eksport za pomocą `pg_dump --jobs` staje się wydajny, ponieważ wiele partycji może być zapisywanych jednocześnie.
pg_dump opcje do zarządzania eksportem tabel partycjonowanych
* `--load-via-partition-rootp` pozwala generować polecenia COPY, celując w tabelę główną, a nie w partycję. Może to być przydatne do przywracania danych w bazie, gdzie tabela jest partycjonowana osobno.
* Począwszy od PostgreSQL 16, eksportowanie tylko jednej tabeli partycjonowanej odbywa się za pomocą `--table-and-children` (a nie `--table/-t`, które dotyczyłyby tylko tabeli macierzystej). Wykluczanie tabel partycjonowanych odbywa się za pomocą `--exclude-table-and-children` (a nie `--exclude-table/-T`). Aby wykluczyć tylko dane z tabeli partycjonowanej, zachowując jej strukturę, należy użyć `--exclude-table-data-and-children`. Te trzy opcje akceptują wzór (na przykład: pgbench_accounts_*) i mogą być powtarzane w poleceniu.
Ograniczenia:
* Polecenie CLUSTER, aby przekształcić tabelę w kolejności danej indeksu, działa dla tabel partycjonowanych dopiero od PostgreSQL 15. Może jednak być wykonywane ręcznie, tabela po tabeli.
* Możliwe jest dołączenie jako partycji zdalnych tabel, zazwyczaj deklarowanych przy użyciu `postgres_fdw`; jednak propagacja indeksów nie będzie działać na tych tabelach. Należy je stworzyć ręcznie na zdalnych instancjach. (Dodatkowe ograniczenie w wersji 10: zdalne partycje są dostępne tylko do odczytu, jeśli są uzyskiwane przez tabelę macierzystą.
* Triggery wierszy nie propagują się w wersji 10. W wersji 11 możemy tworzyć triggery `AFTER UPDATE...` `FOR EACH ROW`, ale triggery `BEFORE UPDATE...` `FOR EACH ROW` nadal nie mogą być tworzone na tabeli głównej. Można je nadal tworzyć partycja po partycji. Od wersji 13 możliwe są wyzwalacze BEFORE UPDATE … FOR EACH ROW, ale nie pozwalają na modyfikację docelowej partycji.
* WPg 10 nie pozwala na aktualizację (UPDATE) wiersza, w której klucz partycjonowania jest zmieniany w taki sposób, że wiersz musi zmienić partycję. Należy wykonać DELETE i INSERT w tym miejscu.
Aby uzywac partycjonowania, zaleca się używanie jak najnowszej wersji, co najmniej PostgreSQL 13.
#### Extensions
* https://github.com/ghdrako/doc_snipets/wiki/postgres-declare-partitioning-pg_partman-pg_slice
Rozszerzenie `pg_partman` firmy Crunchy Data jest uzupełnieniem dla systemów partycjonowania PostgreSQL. Pojawiło się początkowo, aby zautomatyzować partycjonowanie przez dziedziczenie. Może być przydatne przy deklaratywnym partycjonowaniu, aby uprościć utrzymanie partycjonowania na osi czasu lub według wartości (przez zakres).
#### Migrating data to partitioned table
* https://github.com/ankane/pgslice
* pg_slice - support append-only tables
BEGIN ;
CREATE TABLE "rideshare"."trip_positions_intermediate"
( LIKE "rideshare"."trip_positions"
INCLUDING DEFAULTS
INCLUDING CONSTRAINTS
INCLUDING STORAGE
INCLUDING COMMENTS
INCLUDING STATISTICS
INCLUDING GENERATED
INCLUDING COMPRESSION)
PARTITION BY RANGE ("created_at");
ALTER TABLE "rideshare"."trip_positions_intermediate"
ADD FOREIGN KEY (trip_id) REFERENCES trips(id);
COMMENT ON TABLE "rideshare"."trip_positions_intermediate" IS 'column:created_at,period:month,cast:date,version:3' ;
commit;
Note the intermediate table does not have a primary key defined on it.
\d+ trip_positions_intermediate
INSERT INTO "rideshare"."trip_positions_intermediate"
("id", "position", "trip_id", "created_at", "updated_at")
SELECT "id", "position", "trip_id", "created_at", "updated_at"
FROM "rideshare"."trip_positions"
WHERE "id" > 1 AND "id" <= 10000
AND "created_at" >= '2023-06-01' :: date
AND "created_at" < '2024-01-01' :: date
ALTER TABLE trip_positions ADD PRIMARY KEY (id, created_at);
#### Foreign key
CREATE TABLE event ( id integer NOT NULL GENERATED ALWAYS AS IDENTITY, type_id int,-- REFERENCES event_type(id), occurred_at date ) PARTITION BY RANGE (occurred_at);
CREATE TABLE event_2025_m6 PARTITION OF event FOR VALUES FROM ('2023-06-01') TO ('2025-07-01'); --- Creation of Child table CREATE TABLE event_2023_m7 PARTITION OF event FOR VALUES FROM ('2025-07-01') TO ('2025-08-01'); CREATE TABLE event_default PARTITION OF event DEFAULT ;
ALTER TABLE event ADD CONSTRAINT fk_event FOREIGN KEY (event_id) REFERENCES event_type(id);
W druga strone nie zadziala bo tabela partycjonowana nie ma pk
ALTER TABLE public.event_type ADD CONSTRAINT "FK_1" FOREIGN KEY (id) REFERENCES public.event(type_id); ERROR: there is no unique constraint matching given keys for referenced table "event"
Dodanie pk nic nie da bo w pk oprocz kolumny id musza sie znalezc wszystkie kolumny po ktorych partycjonujemy wiec nadal nie zapewni unikalnosci.
Ewentualnie juz na samych partycjach zalozyc pk tylko na polu id i na kazdej partycji zalozyc fk.
CREATE TABLE items ( item_id integer PRIMARY KEY, description text NOT NULL ) PARTITION BY hash (item_id); CREATE TABLE items_0 PARTITION OF items FOR VALUES WITH (modulus 3, remainder 0); CREATE TABLE items_1 PARTITION OF items FOR VALUES WITH (modulus 3, remainder 1); CREATE TABLE items_2 PARTITION OF items FOR VALUES WITH (modulus 3, remainder 2);
CREATE TABLE warehouses (warehouse_id integer primary key, location text not null);
CREATE TABLE stock ( item_id integer not null REFERENCES items, warehouse_id integer not null REFERENCES warehouses, amount int not null ) partition by hash (warehouse_id); CREATE TABLE stock_0 PARTITION OF stock FOR VALUES WITH (modulus 5, remainder 0); CREATE TABLE stock_1 PARTITION OF stock FOR VALUES WITH (modulus 5, remainder 1); CREATE TABLE stock_2 PARTITION OF stock FOR VALUES WITH (modulus 5, remainder 2); CREATE TABLE stock_3 PARTITION OF stock FOR VALUES WITH (modulus 5, remainder 3); CREATE TABLE stock_4 PARTITION OF stock FOR VALUES WITH (modulus 5, remainder 4);
#### partition-wise
* https://www.postgresql.fastware.com/postgresql-insider-prf-prt-mec
##### Partition-wise JOIN
events (created_at, user_id, ...) metrics (created_at, user_id, ...)
Obie partycjonowane RANGE po created_at w tych samych granicach.
Partition-wise join zadziała, gdy obie tabele mają:
* ten sam typ partycjonowania
* te same granice partycji
https://www.postgresql.fastware.com/hs-fs/hubfs/Images/PI/img-pi-dgm-prf-prt-mec-join-partition-viewport-wide.png?width=624&name=img-pi-dgm-prf-prt-mec-join-partition-viewport-wide.png
Przykład:
SELECT e.user_id, m.value FROM events e JOIN metrics m ON e.user_id = m.user_id AND e.created_at = m.created_at WHERE e.created_at >= '2025-01-01' AND e.created_at < '2025-02-01';
Postgres może:
* zjoinować events_2025_01 tylko z metrics_2025_01
* zamiast robić jeden wielki join.
Jeśli jedna tabela jest partycjonowana miesięcznie, druga tygodniowo, nie będzie partition-wise join.
In query plan instead of the Append nodes coming first and then the join sitting on top of them, you will see that the join essentially moves underneath the Append nodes and runs multiple times. Further, Postgres may actually choose to use different join techniques between different partitions, depending on their size and their data distribution.
It's important to note that in order to be able to use partition-wise join, the join condition needs to include all the partition keys.
##### Agregacje partition-wise
SELECT date_trunc('month', created_at), COUNT(*) FROM events WHERE created_at >= '2024-01-01' GROUP BY 1;
Postgres może liczyć COUNT(*) osobno na każdej partycji, a potem tylko sumować wyniki.
To działa najlepiej, gdy:
* partycjonujesz po czasie,
* a agregujesz też po czasie.
##### Parameters https://www.postgresql.org/docs/15/runtime-config-query.html#GUC-ENABLE-PARTITION-PRUNING
* `enable_partition_pruning` (on) - Postgres planner is able to determine that certain partitions do not match those expressions, and so it can either during planning time or right at the start of execution time, eliminate particular partitions that are just not matched.
* `enable_partitionwise_join` (off) - Postgres planner consider joins between individual partitions - do a partition per partition join
* `enable_partitionwise_aggregate`(off) - Postgres planner consider aggregates on individual partitions before they're grouped together in an Append node.
Because this is expensive and uses significantly more CPU and memory during planning, the default is off. This applies to both of them.
Planujac proces postgrs nie wie czy ta optymalizacja przyniesie korzysc bo planowanie sie wydluzy dlatego jest defaultowo wylaczone. Parametry mozna właczać na poziomie sesji
set enable_partitionwise_aggregate to on;
##### Automatyzacja zarzadzania partycjami
* cron/pg_cron + skrypt pg psql
CREATE OR REPLACE FUNCTION create_next_month_partition() RETURNS void AS $$ DECLARE next_month date; table_name text; BEGIN next_month := date_trunc('month', now()) + interval '1 month';
table_name := format( 'events_%s', to_char(next_month, 'YYYY_MM') );
EXECUTE format( 'CREATE TABLE IF NOT EXISTS %I PARTITION OF events FOR VALUES FROM (%L) TO (%L)', table_name, next_month, next_month + interval '1 month' ); END; $$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION drop_old_partitions() RETURNS void AS $$ DECLARE rec record; cutoff date := date_trunc('month', now()) - interval '12 months'; BEGIN FOR rec IN SELECT inhrelid::regclass AS partition FROM pg_inherits WHERE inhparent = 'events'::regclass LOOP IF rec.partition::text < format('events_%s', to_char(cutoff, 'YYYY_MM')) THEN EXECUTE format('ALTER TABLE events DETACH PARTITION %I', rec.partition); EXECUTE format('DROP TABLE %I', rec.partition); END IF; END LOOP; END; $$ LANGUAGE plpgsql
0 3 * * 1 psql -d yourdb -c "SELECT create_next_month_partition();" 0 4 1 * * psql -d yourdb -c "SELECT drop_old_partitions();"
* pg_partman
CREATE EXTENSION pg_partman;
SELECT partman.create_parent( p_parent_table := 'public.events', p_control := 'created_at', p_type := 'range', p_interval := '1 month', p_premake := 3 );
* tworzy nowe partycje z wyprzedzeniem (np. 3 miesiące do przodu p_premake),
* może usuwać stare partycje,
* obsługuje: RANGE LIST lub nawet bardziej złożone scenariusze.