Blog JSystems - uwalniamy wiedzę!

Szukaj
Z tego artykułu dowiesz się:
  • Czym jest pg_partman i kiedy warto go użyć
  • Jak zainstalować i skonfigurować rozszerzenie
  • Jak tworzyć partycje czasowe (dzień, miesiąc, rok) i zakresowe (po ID)
  • Jak zarządzać retencją — automatyczne usuwanie starych partycji
  • Jak migrować dane z niepatycjonowanej tabeli
  • Jak skonfigurować background worker do automatycznej konserwacji

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.

Czym jest pg_partman?

pg_partman (Partition Manager) to rozszerzenie PostgreSQL, które automatyzuje cały cykl życia partycji:

  • Premake — tworzy partycje z wyprzedzeniem (domyślnie 4 naprzód)
  • Retention — usuwa stare partycje po zadanym czasie
  • Migracja danych — przeprowadza dane z istniejącej tabeli do struktury partycjonowanej
  • Background worker — odpala konserwację automatycznie, bez crona
  • Monitorowanie — widok part_config ze stanem wszystkich partycjonowanych tabel

pg_partman współpracuje z deklaratywnym partycjonowaniem PostgreSQL (dostępnym od wersji 10). Obsługuje PostgreSQL 13–17 i jest aktywnie rozwijany na GitHubie.

Instalacja

Debian / Ubuntu

# PostgreSQL 16
sudo apt install postgresql-16-partman

# PostgreSQL 17
sudo apt install postgresql-17-partman

Red Hat / Rocky Linux

sudo dnf install pg_partman_17

Aktywacja w bazie

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

Partycjonowanie czasowe — tabela logów

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).

Krok 1 — utwórz tabelę główną (parent)

-- 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);

Krok 2 — zarejestruj tabelę w pg_partman

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
Partycja DEFAULT — pg_partman tworzy partycję _default, która przechowuje wiersze nienależące do żadnej istniejącej partycji. Gdy tworzysz nową partycję, dane z DEFAULT są do niej migrowane automatycznie.

Dostępne interwały czasowe

InterwałPrzykład nazwy partycjiKiedy stosować
'1 hour'events_p2026060914Logi o bardzo wysokim wolumenie (miliony/dzień)
'1 day'events_p20260609Logi aplikacji, dane IoT
'1 week'events_p2026w23Dane transakcyjne, sesje
'1 month'events_p20260601Zamówienia, faktury — najczęstszy wybór
'3 months'events_p20260401Dane archiwalne, rzadkie zapytania
'1 year'events_p20260101Długoterminowe dane historyczne

Retencja — automatyczne usuwanie starych partycji

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';
Uwaga: 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.

run_maintenance() — ręczna konserwacja

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 zakresowe po ID

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, ...
Partycjonowanie po ID kontra po czasie — partycjonowanie po ID ma sens gdy zapytania filtrują po zakresie ID lub gdy chcesz archiwizować starsze partie danych. Dla danych czasowych (logi, zdarzenia) zdecydowanie lepsze jest partycjonowanie po TIMESTAMPTZ.

Migracja danych z istniejącej tabeli

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

Background worker — bez crona

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  |
bgw vs cron: Background worker to prostsze rozwiązanie dla typowych przypadków. Jeśli potrzebujesz różnych interwałów dla różnych baz lub bardziej szczegółowego harmonogramu, użyj pg_cron lub systemowego crona wywołującego CALL partman.run_maintenance_proc().

Praktyczny przykład — tabela zamówień e-commerce

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!

Monitorowanie partycji

-- 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;

Na co uważać

ProblemRozwią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 partycjiDane trafiają do partycji DEFAULT; po run_maintenance() są migrowane
pg_dump partycjonowanej tabelipg_dump domyślnie dumping tabeli parent + wszystkich partycji — działa poprawnie
Zmiana interwału partycjonowaniaNiemożliwa bez odtworzenia struktury — zaplanuj to przed wdrożeniem
Wysoka liczba partycji spowalnia planowaniePowyżej ~1000 partycji czas planowania rośnie; użyj większych interwałów

Chcesz opanować PostgreSQL w praktyce?

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ę

Najczęściej zadawane pytania

Co to jest pg_partman?
pg_partman to rozszerzenie PostgreSQL, które automatyzuje zarządzanie partycjami tabel: automatycznie tworzy nowe partycje (na przyszłość), usuwa stare (retencja), migruje istniejące dane do partycjonowanej struktury i może działać jako background worker. Obsługuje partycjonowanie czasowe (dzień, tydzień, miesiąc, rok) i zakresowe (np. po ID).
Jak zainstalować pg_partman?
Na Debianie/Ubuntu: sudo apt install postgresql-17-partman. Następnie w psql: CREATE SCHEMA partman; CREATE EXTENSION pg_partman SCHEMA partman;. Do automatycznej konserwacji: ALTER SYSTEM SET shared_preload_libraries = 'pg_partman_bgw'; + restart PostgreSQL.
Czym różni się pg_partman od natywnego partycjonowania PostgreSQL?
Natywne partycjonowanie PostgreSQL (PARTITION BY RANGE/LIST/HASH) wymaga ręcznego tworzenia każdej partycji i ręcznego zarządzania retencją. pg_partman robi to automatycznie: tworzy przyszłe partycje z wyprzedzeniem (parametr premake), usuwa stare po zadanym czasie (retention), migruje dane z nieplatycjonowanej tabeli i monitoruje wszystko w widoku part_config.
Jak skonfigurować automatyczne usuwanie starych partycji?
W tabeli partman.part_config ustaw retention na wartość interwału (np. '3 months') i retention_keep_table na false. pg_partman usunie partycje starsze niż 3 miesiące przy każdym wywołaniu run_maintenance() lub automatycznie przez background worker.

Komentarze (0)

Musisz być zalogowany by móc dodać komentarz. Zaloguj się przez Google

Brak komentarzy...