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
PostgreSQL does not support join between partition-leafs and non-partition tables before merging.
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.
- https://www.percona.com/blog/postgresql-vacuuming-to-optimize-database-performance-and-reclaim-space
- 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
, andenable_partitionwise_aggregate=on
either at the session or system level, you can leverage the parallel processing capabilities of PostgreSQL.
- Sanity check
DROP TABLE IF EXISTS part_tags cascade;
- 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.
- 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);
CREATE INDEX part_tags_tag on part_tags using GIN (tag gin_trgm_ops);
\d part_tags;
\d part_tags_level_0;
select * from part_tags;
select * from part_tags_level_0;
select * from part_tags_level_1;
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_<year>_<month>
PARTITION OF erp.payments_p
FOR VALUES FROM (<first day of month>) TO (<last day of month>)
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
- 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);
- 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);
- 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;
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
- 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);
- 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);
- 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:
EXPLAIN SELECT * FROM partitioned_payments_hash WHERE customer_id = 47209;
QUERY PLAN
-----------------------------------------------------------------------------------------------
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)
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);
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');
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;
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;
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
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.
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.
use the pg_partition_tree
function to get the list of partition names in the table:
# Verify the partitioned tab
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;
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');
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);
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.
- 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');
SELECT pg_partition_root('logs_2019');
pg_partition_root
------------------
logs
SELECT pg_partition_root('logs_201901');
pg_partition_root
------------------
logs
SELECT pg_partition_ancestors('logs_2018');
pg_partition_ancestors
-----------------------
logs_2018
logs
SELECT pg_partition_ancestors('logs_201901');
pg_partition_ancestors
-----------------------
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
umożliwia bardziej złożone zapytania. Pole pg_class.relpartbound
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.
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.
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.
pg_dump -c -Fc \ --table rideshare.trip_positions_202309 \ $PGSLICE_URL > trip_positions_202309.dump
- delete table
drop table rideshare.trip_positions_202309;
ALTER TABLE part_tags ATTACH PARTITION part_tags_already_exists FOR
VALUES FROM ('1970-01-01') TO ('2019-12-31');
- 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 ;
QUERY PLAN
--------------------------------------------------------------------------------
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 ;
QUERY PLAN
--------------------------------------------------------------------------------
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 ;
QUERY PLAN
--------------------------------------------------------------------------------
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 ;
QUERY PLAN
--------------------------------------------------------------------------------
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
.
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;
-
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.
- 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)
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.
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
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 triggeryBEFORE 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.
-
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).
- 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);
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);