Blog JSystems - uwalniamy wiedzę!
Blog JSystems - uwalniamy wiedzę!
Partycjonowanie tabel to jeden z najskuteczniejszych sposobów na przyspieszenie zapytań i zarządzanie rozrastającymi się danymi. Logi zdarzeń, dane IoT, tabele zamówień z wieloma milionami wierszy — bez partycjonowania pełne skany i indeksy stają się wąskim gardłem.
Problem polega na tym, że natywne partycjonowanie PostgreSQL (PARTITION BY RANGE) jest świetne strukturalnie, ale wymaga ręcznego tworzenia każdej partycji. Przyszłe partycje nie powstaną same z siebie. Stare nie usuną się automatycznie. Wchodzi pg_partman.
pg_partman (Partition Manager) to rozszerzenie PostgreSQL, które automatyzuje cały cykl życia partycji:
part_config ze stanem wszystkich partycjonowanych tabelpg_partman współpracuje z deklaratywnym partycjonowaniem PostgreSQL (dostępnym od wersji 10). Obsługuje PostgreSQL 13–17 i jest aktywnie rozwijany na GitHubie.
# PostgreSQL 16
sudo apt install postgresql-16-partman
# PostgreSQL 17
sudo apt install postgresql-17-partman
sudo dnf install pg_partman_17
pg_partman wymaga własnego schematu. Dobrą praktyką jest dedykowany schemat partman:
-- Utwórz dedykowany schemat
CREATE SCHEMA partman;
-- Zainstaluj rozszerzenie w tym schemacie
CREATE EXTENSION pg_partman SCHEMA partman;
-- Weryfikacja
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'pg_partman';
-- extname | extversion
-- ------------+------------
-- pg_partman | 5.2.4
Najpopularniejszy przypadek użycia: tabela zdarzeń lub logów partycjonowana po dacie. Zamiast jednej ogromnej tabeli z indeksem na created_at, mamy osobną partycję na każdy miesiąc (lub dzień, rok).
-- Tabela główna: musi mieć PARTITION BY RANGE
CREATE TABLE app_events (
id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id INTEGER,
event_type VARCHAR(64),
payload JSONB
) PARTITION BY RANGE (created_at);
-- Indeks na tabeli głównej jest automatycznie kopiowany na partycje
CREATE INDEX ON app_events (user_id, created_at);
CREATE INDEX ON app_events (event_type, created_at);
SELECT partman.create_parent(
p_parent_table => 'public.app_events',
p_control => 'created_at', -- kolumna partycjonująca
p_interval => '1 month', -- wielkość partycji
p_premake => 3 -- ile partycji naprzód tworzyć
);
-- Wynik: t (true = sukces)
Po wywołaniu create_parent pg_partman automatycznie tworzy partycje: bieżący miesiąc + 3 naprzód. Sprawdź efekt:
-- Lista partycji tabeli app_events
SELECT
child.relname AS partycja,
pg_get_expr(child.relpartbound, child.oid) AS zakres
FROM pg_inherits i
JOIN pg_class parent ON i.inhparent = parent.oid
JOIN pg_class child ON i.inhrelid = child.oid
WHERE parent.relname = 'app_events'
ORDER BY child.relname;
-- partycja | zakres
-- --------------------------+----------------------------------------------
-- app_events_p20260601 | FOR VALUES FROM ('2026-06-01') TO ('2026-07-01')
-- app_events_p20260701 | FOR VALUES FROM ('2026-07-01') TO ('2026-08-01')
-- app_events_p20260801 | FOR VALUES FROM ('2026-08-01') TO ('2026-09-01')
-- app_events_p20260901 | FOR VALUES FROM ('2026-09-01') TO ('2026-10-01')
-- app_events_default | DEFAULT
_default, która przechowuje wiersze nienależące do żadnej istniejącej partycji. Gdy tworzysz nową partycję, dane z DEFAULT są do niej migrowane automatycznie.
| Interwał | Przykład nazwy partycji | Kiedy stosować |
|---|---|---|
'1 hour' | events_p2026060914 | Logi o bardzo wysokim wolumenie (miliony/dzień) |
'1 day' | events_p20260609 | Logi aplikacji, dane IoT |
'1 week' | events_p2026w23 | Dane transakcyjne, sesje |
'1 month' | events_p20260601 | Zamówienia, faktury — najczęstszy wybór |
'3 months' | events_p20260401 | Dane archiwalne, rzadkie zapytania |
'1 year' | events_p20260101 | Długoterminowe dane historyczne |
Retencja to jedna z najważniejszych cech pg_partman. Zamiast pisać cron z DROP TABLE, konfigurujesz politykę raz:
-- Ustaw retencję: zachowaj dane z ostatnich 6 miesięcy
UPDATE partman.part_config
SET
retention = '6 months', -- usuń partycje starsze niż 6 mies.
retention_keep_table = false, -- DROP TABLE (nie tylko DETACH)
retention_keep_index = false, -- usuń też indeksy
infinite_time_partitions = true -- zezwól na dane z dalekiej przyszłości
WHERE parent_table = 'public.app_events';
-- Alternatywnie: tylko DETACH bez DROP (bezpieczniej na start)
UPDATE partman.part_config
SET
retention = '6 months',
retention_keep_table = true -- tabela zostaje, tylko odłączona od parenta
WHERE parent_table = 'public.app_events';
retention_keep_table = false trwale usuwa dane. Zanim włączysz DROP, przetestuj z retention_keep_table = true (DETACH) i sprawdź, czy odłączone tabele mają właściwe dane.
Konserwacja (tworzenie przyszłych partycji + retencja) uruchamia się przez:
-- Uruchom konserwację dla wszystkich tabel zarządzanych przez pg_partman
CALL partman.run_maintenance_proc();
-- Lub tylko dla konkretnej tabeli
CALL partman.run_maintenance_proc(p_parent_table => 'public.app_events');
-- Starsza forma (PostgreSQL < 14, bez CALL)
SELECT partman.run_maintenance();
Po wywołaniu sprawdź logi i stan partycji:
-- Stan konfiguracji partycjonowania
SELECT
parent_table,
control,
partition_interval,
premake,
retention,
retention_keep_table,
last_partition
FROM partman.part_config;
-- parent_table | control | partition_interval | premake | retention | ...
-- --------------------+------------+--------------------+---------+-----------+----
-- public.app_events | created_at | 1 month | 3 | 6 months | ...
Partycjonowanie po kluczu całkowitym (np. ID) sprawdza się przy ogromnych tabelach, gdzie zapytania często filtrują po zakresie ID:
-- Tabela zamówień partycjonowana co 1 000 000 rekordów
CREATE TABLE zamowienia (
id BIGINT NOT NULL,
klient_id INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
kwota NUMERIC(12,2),
status VARCHAR(32)
) PARTITION BY RANGE (id);
SELECT partman.create_parent(
p_parent_table => 'public.zamowienia',
p_control => 'id',
p_interval => '1000000', -- partycja co milion wierszy
p_start_partition => '1' -- zacznij od ID=1
);
-- Partycje: zamowienia_p1, zamowienia_p1000001, zamowienia_p2000001, ...
TIMESTAMPTZ.
Jeśli masz już tabelę z danymi i chcesz ją przepartycjonować, pg_partman udostępnia funkcję do migracji wsadowej — bez blokowania tabeli na czas całej operacji:
-- Stara tabela (bez partycjonowania)
CREATE TABLE logi_stare (
id BIGINT GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMPTZ NOT NULL,
tresc TEXT
);
-- ... miliony istniejących wierszy ...
-- 1. Utwórz nową tabelę partycjonowaną (z tą samą strukturą)
CREATE TABLE logi (
id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
tresc TEXT
) PARTITION BY RANGE (created_at);
SELECT partman.create_parent(
p_parent_table => 'public.logi',
p_control => 'created_at',
p_interval => '1 month'
);
-- 2. Migruj dane wsadami (batch_interval = rozmiar wsadu)
-- Ta funkcja może być wywoływana wielokrotnie, aż zwróci 0
CALL partman.partition_data_proc(
p_parent_table => 'public.logi',
p_source_table => 'public.logi_stare', -- skąd przenosić
p_batch_count => 10, -- ile wsadów na raz
p_batch_interval => '1 month' -- przenoś po miesiącu danych
);
-- Wywołuj w pętli, aż wszystkie dane zostaną przeniesione
Wsadowa migracja zajmuje krótkie blokady na małe partie danych, więc tabela źródłowa jest dostępna podczas całego procesu. W praktyce uruchamia się to przez prostą pętlę w shellu:
# Bash: migruj dane, aż partition_data_proc zwróci 0 rowcount
while true; do
result=$(psql -d mydb -c "CALL partman.partition_data_proc(
p_parent_table => 'public.logi',
p_source_table => 'public.logi_stare',
p_batch_count => 20
);" 2>&1)
echo "$result"
sleep 1
done
Zamiast konfigurować cron do wywołania run_maintenance(), pg_partman oferuje wbudowany background worker, który robi to automatycznie co N sekund:
-- postgresql.conf lub przez ALTER SYSTEM
ALTER SYSTEM SET shared_preload_libraries = 'pg_partman_bgw';
-- Interwał konserwacji (sekundy): domyślnie 3600 (1h)
ALTER SYSTEM SET pg_partman_bgw.interval = '3600';
-- Bazy, na których bgw ma działać (przecinkami)
ALTER SYSTEM SET pg_partman_bgw.dbname = 'mydb';
-- Rola, jako która bgw się łączy (musi mieć uprawnienia do partman)
ALTER SYSTEM SET pg_partman_bgw.role = 'postgres';
-- Załaduj konfigurację (wymaga restartu PostgreSQL)
-- Restart serwisu
sudo systemctl restart postgresql
-- Weryfikacja — bgw pojawi się w pg_stat_activity
SELECT pid, backend_type, state, query
FROM pg_stat_activity
WHERE backend_type = 'pg_partman_bgw';
-- pid | backend_type | state | query
-- -------+------------------+-------+---------
-- 12345 | pg_partman_bgw | idle |
CALL partman.run_maintenance_proc().
Zestawmy wszystko razem: tabela zamówień partycjonowana miesięcznie, z retencją 2 lat i background workerem:
-- ============================================================
-- Tabela główna
-- ============================================================
CREATE TABLE orders (
order_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
customer_id INTEGER NOT NULL,
total_netto NUMERIC(12,2) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'NEW'
) PARTITION BY RANGE (created_at);
CREATE INDEX orders_customer_idx ON orders (customer_id, created_at);
CREATE INDEX orders_status_idx ON orders (status, created_at);
-- ============================================================
-- Rejestracja w pg_partman
-- ============================================================
SELECT partman.create_parent(
p_parent_table => 'public.orders',
p_control => 'created_at',
p_interval => '1 month',
p_premake => 4 -- twórz 4 partycje do przodu
);
-- ============================================================
-- Konfiguracja retencji: zachowaj 24 miesiące
-- ============================================================
UPDATE partman.part_config
SET
retention = '24 months',
retention_keep_table = false,
retention_keep_index = false,
premake = 4
WHERE parent_table = 'public.orders';
-- ============================================================
-- Sprawdź co zostało utworzone
-- ============================================================
SELECT
child.relname AS partycja,
pg_size_pretty(pg_total_relation_size(child.oid)) AS rozmiar,
pg_get_expr(child.relpartbound, child.oid) AS zakres
FROM pg_inherits i
JOIN pg_class parent ON i.inhparent = parent.oid
JOIN pg_class child ON i.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY child.relname;
-- ============================================================
-- Partition pruning — PostgreSQL automatycznie pomija partycje
-- ============================================================
EXPLAIN (ANALYZE, FORMAT TEXT)
SELECT COUNT(*), SUM(total_netto)
FROM orders
WHERE created_at BETWEEN '2026-05-01' AND '2026-05-31';
-- Planner czyta tylko partycję orders_p20260501 — reszta pomijana!
-- Wszystkie tabele pod kontrolą pg_partman
SELECT
parent_table,
partition_interval,
premake,
retention,
last_partition,
sub_partition_set_full
FROM partman.part_config
ORDER BY parent_table;
-- Liczba wierszy per partycja (z katalogu systemowego)
SELECT
child.relname AS partycja,
child.reltuples::bigint AS wiersze_est,
pg_size_pretty(pg_total_relation_size(child.oid)) AS rozmiar
FROM pg_inherits i
JOIN pg_class parent ON i.inhparent = parent.oid
JOIN pg_class child ON i.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY child.relname;
-- Historia konserwacji (logi pg_partman)
SELECT *
FROM partman.part_config_sub
LIMIT 20;
| Problem | Rozwiązanie |
|---|---|
| Klucz główny musi zawierać kolumnę partycjonującą | PRIMARY KEY (order_id, created_at) — dodaj created_at do PK |
| Indeksy UNIQUE muszą zawierać kolumnę partycjonującą | W PostgreSQL 10+ każdy UNIQUE obejmuje tylko partycję, nie całą tabelę |
| INSERT do nieistniejącej partycji | Dane trafiają do partycji DEFAULT; po run_maintenance() są migrowane |
| pg_dump partycjonowanej tabeli | pg_dump domyślnie dumping tabeli parent + wszystkich partycji — działa poprawnie |
| Zmiana interwału partycjonowania | Niemożliwa bez odtworzenia struktury — zaplanuj to przed wdrożeniem |
| Wysoka liczba partycji spowalnia planowanie | Powyżej ~1000 partycji czas planowania rośnie; użyj większych interwałów |
Szkolenie Administracja, replikacja i tuning baz danych PostgreSQL
Termin gwarantowany: 22.06.2026 | 3 500 zł netto | 5 dni
Partycjonowanie, pg_partman, replikacja strumieniowa, HA z Patroni, EXPLAIN ANALYZE, vacuuming
Sprawdź termin i zapisz się
Komentarze (0)
Brak komentarzy...