PostgreSQL 19: zmenšení tabulek pomocí REPACK bez obav

Dnes
Doba čtení: 34 minut

Sdílet

Dva sloni, maskoti PostgreSQL
Autor: Duck.ai
O předchozích verzích jsem napsal, že změny byly primárně interní. Devatenáctka je jiná. Tam je viditelných změn opravdu hodně. Nová verze je pelmelem nových funkcí a různých vylepšení, menších nebo větších.

Administrace

Co se dozvíte v článku
  1. Administrace
  2. pg_plan_advice
  3. SQL
  4. SQL/PGQ
  5. Nové datové typy a funkce
  6. Optimalizace
  7. Replikace
  8. Ostatní
  9. PL/pgSQL
  10. Vývoj

Bloating tabulek může být problém. Skoro všude jsem se setkal se situací, kdy „mazací“ skript přestal fungovat (z různých důvodů), přičemž se tato chyba detekovala příliš pozdě, kdy dotčené tabulky měly místo jednotek GB například desítky GB. To už musí zaúřadovat VACUUM FULL, což často znamená vynucenou plánovanou odstávku (kvůli držení exkluzivního zámku). Je to letitý problém, který zkušenější uživatelé řešili buďto pečlivějším monitoringem, v některých případech partišningem (partitioning), případně extenzemi pg_repackpg_squeeze.

Autor posledně jmenované extenze Tonda (Antonín Houska) spolu s Alvarem (Alvaro Herrera) poslední dva roky pracovali na integraci pg_squeeze do jádra Postgresu. Paradoxně příkaz, který bude provádět zdrcnutí tabulek, se bude jmenovat REPACK. Základem je logická replikace – zadaná tabulka se lokálně zreplikuje (vytvoří se její nová kopie, a tím se zbaví vnitřního neobsazeného místa). Co je zásadní, je možnost použít volbu CONCURRENTLY. S touto volbou příkaz nepoužije exkluzivní zámek. Tudíž je možné jej použít za provozu. Nutnou podmínkou je existence primárního klíče (a unikátního indexu).

REPACK (CONCURRENTLY, VERBOSE) obce;
INFO:  repacking "public.obce" in physical order
INFO:  "public.obce": found 0 removable, 6250 nonremovable row versions in 59 pages
DETAIL:  0 dead row versions cannot be removed yet.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
REPACK

Kromě toho, že příkaz REPACK nahrazuje příkaz VACUUM FULL, tak tento příkaz ještě nahrazuje příkaz CLUSTER, kterým se tabulka fyzicky (jednorázově) přeuspořádá podle zadaného indexu.

Možná jste si všimli, že dotaz běžící pod příkazem EXPLAIN ANALYZE je výrazně pomalejší. Důvodem je měření času – executor, v tomto režimu, neustále poptává aktuální čas, a to má brutální režii. U větších dotazů se vyplatí použít volbu TIMING OFF. V 19 na x86 se místo systémového času použíjí takty CPU. Je to výrazně rychlejší – zatímco na osmnáctce mi jednoduchý testovací dotaz běžel 2.5× pomalejší v explainu, na devatenáctce pouze 1.5× pomaleji.

Malou velkou změnou je vypnutí JIT ve výchozí konfiguraci. JIT se objevil v Postgresu 11, a v Postgresu 12 byl zapnutý ve výchozí konfiguraci. Bohužel se zjistilo, že většině uživatelů, kteří Postgres používají pro OLTP, dělá víc škody než užitku. Došlo to až do stavu, kdy se uživatelům doporučovalo, neoficiálně, po instalaci JIT vypnout. Je tam víc problémů. clang, na kterém je to postavené, v novějších verzích kompiluje lépe, ale pomaleji. Existují alternativní implementace JIT, ale všechny jsou více než méně experimentální. Druhým problémem je model, na jehož základě se JIT aktivuje.

Tento model jednak nedokáže dobře odhadnout náročnost „jitifikace“ – inliningu, a navíc je citlivý na přestřelení odhadu počtu řádek. Ohledně modelu a citlivosti na odhady pochybuji, že se dá něco dělat. Alternativní implementace JIT mají výrazně nižší režii. Přijde mi to ale jako slepá cesta. Budoucnost vidím v integraci vektorového executoru a vektorových funkcí (tak jak to používá DuckDB). Může to znamenat, že Postgres bude mít duální implementace funkcí (pro skalární parametr, a pro vektor parametrů).

Nově lze vytvořit replikační slot pro logickou replikaci, bez nutnosti mít konfiguraci wal_level=logical. Postačuje nastavení replica, což je výchozí nastavení. Jakmile je logický replikační slot aktivní, interně se úroveň zápisu do transakčního logu nastaví na logical. Jaký je aktuální stav zjistíte v konfigurační proměnné  effective_wal_level.

Další bombou je možnost zapínat nebo vypínat kontrolní součty (na úrovni datových stránek) za běhu. Kontrolní součty jsou v Postgresu už relativně dlouho, a ačkoliv na dnešním hardware je jejich režie zanedbatelná, tak určitě bude dost databází, kde nebudou zapnuté. Zapnutí kontrolních součtů vyžadovalo odstávku úměrnou velikosti databáze. Navíc se silně doporučuje po zapnutí součtů znovu zreplikovat všechny fyzické repliky. Výhodou je diagnostika, možnost identifikovat poškozené stránky (a přeskočit je), možnost vyrobit replikovaný server ze starého primárního příkazem  pg_rewind.

Naplánovat a provést delší odstávku někde nemusí být jednoduché (nebo to může být skoro nemožné). U jednoho mého klienta zapnutí kontrolních součtů na cca 2TB databázi trvalo cca 2 hodiny (na vlastním hardware z roku 2025). Zapnutí se provede voláním funkce pg_enable_data_checksums. Nastavením volitelných parametrů lze běh funkce zpomalit, tak aby nepřetěžovala IO. Vypnutí provede funkce pg_disable_data_checksums. Stav procesu můžeme vidět v pohledu  pg_stat_progress_data_checksums.

Snad se už konečně vyřešil problém s upgradem databází obsahující extrémně velký počet LO (large objects). Nově binární upgrade ( pg_upgrade) použije pro export metadat LO příkaz COPY místo původní sekvence příkazů:

-- původní sekvence
SELECT pg_catalog.lo_create('5432');
ALTER LARGE OBJECT 5432 OWNER TO alice;
GRANT SELECT ON LARGE OBJECT 5432 TO bob;

-- nově
COPY pg_catalog.pg_largeobject_metadata (oid, lomowner, lomacl) FROM stdin;
5432 16384 {alice=rw/alice,bob=r/alice}
\.

COPY pg_catalog.pg_shdepend (dbid, classid, objid, objsubid, refclassid, refobjid, deptype) FROM stdin;
5 2613 5432 0 1260 16384 o
5 2613 5432 0 1260 16385 a
\.

Nová implementace by měla být řádově rychlejší.

Konečně se dotáhlo do konce začlenění podpory více formátů v příkazu pg_dumpall. Postgres má dva základní příkazy pro export (backup) – pg_dump a pg_dumpall. Historicky tyto příkazy podporovaly pouze plain text formát. Postupem času pg_dump získal možnost exportu do custom, tar a dictionary formátu plus import příkazem pg_restore umožňoval paralelní import více tabulek. pg_dumpall byl díky podpoře pouze SQL formátu dost omezený.

To se nyní změnilo, a v pg_dumpall  jsou podporovány všechny formáty, které podporuje pg_dump. Vypadá to trochu jako workaround. V případě, že se použije jiný než plain text (SQL) formát, tak se vyrobí adresář, který do podadresářů uloží výstup z pg_dumpu. pg_restore umí s touto adresářovou strukturou pracovat. Tato možnost se dostala už do 18tky, ale byla na poslední chvíli stažena.

V pg_rewind  zredukovalo kopírování WAL logů z nového primárního serveru na budovanou repliku a to tak, že se budou kopírovat pouze WAL logy vzniklé po rozpadu replikace. V mnoha scénářích se tím výrazně urychlí běh tohoto příkazu.

Doposud autovacuum vybíralo tabulky k vakuování na základě porovnání vypočítaných koeficientů a prahových hodnot. Pořadí vybraných tabulek záleželo na fyzickém pořadí v systémové tabulce pg_class. Nově se vakuum bude prioritizovat podle míry vzdálenosti od prahu. Dalším kritériem je stáří tabulky (počet transakcí od freeze). Stará implementace byla férová v tom, že garantovala, že každá tabulka bude zvakuovaná a že žádná tabulka nebude „předbíhat“ ve frontě. Na druhou stranu se mohlo stát, že tabulka s intenzivnějším provozem čekala příliš dlouho a došlo k zbytečnému bloatingu.

Nová implementace se snaží být chytřejší. Uvidíme, jak to bude fungovat. Moje zkušenost je taková, že a) není dobré dávat do Postgresu příliš dynamická data, b) je dobré si nastavit počet aktivních autovacuum workerů tak, aby byla určitá rezerva, c) není dobré vytěžovat IO na maximum (je dobré mít dostatečně nadimenzované IO a mít nějakou rezervu). Skóre tabulek můžeme sledovat v pohledu pg_stat_autovacuum_score:

SELECT * FROM pg_stat_autovacuum_scores WHERE score > 0.05;
┌─[ RECORD 1 ]────────┬──────────────────────┐
│ relid               │ 1262                 │
│ schemaname          │ pg_catalog           │
│ relname             │ pg_database          │
│ score               │ 0.15936254737880529  │
│ xid_score           │ 1e-07                │
│ mxid_score          │ 0                    │
│ vacuum_score        │ 0.0793650769622719   │
│ vacuum_insert_score │ 0.003998400542165933 │
│ analyze_score       │ 0.15936254737880529  │
│ do_vacuum           │ f                    │
│ do_analyze          │ f                    │
│ for_wraparound      │ f                    │
╞═[ RECORD 2 ]════════╪══════════════════════╡
│ relid               │ 1214                 │
│ schemaname          │ pg_catalog           │
│ relname             │ pg_shdepend          │
│ score               │ 0.12                 │
│ xid_score           │ 1.15e-07             │
│ mxid_score          │ 0                    │
│ vacuum_score        │ 0                    │
│ vacuum_insert_score │ 0.006                │
│ analyze_score       │ 0.12                 │
│ do_vacuum           │ f                    │
│ do_analyze          │ f                    │
│ for_wraparound      │ f                    │
└─────────────────────┴──────────────────────┘

Nově autovacuum může použítVACUUM s podporou paralelismu.

Přehled o aktuálním stavu recovery je k dispozici v pohledu pg_stat_recovery. V pohledu pg_stat_statements jsou dva nové sloupce generic_plan_calls a custom_plan_calls zobrazující kolikrát předpřipravený příkaz (prepared statement) použil generický nebo zákaznický plán. Nový sloupec wal_fpi_bytes zobrazuje velikost zápisu tzv. plných stránek do transakčního logu. Vysoká hodnota může signalizovat nepříjemně velkou četnost vynucených checkpointů (je nutné zvednout max_wal_size případně checkpoint_timeout). V novém pohledu pg_stat_lock uvidíme počet a celkový čas čekání na různé typy zámků. Zaznamenány jsou pouze ty situace, kdy se čeká na zámek déle než jednu sekundu (stejně jako např. log_lock_waits, které je v nyní ve výchozí konfiguraci zapnuté). Bohužel zámky, které trvají cca 100ms, mající nepříjemný dopad na dotazy kolem 10ms, zde neuvidíme:

SELECT * FROM pg_stat_lock ;
┌──────────────────┬───────┬───────────┬───────────────────┬───────────────────────────────┐
│     locktype     │ waits │ wait_time │ fastpath_exceeded │          stats_reset          │
╞══════════════════╪═══════╪═══════════╪═══════════════════╪═══════════════════════════════╡
│ relation         │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ extend           │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ frozenid         │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ page             │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ tuple            │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ transactionid    │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ virtualxid       │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ spectoken        │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ object           │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ userlock         │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ advisory         │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
│ applytransaction │     0 │         0 │                 0 │ 2026-04-17 07:43:12.415431+02 │
└──────────────────┴───────┴───────────┴───────────────────┴───────────────────────────────┘
(12 rows)

Od minulé verze Postgres podporuje asynchronní IO. Čtení je realizováno dedikovanými procesy (workers). Počáteční implementace pracovala s konfigurovatelným počtem těchto procesů (výchozí nastavení mělo hodnotu 3). Nově je správa workerů dynamičtější – v případě potřeby si je Postgres nastartuje nebo naopak zavře. Místo fixního počtu nyní nastavujeme minimální a maximální počet workerů:

io_min_workers=2
io_max_workers=8 (up to 32)
io_worker_idle_timeout=60s
io_worker_launch_interval=100ms

Poměrně razantně se změnila konfigurace log_min_messages , která udává, na které úrovni se mají chyby zapsat do systémového logu (DEBUG5 ..PANIC ). Nově lze tuto úroveň nastavit pro každý typ procesu (archiver , autovacuum , backend , bgworker , …) např. error, walsender:debug1, autovacuum:debug1 .

Správa partišen (partitions) byla v Postgresu základní. Díky možnosti sloučit nebo rozdělit partišny (partitions) se dostává trochu dál. Je to spíš první krok se spoustou omezení – používá se pouze jedno CPU a po celou dobu je exkluzivně zamknutá rodičovská tabulka. Ale i tak to může dost uživatelům ušetřit práci (a jejich vlastní řešení by pravděpodobně mělo stejná omezení):

ALTER TABLE sales_list MERGE PARTITIONS (sales_west, sales_east, sales_central)  INTO sales_all;

ALTER TABLE measurement SPLIT PARTITION measurement_y2006q1 INTO
   (PARTITION measurement_y2006m01 FOR VALUES FROM ('2006-01-01') TO ('2006-02-01'),
    PARTITION measurement_y2006m02 FOR VALUES FROM ('2006-02-01') TO ('2006-03-01'),
    PARTITION measurement_y2006m03 FOR VALUES FROM ('2006-03-01') TO ('2006-04-01'));

Oproti předchozím verzím lze zapínat a vypínat všechny typy omezení (nejen cizí klíče):

CREATE TABLE foo(a int, CONSTRAINT cc CHECK(a > 0) NOT ENFORCED);
CREATE TABLE
INSERT INTO foo VALUES(0);
INSERT 0 1
INSERT INTO foo VALUES(1);
INSERT 0 1
ALTER TABLE foo ALTER CONSTRAINT cc ENFORCED;
ERROR:  check constraint "cc" of relation "foo" is violated by some row
DELETE FROM foo WHERE a = 0;
DELETE 1
ALTER TABLE foo ALTER CONSTRAINT cc ENFORCED;
ALTER TABLE
INSERT INTO foo VALUES(0);
ERROR:  new row for relation "foo" violates check constraint "cc"
DETAIL:  Failing row contains (0).
ALTER TABLE foo ALTER CONSTRAINT cc NOT ENFORCED;
ALTER TABLE
INSERT INTO foo VALUES(0);
INSERT 0 1

pg_plan_advice

Až doposud uživatelé Postgresu neměli moc možností, jak ovlivnit generování prováděcích plánů. Hinty ve stylu Oracle byly a jsou z historických důvodů tabu (jsou implementovány extenzí pg_hint_plan). Uživatel měl pouze možnost penalizace metod executoru – např. enable_nestloop nebo enable_indexscan nastavením na off. Tato penalizace je ale dost hrubá, a neumožňuje jemné ovlivnění plánovače. Přesto se používala a používá. Rober Haas napsal dvě extenzepg_plan_advice a pg_stash_advice . Prvně zmíněná extenze umožňuje definovat jakási doporučení, která by měl planner respektovat. Pro každý plán lze zpětně dogenerovat tato doporučení:

EXPLAIN (PLAN_ADVICE)
 SELECT *
   FROM obce
  ORDER BY pocet_muzu + pocet_zen DESC
  LIMIT 10;
┌──────────────────────────────────────────────────────────────────────┐
│                              QUERY PLAN                              │
╞══════════════════════════════════════════════════════════════════════╡
│ Limit  (cost=272.19..272.21 rows=10 width=45)                        │
│   ->  Sort  (cost=272.19..287.81 rows=6250 width=45)                 │
│         Sort Key: ((pocet_muzu + pocet_zen)) DESC                    │
│         ->  Seq Scan on obce  (cost=0.00..137.12 rows=6250 width=45) │
│ Generated Plan Advice:                                               │
│   SEQ_SCAN(obce)                                                     │
│   NO_GATHER(obce)                                                    │
└──────────────────────────────────────────────────────────────────────┘
(7 rows)

EXPLAIN (PLAN_ADVICE)
 SELECT *
   FROM obce JOIN okresy ON obce.okres_id = okresy.id
  WHERE okresy.nazev = 'Beroun';
┌──────────────────────────────────────────────────────────────────────────────────────┐
│                                      QUERY PLAN                                      │
╞══════════════════════════════════════════════════════════════════════════════════════╡
│ Nested Loop  (cost=0.28..12.56 rows=81 width=58)                                     │
│   ->  Seq Scan on okresy  (cost=0.00..1.96 rows=1 width=17)                          │
│         Filter: (nazev = 'Beroun'::text)                                             │
│   ->  Index Scan using obce_okres_id_idx on obce  (cost=0.28..9.79 rows=81 width=41) │
│         Index Cond: ((okres_id)::text = okresy.id)                                   │
│ Generated Plan Advice:                                                               │
│   JOIN_ORDER(okresy obce)                                                            │
│   NESTED_LOOP_PLAIN(obce)                                                            │
│   SEQ_SCAN(okresy)                                                                   │
│   INDEX_SCAN(obce public.obce_okres_id_idx)                                          │
│   NO_GATHER(obce okresy)                                                             │
└──────────────────────────────────────────────────────────────────────────────────────┘
(11 rows)

Prostřednictvím konfigurační proměnnépg_plan_advice.advice můžeme ovlivnit chování planneru vlastními doporučeními:

SET pg_plan_advice.advice to 'JOIN_ORDER(obce okresy)';
SET
EXPLAIN (PLAN_ADVICE)
 SELECT *
   FROM obce JOIN okresy ON obce.okres_id = okresy.id
  WHERE okresy.nazev = 'Beroun';
┌───────────────────────────────────────────────────────────────────┐
│                            QUERY PLAN                             │
╞═══════════════════════════════════════════════════════════════════╡
│ Hash Join  (cost=1.97..140.78 rows=81 width=58)                   │
│   Hash Cond: ((obce.okres_id)::text = okresy.id)                  │
│   ->  Seq Scan on obce  (cost=0.00..121.50 rows=6250 width=41)    │
│   ->  Hash  (cost=1.96..1.96 rows=1 width=17)                     │
│         ->  Seq Scan on okresy  (cost=0.00..1.96 rows=1 width=17) │
│               Filter: (nazev = 'Beroun'::text)                    │
│ Supplied Plan Advice:                                             │
│   JOIN_ORDER(obce okresy) /* matched */                           │
│ Generated Plan Advice:                                            │
│   JOIN_ORDER(obce okresy)                                         │
│   HASH_JOIN(okresy)                                               │
│   SEQ_SCAN(obce okresy)                                           │
│   NO_GATHER(obce okresy)                                          │
└───────────────────────────────────────────────────────────────────┘
(13 rows)

Konfigurace planneru prostřednictvímpg_plan_advice.advice nestačí, pokud potřebujeme ovlivnit plán dotazu z aplikace, u které nemůžeme nebo nechceme měnit zdrojový kód. A proto je tu druhá zmíněná extenze pg_stash_advice , která umožňuje naše „doporučení“ uložit a pak automaticky aplikovat na základě tzv queryid .

Doporučení jsou uložena v pojmenovaných skladech (stashes). Skrze konfigurační proměnnoupg_stash_advice.stash_name volíme aktivní sklad. Každý uživatel, každá databáze může mít svůj vlastní. Pokud proměnná není nastavená, tak je extenzepg_stash_advice neaktivní. Uložené nápovědy jsou perzistentní – při vypnutí serveru se uloží, po startu znovu načtou do paměti.

SQL

GROUP BY ALL je syntaxe, kterou postres přejímá z analytických databází (zde to snad byla DuckDB). Pokud se použije tento zápis, tak se automaticky agreguje podle všech sloupců, které nejsou v agregační funkci:

SELECT sum(pocet_zen + pocet_muzu), okresy.id, okresy.nazev
  FROM obce join okresy ON obce.okres_id = okresy.id
 GROUP BY ALL;

Tento zápis může ušetřit dost psaní, může to být také „tichý“ zabiják výkonu. Čím více atributů je v klauzuli GROUP BY, tím je agregace pomalejší. Zápis GROUP BY ALL ještě není ve standardu, ale je standardizační komisí akceptován, a měl by být v některé další verzi ANSI/SQL.

Počínaje Postgresem 19 už nebude možnost používat backlash jako escape symbol v řetězcích. Před 15 roky se přešlo na standardní chování (předtím se řetězce v Postgresu chovali stejně jako v Cčku), nicméně nestandardní chování bylo možné vynutit nastavenímstandard_conforming_strings na off .

Mezi top vývojáři Postgresu je hodně velká averze k tzv kompatibility flagům (zmíněný flag nebyl výjimkou) (pozn. u komerčních databází jsou naopak kompatibility flagu standardem, a nejen u nich – např. MySQL). Nyní je tato možnost zrušena, a Cčkové stringy dostaneme pouze jen zápisem (jako tzv extended strings):

SELECT 'ahoj\nsvete';
┌─────────────┐
│  ?column?   │
╞═════════════╡
│ ahoj\nsvete │
└─────────────┘
(1 row)

SELECT e'Ahoj\nSvete';
┌──────────┐
│ ?column? │
╞══════════╡
│ Ahoj    ↵│
│ Svete    │
└──────────┘
(1 row)

Nově lze v u window funkcí lead , lag , first_value ,last_value anth_value použít klauzuliIGNORE NULLS nebo RESPECT NULLS . Výchozí chování, které bylo implementováno již dříve odpovídá klauzuli RESPECT NULLS .

SQL/PGQ

V tomto případě PG není častým prefixem symbolizujícím Postgres (a Q jako queue), ale Property Graph Queries. Počínaje podporou neatomických datových typů v SQL:2000, přes podporu XML v SQL:2003 se standard rozšířil nad rámec čistě relačních databází (stejně ale už dříve existovala kritika SQL z pohledu čistoty od relačních dogmatiků). Standard SQL:2023 (ISO/IEC 9075–16:2023) SQL rozšiřuje o podporu grafových databází, které mají svůj vlastní datový model odlišný od relačního modelu. Přijde mi to jako dost odvážný krok (a ještě teď tomu nemohu úplně uvěřit). Zatím existovala zřetelná jednota mezi SQL a relačním modelem.

Od nové části standardu se očekává, že pomůže s přehlednějším zápisem určitých dotazů, které se až doposud řešily pomocí rekurze. Přiznám se, že zápis dotazu v PGQ (využívajícího ascii art) mi přijde hodně cizí (možná zažívám pocity programátora v Cobolu, když se poprvé podíval na SQL. Osobně mi to nesedí – reprezentaci znalostí ve formátu grafu jsem nikdy nemusel (vím, že je to jen jiná reprezentace téhož)). Row pattern recognition z roku SQL:2016 toho také nemá s relačními databázemi moc společného, takže to takové překvapení není. Práce na této části standardu začaly v roce 2017 (a integrovaly se prvky jazyka Cypher (Neo4j) a PGQL (Oracle)).

Základem grafových databází je graf skládající se z uzlů a hran. Ka každému uzlu i hraně mohou být přiřazeny atributy (properties). Hrany mohou být orientované i neorientované. Standard pro takové grafy používá termín Property Graphs. Graf se definuje příkazem CREATE PROPERTY GRAPH a je to určitá forma pohledu. Přesto, že jsem zkoušel poměrně triviální příklad, nebylo pro mne intuitivní napsat SQL/PGQ dotaz. Pro uživatele bez zkušenosti s Neo4j nebo jinou grafovou databází budou začátky s SQL/PGQ dost bolestivé. V tuto chvíli (duben 2026) ještě není hotová dokumentace. Je možné, že se díky integraci SQL/PGQ do Postgresu setkám s grafovými databázemi častěji (SQL/PGQ je podporováno v DuckDB a v Oracle):

CREATE TABLE public.uzivatele (
  id integer NOT NULL,
  jmeno character varying
);

CREATE TABLE public.zpravy (
  id integer NOT NULL,
  odesilatel integer,
  prijemce integer,
  tema text
);

-- uzivatel - odeslal -> uzivatel
CREATE PROPERTY GRAPH public.pg1
  VERTEX TABLES (
    public.uzivatele AS uzivatel KEY (id) PROPERTIES (id, jmeno)
  )
  EDGE TABLES (
    public.zpravy AS odeslal KEY (id)
    SOURCE KEY (odesilatel) REFERENCES uzivatel (id)
    DESTINATION KEY (prijemce) REFERENCES uzivatel (id)
  )
);

INSERT INTO public.uzivatele VALUES (1, 'Pavel');
INSERT INTO public.uzivatele VALUES (2, 'Zdenek');
INSERT INTO public.uzivatele VALUES (3, 'Tomas');

INSERT INTO public.zpravy VALUES (1, 1, 2, 'plan dovolene');
INSERT INTO public.zpravy VALUES (2, 2, 1, 'plan dovolene');
INSERT INTO public.zpravy VALUES (3, 1, 3, 'dotaz ohledne dopravy');

-- od koho uzivatel Pavel dostal zpravu
SELECT *
  FROM GRAPH_TABLE(pg1
       MATCH (a IS uzivatel WHERE a.jmeno = 'Pavel') <-[IS odeslal]- (b IS uzivatel)
       COLUMNS (b.jmeno));
┌────────┐
│ jmeno  │
╞════════╡
│ Zdenek │
└────────┘
(1 row)

-- komu uzivatel Pavel poslal zpravu
SELECT *
  FROM GRAPH_TABLE(pg1
       MATCH (a IS uzivatel WHERE a.jmeno = 'Pavel') -[IS odeslal]-> (b IS uzivatel)
       COLUMNS (b.jmeno));
┌────────┐
│ jmeno  │
╞════════╡
│ Zdenek │
│ Tomas  │
└────────┘
(2 rows)

Osobně si nedovedu představit, že bych psal dotazy v SQL/PGQ (myslím, že podobně na tom bude většina uživatelů podobně). Je otázkou, jestli se SQL/PGQ neuplatní ve spolupráci s AI. V tomto případě se běžný dotaz rozpadá do dvou kroků. Navíc i joinovací podmínka získává label, takže je to v určitém smyslu více popisné, než běžné SQL, a je tak možné, že tento zápis bude AI více sedět.

Pomalu se pokračuje na implementaci tzv temporálních tabulek. Letos je to podpora klauzule FOR PORTION OF valid_at FROM TO. Zjednodušeně, pokud máme temporální databáze nebo temporální tabulky, tak se můžeme podívat na stav dat v nějakém čase. Pokud se použije klauzule FOR PORTION OF FROM TO, tak se jedná o tzv temporální UPDATE, DELETE:

-- je nutne mit nainstalovanou extenzi btree_gist
CREATE TABLE ceny(
  id int,
  valid_at daterange,
  nazev text,
  cena int,
  PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);
CREATE TABLE

-- jednotna cena mezi 2020-01-01 a 2025-12-31
INSERT INTO ceny VALUES(1, '[2020-01-01,2026-01-01)', 'pecivo', 20);
INSERT 0 1

SELECT * FROM ceny;
┌────┬─────────────────────────┬────────┬──────┐
│ id │        valid_at         │ nazev  │ cena │
╞════╪═════════════════════════╪════════╪══════╡
│  1 │ [2020-01-01,2026-01-01) │ pecivo │   20 │
└────┴─────────────────────────┴────────┴──────┘
(1 row)

-- uprava ceny mezi 2021-01-01 a 2021-02-28
UPDATE ceny
   FOR PORTION OF valid_at FROM '2021-01-01' TO '2021-03-01'
   SET cena = 30
 WHERE id = 1;
UPDATE 1

SELECT * FROM ceny ORDER BY valid_at;
┌────┬─────────────────────────┬────────┬──────┐
│ id │        valid_at         │ nazev  │ cena │
╞════╪═════════════════════════╪════════╪══════╡
│  1 │ [2020-01-01,2021-01-01) │ pecivo │   20 │
│  1 │ [2021-01-01,2021-03-01) │ pecivo │   30 │
│  1 │ [2021-03-01,2026-01-01) │ pecivo │   20 │
└────┴─────────────────────────┴────────┴──────┘
(3 rows)

Nové datové typy a funkce

V Postgresu se můžete setkat s datovými typy s prefixem reg. regrole, regproc, regclass. Tyto datové typy jsou vždy spojeny s jednou konkrétní systémovou tabulkou a hodnoty těchto typů vždy nesou unikátní číselný identifikátor databázového objektu. Tyto typy podporují přetypování z textu, což se používá pro zkrácený zápis vyhledávání nějakého databázového objektu v katalogu. Nově můžeme používat typ regdatabase a tento typ se odkazuje, jak je patrné z názvu, na tabulku pg_database:

SELECT 'template1'::regdatabase;
┌─────────────┐
│ regdatabase │
╞═════════════╡
│ template1   │
└─────────────┘
(1 row)

SELECT 'template1'::regdatabase::oid;
┌─────┐
│ oid │
╞═════╡
│   1 │
└─────┘
(1 row)

SELECT oid
 FROM pg_database
 WHERE datname = 'template1';
┌─────┐
│ oid │
╞═════╡
│   1 │
└─────┘
(1 row)

Díky těmto typům jsou dotazy nad systémovým katalogem kratší a čitelnější. Navíc autor dotazů nemusí brát v potaz některé detaily (jako například case sensitivitu), kde se chování SQL identifikátorů liší od SQL stringů. To je ošetřeno interně při přetypování.

Funkce error_on_null vyhodí výjimku, pokud je parametr NULL. Jinak vrátí hodnotu parametru. Implementace je extrémně jednoduchá – sleduje flag isnull. Každý parametr každé SQL funkce má tento flag. Stejné chování má flag funkce STRICT. Je tu drobná zrada. V případě kompozitních typů to není kompatibilní s operátorem IS NULL. Kompozitní hodnota obsahující pouze NULL y je interně ne nullová, ale operátor IS NULL pro takovou kompozitní hodnotu vrátí true:

SELECT ROW(NULL, NULL) IS NULL;
┌──────────┐
│ ?column? │
╞══════════╡
│ t        │
└──────────┘
(1 row)

SELECT error_on_null(NULL::integer);
ERROR:  null value not allowed
SELECT error_on_null(ROW(NULL, NULL));
┌───────────────┐
│ error_on_null │
╞═══════════════╡
│ (,)           │
└───────────────┘
(1 row)

Roky se v Postgresu mluví o tom, že by měl existovat snadný způsob, jak zpětně z databázového objektu vygenerovat DDL příkaz. Osobně jsem nikdy tuhle potřebu neměl, ale do jisté míry této potřebě rozumím. Skoro v každém admin nástroji můžete iterovat přes databázové objekty a k nim si nechat zobrazit DDL. A skoro každý admin tool, pak tuto funkcionalitu duplicitně implementuje – s chybami nebo bez chyb. Je to kód, který vyžaduje neustálou údržbu a aktualizace. Po letech diskuzí se je v upstreamu první část funkcípg_get_...ddl (aktuálně pg_get_database_ddl ,pg_get_role_ddl pg_get_tablespace_ddl ):

SELECT pg_get_role_ddl('pavel');
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                       pg_get_role_ddl                                        │
╞══════════════════════════════════════════════════════════════════════════════════════════════╡
│ CREATE ROLE pavel SUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS; │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
(1 row)

Od minulé verze je v postgresu funkce generující náhodné číslo v zadaném rozsahu. K nim nyní přibyly funkce pro rozsah typů date ,timestamp timestamptz :

SELECT random(current_date - 6, current_date);
┌────────────┐
│   random   │
╞════════════╡
│ 2026-04-13 │
└────────────┘
(1 row)

postgres=# SELECT random(current_timestamp - interval '1 week', current_timestamp);
┌───────────────────────────────┐
│            random             │
╞═══════════════════════════════╡
│ 2026-04-09 02:30:30.665819+02 │
└───────────────────────────────┘
(1 row)

Optimalizace

Jako nejdůležitější optimalizaci v nové verzi vidím optimalizaci implementace kontroly referenční integrity. RI je v postgresu realizována v systémových řádkových triggerech, které, až doposud, provedly (pro každý řádek) jeden kontrolní SQL příkaz (s použitím SPI API – stejné API, které se používá např. v PL/pgSQL). Nově se hledá přímo v indexech – přeskakuje se celý aparát SQL. To je první výrazná optimalizace.

Druhou výraznou optimalizací je hledání nikoliv jedné hodnoty, ale pole hodnot (n = 64). Podle autora patche je nyní kontrola referenční integrity cca 3× rychlejší. Pořád to bude znát. Výrazně se ale posune hranice, od kdy se vyplatí při masivních importech kontrolu referenční integrity vypínat.

CREATE TABLE tab_a(id int PRIMARY KEY);
INSERT INTO tab_a SELECT generate_series(1,1000);
VACUUM ANALYZE tab_a;

CREATE UNLOGGED TABLE tab_b_no_ri(id int, a_id int);
CREATE UNLOGGED TABLE tab_b(id int, a_id int references tab_a(id));
\timing
-- postgres 18
INSERT INTO tab_b select i, random()*999 + 1
  FROM generate_series(1,1000000) g(i);
INSERT 0 1000000
Time: 12844,206 ms (00:12,844)
INSERT INTO tab_b_no_ri select i, random()*999 + 1
  FROM generate_series(1,1000000) g(i);
INSERT 0 1000000
Time: 659,462 ms

-- postgres 19
INSERT INTO tab_b select i, random()*999 + 1
  FROM generate_series(1,1000000) g(i);
INSERT 0 1000000
Time: 2728,073 ms (00:02,728)
INSERT INTO tab_b_no_ri select i, random()*999 + 1
  FROM generate_series(1,1000000) g(i);
INSERT 0 1000000
Time: 673,444 ms

Ukázkový test je best case – zrychlení je cca čtyřnásobné. Stále je import s RI cca 4× pomalejší než bez RI (a to cílová tabulka obsahuje pouze jeden cizí klíč). V praxi to bude jiné – kromě RI je náročná i aktualizace indexů a samozřejmě všechny diskové operace.

Určitě každý, kdo pracuje s Postgresem trochu déle, se setkal s doporučením „nepoužívej NOT IN (SELECT ... “. Důvodem byla (v některých případech) „špatná“ optimalizace. Optimalizátor neměl garanci, že subselect nevrátí NULL. Poté, co se ve verzi 18 přepsal systém pro ukládání omezujících pravidel (constraints), už bylo možné do tuto informaci v optimalizátoru použít. Nyní se by se měl subselect NOT IN transformovat na tzv anti-join (v těch případech, kdy je garantováno, že žádná proměnná v predikátu nebude NULL). Jinými slovy, pokud optimalizátor bude mít garance, že se v predikátu subselectu nevyskytne NULL, tak vygeneruje stejný plán, jako kdybyste použili zápis s NOT EXISTS:

CREATE TABLE osoby(id int PRIMARY KEY, jmeno varchar);
CREATE TABLE faktury(id int primary key,
                     vlozeno timestamp,
                     podal int REFERENCES osoby(id) NOT NULL);

EXPLAIN SELECT *
          FROM osoby
         WHERE id NOT IN (SELECT podal
                            FROM faktury);
┌──────────────────────────────────────────────────────────────────────┐
│                              QUERY PLAN                              │
╞══════════════════════════════════════════════════════════════════════╡
│ Hash Right Anti Join  (cost=38.58..82.22 rows=635 width=36)          │
│   Hash Cond: (faktury.podal = osoby.id)                              │
│   ->  Seq Scan on faktury  (cost=0.00..28.50 rows=1850 width=4)      │
│   ->  Hash  (cost=22.70..22.70 rows=1270 width=36)                   │
│         ->  Seq Scan on osoby  (cost=0.00..22.70 rows=1270 width=36) │
└──────────────────────────────────────────────────────────────────────┘
(5 rows)

EXPLAIN SELECT *
          FROM osoby
         WHERE NOT EXISTS(SELECT podal
                            FROM faktury
                           WHERE podal = osoby.id);
┌──────────────────────────────────────────────────────────────────────┐
│                              QUERY PLAN                              │
╞══════════════════════════════════════════════════════════════════════╡
│ Hash Right Anti Join  (cost=38.58..82.22 rows=635 width=36)          │
│   Hash Cond: (faktury.podal = osoby.id)                              │
│   ->  Seq Scan on faktury  (cost=0.00..28.50 rows=1850 width=4)      │
│   ->  Hash  (cost=22.70..22.70 rows=1270 width=36)                   │
│         ->  Seq Scan on osoby  (cost=0.00..22.70 rows=1270 width=36) │
└──────────────────────────────────────────────────────────────────────┘
(5 rows)
-- ve starších verzích dostanete různé plány

Ve starších verzích spojení relací vždy předcházelo agregaci. To v některých případech může být neefektivní (pokud agregace výrazně redukuje počet řádek). Pokud jsem chtěl změnit pořadí operací, musel jsem si to vynutit zápisem (poddotazem). V devatenáctce optimalizátor podporuje tzv předběžnou agregaci (eager aggregation), která předbíhá před join:

EXPLAIN SELECT sum(pocet_zen + pocet_muzu), okresy.nazev
                  FROM okresy JOIN obce ON okresy.id = obce.okres_id
                 GROUP BY okresy.nazev;
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                QUERY PLAN                                                │
╞══════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Finalize HashAggregate  (cost=157.38..158.15 rows=77 width=18)                                           │
│   Group Key: okresy.nazev                                                                                │
│   ->  Hash Join  (cost=3.02..157.00 rows=77 width=18)                                                    │
│         Hash Cond: ((obce.okres_id)::text = okresy.id)                                                   │
│         ->  Partial GroupAggregate  (cost=0.28..154.05 rows=77 width=15)                                 │
│               Group Key: obce.okres_id                                                                   │
│               ->  Index Only Scan using obce_okres_id_idx on obce  (cost=0.28..122.03 rows=6250 width=7) │
│         ->  Hash  (cost=1.77..1.77 rows=77 width=17)                                                     │
│               ->  Seq Scan on okresy  (cost=0.00..1.77 rows=77 width=17)                                 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(9 rows)

Na datech pro ČR je výše uvedený dotaz cca třikrát rychlejší (v pg19 vůči pg18).

Pokud je jeden řádek zamčený vícero transakcemi, tak se do jeho systémového atributuxmax uloží tzvMultiXact ID (MultiXact je „aparát“, kterým se v Postgresu implementují řádkové zámky). To je unikátní identifikátor skupiny transakcí, které drží zámek. Každá skupina transakcí se skládá z tzv členských záznamů MultiXactMember , ve kterých se drží identifikátor transakce, typ zámku a stav transakce. Také tyto záznamy mají své unikátní id MultiXactOffset . DoposudMultiXactOffset byla 32bitová hodnota. Nově je tato hodnota 64bitová, a nehrozí její přetečení.

MultiXact ID i id transakcí jsou stále 32bitové hodnoty (existuje několik rozpracovaných řešení, které zvyšují jejich rozsah), takže stále hrozí přetečení těchto id, a nastartování vynuceného freezování (což u více zatížených systémů může způsobit problémy s produkcí). Eliminovala se ale možnost přetečení MultiXactOffset . Je tp důležité, protožeMultiXactOffset rostl ze všech zmíněných id nejrychleji (pokudn je počet transakcí se sdílenými zámky, pak růstMultiXactOffset je nxn ). Informace o MultiXact (skupinách transakcí) lze získat novou funkcí pg_get_multixact_stats() .

Přepracoval se kód, který implementuje příkazy LISTEN, NOTIFY. Předchozí verze probouzely každý proces, který čekal na notifikaci (bez ohledu na to jestli čekal nebo nečekal na kanálu, který dostal notifikaci). Stará implementace fungovala dobře, pokud počet použitých kanálů byl malý, a tudíž byla i malá šance, že se probudí proces, který čeká na jiném kanálu než je aktuální notifikace. Uživatelé ale začali notifikace používat i jinak, a reportovali výrazné zpomalení pokud větší množství klientů poslouchalo na větším množství různých kanálů.

Ve zprávě ke patchi je uvedeno zpomalení (pro 1 klienta 9100 TPS, pro 1000 klientů 200 TPS). Nová implementace ve sdílené paměti ke každému kanálu drží seznam čekajících klientů. V případě zprávy do některého kanálu se probudí pouze procesy, které na tomto kanálu poslouchají (i zde je vidět pokles TPS, ale výrazně menší – pro 1000 klientů 9000 TPS).

Výchozí komprimace TOASTu bude lz4 (namísto pglz). LZ4 je jako volba už cca 5 let, a měla by být efektivnější (jak v komprimaci, tak ve využití CPU) než původní LZ.

Postupně víc funkcí je akcelerováno pomocí SIMD. Nově to jsou funkce hex_encode, hex_decode a parsování csv, tsv formátů příkazu COPY (hledání speciálních znaků).

Některé extenze, jako např. pgstattuple, nově používají direct API pro čtení dat (implementované v pg18). Na systémech s rychlým IO by měly běžet rychleji.

Na základě reportů uživatelů se upravila implementace hashjoinu, tak aby nedocházelo k extrémnímu nárůstu alokace paměti v případech, kdy atribut použitý pro spojení relací obsahuje velké množství NULL hodnot.

V těch případech, kdy je garantováno, že porovnávané hodnoty jsou ne nullové, tak se operátor IS DISTICT FROM nahradí operátorem <>, případně IS NOT DISTINCT FROM operátorem =. Tady je otázkou, jaký bude reálný dopad. Jako programátor používám IS [ NOT ] DISTINCT FROM jedině v těch případech, které komplikují potenciální NULLy. Je ale pravdou, že drtivou většinu SQL, dnes generují ORM frameworky, ktere nemusí mít vždy dostatečnou chytrost. Výraz IS DISTINCT FROM NULL se nyní transformuje na IS NOT NULL, obdobně IS NOT DISTINCT FROM NULL na  IS NULL.

Replikace

Dlouhodobým cílem implementace logické replikace je podpora multimaster řešení. Zatím je k tomu ještě dlouhá cesta. Chybí podpora replikace DDL příkazů, plná podpora sekvencí, plná podpora řešení replikačních kolizí. Postupuje se v malých krocích. Letos je to podpora jednorázového refrešnutí sekvencí ALTER SUBSCRIPTION ... REFRESH SEQUENCES. S multimasterem to úplně nepomůže, ale může to zjednodušit upgrade skrze logickou replikaci.

Novým příkazem je příkaz WAIT FOR, který je možné použít na replikách, a který zajistí čekání na zreplikování zadané transakce. Lze nastavit timeout. Parametrem NO_THROW zajistí tiché (bez výjimky) ukončení příkazu po timeoutu (změní se pouze vrácená hodnota):

postgres=# WAIT FOR LSN '0/306EE20' WITH (TIMEOUT '100ms', NO_THROW);
 status
--------
 timeout

V příkazuCREATE PUBLICATION ... FOR ALL TABLES je nyní možné použít klauzuli EXCEPT TABLE (t1, ...) . KlauzuliEXCEPT TABLE lze použít i v příkazu ALTER PUBLICATION . Pro registraci odběru (CREATE SUBSCRIPTION ) lze místo explicitního connection stringu použít odkaz na dříve definovaný FDW server.

V pohledu pg_replication_slots je nový sloupec slotsync_skip_reason popisující důvod, proč nebylo možné synchronizovat repliku. V tom případě bude obsahovat jednu z následujících hodnot: wal_or_rows_removed, wal_not flushed, no_consistent_snapshot nebo  slot_invalidated.

Pokud při registraci odběru nastavíme parametr retain_dead_tuples na on (výchozí nastavení je off), budou se detekovat kolize update-delete, kdy nedojde k update z důvodu smazaného řádku. Pozor, tato volba dočasně blokuje vakuování zrušených řádků (zvlášť pokud je replika nedostupná a nedochází k synchronizaci). Lze nastavit timeout max_retention_duration, samozřejmě pak ovšem může dojít ke kolizi update-delete. Bez detekce této kolize může dojít k tiché nekonzistenci obsahu napříč různými zdroji.

Ostatní

Nově příkaz COPY podporuje formát JSON (pouze export).

\copy (SELECT * FROM obce LIMIT 10) TO STDOUT JSON
{"id":1,"okres_id":"CZ0100","nazev":"Praha","pocet_muzu":608316,"pocet_zen":640710,"vek_muzu":39.8,"vek_zen":43.2}
{"id":2,"okres_id":"CZ0201","nazev":"Benešov","pocet_muzu":7875,"pocet_zen":8507,"vek_muzu":39.2,"vek_zen":41.9}
{"id":3,"okres_id":"CZ0201","nazev":"Bernartice","pocet_muzu":108,"pocet_zen":115,"vek_muzu":45.9,"vek_zen":43.3}
{"id":4,"okres_id":"CZ0201","nazev":"Bílkovice","pocet_muzu":93,"pocet_zen":89,"vek_muzu":41.4,"vek_zen":46.8}
{"id":5,"okres_id":"CZ0201","nazev":"Blažejovice","pocet_muzu":52,"pocet_zen":48,"vek_muzu":44.6,"vek_zen":50.8}
{"id":6,"okres_id":"CZ0201","nazev":"Borovnice","pocet_muzu":39,"pocet_zen":37,"vek_muzu":45.6,"vek_zen":49.5}
{"id":7,"okres_id":"CZ0201","nazev":"Bukovany","pocet_muzu":364,"pocet_zen":372,"vek_muzu":38.1,"vek_zen":38.8}
{"id":8,"okres_id":"CZ0201","nazev":"Bystřice","pocet_muzu":2124,"pocet_zen":2096,"vek_muzu":38.5,"vek_zen":41.1}
{"id":9,"okres_id":"CZ0201","nazev":"Ctiboř","pocet_muzu":55,"pocet_zen":50,"vek_muzu":38.6,"vek_zen":42.0}
{"id":10,"okres_id":"CZ0201","nazev":"Čakov","pocet_muzu":65,"pocet_zen":60,"vek_muzu":36.4,"vek_zen":40.6}

\copy (SELECT * FROM obce LIMIT 10) TO STDOUT (FORMAT JSON, FORCE_ARRAY)
[
 {"id":1,"okres_id":"CZ0100","nazev":"Praha","pocet_muzu":608316,"pocet_zen":640710,"vek_muzu":39.8,"vek_zen":43.2}
,{"id":2,"okres_id":"CZ0201","nazev":"Benešov","pocet_muzu":7875,"pocet_zen":8507,"vek_muzu":39.2,"vek_zen":41.9}
,{"id":3,"okres_id":"CZ0201","nazev":"Bernartice","pocet_muzu":108,"pocet_zen":115,"vek_muzu":45.9,"vek_zen":43.3}
,{"id":4,"okres_id":"CZ0201","nazev":"Bílkovice","pocet_muzu":93,"pocet_zen":89,"vek_muzu":41.4,"vek_zen":46.8}
,{"id":5,"okres_id":"CZ0201","nazev":"Blažejovice","pocet_muzu":52,"pocet_zen":48,"vek_muzu":44.6,"vek_zen":50.8}
,{"id":6,"okres_id":"CZ0201","nazev":"Borovnice","pocet_muzu":39,"pocet_zen":37,"vek_muzu":45.6,"vek_zen":49.5}
,{"id":7,"okres_id":"CZ0201","nazev":"Bukovany","pocet_muzu":364,"pocet_zen":372,"vek_muzu":38.1,"vek_zen":38.8}
,{"id":8,"okres_id":"CZ0201","nazev":"Bystřice","pocet_muzu":2124,"pocet_zen":2096,"vek_muzu":38.5,"vek_zen":41.1}
,{"id":9,"okres_id":"CZ0201","nazev":"Ctiboř","pocet_muzu":55,"pocet_zen":50,"vek_muzu":38.6,"vek_zen":42.0}
,{"id":10,"okres_id":"CZ0201","nazev":"Čakov","pocet_muzu":65,"pocet_zen":60,"vek_muzu":36.4,"vek_zen":40.6}
]

Nyní také tento příkaz podporuje volbu ON_ERROR SET_NULL , která způsobí ignorování hodnot, na kterých selže typová konverze. To je možné díky tomu, že se již dříve zavedla podpora tzv měkkých chyb (soft errors). Tyto chyby lze ošetřit s řádově menší režií než běžné chyby (nejsou potřeba subtransakce). Podpora soft errors se v Postgresu objevila s podporou klauzuleON ERROR SQL/JSON funkcí. V plánu je podpora error safe přetypování (CAST expr AS type xxx DEFAULT expr ON CONVERSION ERROR ). Prvním nezbytným krokem je požadavek, aby konverzní funkce používaly měkké chyby. To teď v upstreamu je, a díky tomu, bylo i relativně jednoduché implementovatCOPY ON_ERROR SET_NULL (error safe casts by měly být v pg20).

CREATE TABLE foo(a int, b int);

COPY foo FROM stdin (FORMAT csv, ON_ERROR SET_NULL);
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> 10,20
>> 30,40
>> ahoj,20
>> \.
COPY 3

SELECT * FROM foo;
┌────┬────┐
│ a  │ b  │
╞════╪════╡
│ 10 │ 20 │
│ 30 │ 40 │
│  ∅ │ 20 │
└────┴────┘
(3 rows)

Nově také příkazCOPY umožňuje číst přímo partišny (partitions). V předchozích verzích bylo nutné použít syntax s vloženým selectem, což má nějakou režii.

Funkce pro zakódování a dekódování textu ( encode a decode) nově podporují kódování base64url a base32hex:

SELECT encode('Ahoj<>/+', 'hex');
┌──────────────────┐
│      encode      │
╞══════════════════╡
│ 41686f6a3c3e2f2b │
└──────────────────┘
(1 row)

SELECT encode('Ahoj<>/+', 'base64url');
┌─────────────┐
│   encode    │
╞═════════════╡
│ QWhvajw-Lys │
└─────────────┘
(1 row)

SELECT encode('Ahoj<>/+', 'base32hex');
┌──────────────────┐
│      encode      │
╞══════════════════╡
│ 85K6UQHS7ONIM=== │
└──────────────────┘
(1 row)

Dokončit implementaci standardu SQL/JSON, konkrétně implementaci funkceJSON_TABLE se opět nestihlo.

V psql  bude možnost si nastavit zobrazení logických hodnot true a false. Datový typ boolean akceptuje literály true, false, t, f a on, off. Na výstupu se zobrazuje pouze t nebo f:

SELECT 1=1;
┌──────────┐
│ ?column? │
╞══════════╡
│ t        │
└──────────┘
(1 row)

\pset display_true true
Boolean true display is "true".
\pset display_false false
Boolean false display is "false".

SELECT 1=1;
┌──────────┐
│ ?column? │
╞══════════╡
│ true     │
└──────────┘
(1 row)

SELECT 1 <> 1;
┌──────────┐
│ ?column? │
╞══════════╡
│ false    │
└──────────┘
(1 row)
Může se hodit možnost si zobrazit search_path v promptu. V promptu je také možnost použít symbol %i, který se nahradí replikačním statusem ( primary nebo  standby).

PL/pgSQL

PL/pgSQL je relativě stabilizované prostředí, kde je každoročně jen minimum změn. Letos je to jedna malá (a pro většinu uživatelů zanedbatelná změna), a jedna „velká“, která ale interně žádnou změnou není.

Malou změnou je interní optimalizace exekuce embedded SQL ve tvaru SELECT výraz INTO proměnná. Mám pocit, že tento zápis se používal v Sybase a na Sybase navazujících databázích pro inicializaci proměnných. Já jsem se s tímto zápisem setkával v literatuře někdy kolem roku 2000, a už tehdy platilo doporučení tento zápis nepoužívat (v plpgsql se nikdy moc nepoužíval). V posledních letech přišlo víc uživatelů z MSSQL (začalo se migrovat víc aplikací z tohoto serveru), tento zápis používali a zjistili, že tento zápis je cca 20× pomalejší než „nativní“ přiřazení  proměnna := výraz.

Interně jsou v PL/pgSQL dvě metody jak vyhodnotit výraz. Tzv simple metoda, kde se přeskočí SQL exekutor a přímo zavolá exekutor výrazů, je výrazně rychlejší. Druhou metodou je běžná exekuce skrze SQL exekutor (a SPI API). Až doposud, cokoliv co v zdrojovém kódu funkce bylo SQL příkazem (zde příkazem SELECT) se vykonalo běžnou cestou. Nově se detekuje výše zmíněný zápis a vykoná se simple metodou. Pro dlouholeté uživatele Postgresu to nic neznamená, ale lidem, kteří migrují z MSSQL, to může zpříjemnit život.

Druhou, a naopak, velkou změnou je možnost inliningu funkce, která je napsaná v PL/pgSQL (ve speciálních případech). Inlining v kontextu Postgresu je trochu něco jiného než si uživatele představí pod termínem inlining. V Postgresu mohou být funkce volané z nějakého SQL příkazu – typicky příkazu SELECT. Pokud nějaká funkce podporuje inlining, tak to znamená, že v AST stromu dotazu se volání funkce nahradí jejím tělem. Teprve pak nastupuje optimalizace, a díky inliningu vlastně optimalizátor vidí dovnitř funkce (jinak je funkce pro optimalizátor black box).

K tomu, aby PL/pgSQL (nejen v PL/pgSQL) funkce mohla být inlinovaná, potřebujete vlastní kód v Cčkové extenzi. A je to vlastně trik (v tomto případě dost nestydatý podfuk :-)). Už dříve bylo možné napsat extenze, které dodávaly optimalizátoru znalost o vybraných funkcích (jinak, opět, pro optimalizátor, jsou funkce black box). Přímo v jádru je například podpora optimalizátoru pro funkci generate_series (staré verze pg by v rows měly 1000).

EXPLAIN SELECT * FROM generate_series(1,3);
┌────────────────────────────────────────────────────────────────────┐
│                             QUERY PLAN                             │
╞════════════════════════════════════════════════════════════════════╡
│ Function Scan on generate_series  (cost=0.00..0.03 rows=3 width=4) │
└────────────────────────────────────────────────────────────────────┘
(1 row)

Inlining byl dříve podporován pro funkce v jazyku SQL, kdy si planner vytáhl zdrojový kód funkce. V devatenáctce planner může zavolat extenzi, a může si říct nejen o detailnější parametry ohledně exekuce, ale může si říct o substituční SQL. Když ho dostane, tak funkci dále neřeší, a pracuje se substitučním SQL příkazem. Je to docela magie, nicméně efektivní. Kód extenze se volá až v momentu, kdy známe konstantní parametry funkce, a extenze se může rozhodnout – zavolej volanou funkci, nebo zavolej nějaké SQL, zavolej jinou funkci, případně vrať rovnou nějakou speciální hodnotu NULL , 0,…

Je to i způsob, jak opravdu efektivně pracovat s dynamickým SQL. Musíte si ovšem napsat kód v Cčkové extenzi. Požadavek na tuto funkcionalitu přišel od vývojářů PostGISu. Cílem jsou optimalizace v PostGISu, dá se to ale použít i jinde. Časem si dovedu představit i nějakou contrib extenzi, která by tuto možnost zpřístupnila i bez nutnosti psát si vlastní extenzi. Škoda, že nemám autoritu vývojářů PostGISu. Dovedl bych si představit podobný trik v analyzátoru. Dost by mi to pomohlo s Orafce. V té pozici samozřejmě nejsem.

Vývoj

Každoročně dochází k čištění kódu. Občas se zruší podpora některé platformy. Výjimečně se přidá podpora nové platformy nebo jako v případě pg19 se vrátí podpora dříve odstraněné platformy (AIX – operační systém AIX přestal být podporovaný v pg17 díky nezájmu IBM. Pak se ale v IBM chytli za nos, dodali počítač do testovací farmy, a do AIXu přidali aktualizovaný software potřebný k buildu Postgresu). Další čistky v kódu lze provést přechodem na novější verzi programovacího jazyka – přechodem z C99 na C11 (bohužel to může znamenat, že starých platformách se nové verze Postgresu nepřeloží).

Interní datový typ Datum až do předchozí verze byl 4bajtový na 32bitových platformách a 8bajtový na 64bitových platformách. Nově je 8bajtový na všech podporovaných platformách. Určitě nezanedbatelná část kódu se generuje z definičních souborů. Nově se z definic generuje kód definující konfigurační proměnné. Údržba, verzování, zajištění konzistence definičních souborů je násobně jednodušší než údržba adekvátního kódu v Cčku. Dynamická alokace paměti bude jednodušší díky funkcím GetNamedDSA a GetNamedDSMSegment. Dynamicky sdílenou paměť lze sledovat skrze pohled  pg_dsm_registry_allocations.

Mám pocit, že vývoj Postgresu ještě víc zrychlil – je vidět mnohem víc patchů od lidí z Asie (dříve to bylo spíš jenom Japonsko), a o to víc se naráží na kapacitu committerů. Je to paradox, který nejde jednoduše řešit. Postgres je populární díky funkcionalitě a stabilitě. Stabilita je díky pečlivé práci commitetterů (důkladně se řeší každý řádek kódu). Díky popularitě je více programátorů, kteří píší více kódu, více patchů, ale committeři nestíhají, a tak patche zůstávají relativně dlouho ve frontě. Což frustruje programátory.

Patch napíšete během měsíce, během měsíce se dá udělat finalizace a pár iterací s committerem, ale mezitím několik měsíců (v horším případě let (tři roky i v případě bezproblémového malého patche), čekáte, až si na vás některý z committerů udělá čas. Celou dobu musíte udržovat patch v aplikovatelném stavu, musíte reagovat na případné diskuze, musíte sledovat vývoj a případně aktualizovat patch. Je to problém o kterém se ví, diskutuje se o něm, ale není úplně jasné, co by mělo být řešení. Minimálně se zlepšila evidence patchů, která pokrývá i automatické testování na několika různých platformách. Tady je velká klika, že Postgres má extenze.

Neskutečně velký objem kódu (funkcionality) je dnes možné řešit skrz extenze mimo komunitu – což výrazně snižuje tlak na committery. V extenzích toho jde udělat hodně, bohužel, ne všechno. Na druhou stranu, extenze také nejsou bez rizika. Určitým způsobem zvyšují fragmentaci ekosystému Postgresu, který pak ztrácí přehlednost (plus ne všechny extenze jsou dostupné v cloudu). Zase, díky tomu je jádro Postgresu stále ještě rozumně velké. O Oracle kolují zvěsti, že jádro má 14 miliónů řádek, Postgres pod 2 milióny. Náklady na údržbu Postgresu jsou výrazně menší. Kompilace zdrojáků je do 5 minut, regresní testy v největším rozsahu běží 15 minut. S tím ještě lze nějak rozumně pracovat. O Oracle jsem slyšel, že regresní testy běží den.

Školení Linux

Popularita Postgresu jde aktuálně až do absurdna. Z Linkedinu mám pocit, že začíná být přepostgresováno (samozřejmě z mé perspektivy a v mé bublině). Hromada příspěvků vygenerovaných AI jsou banality na úrovni žáka prvního stupně (Postgres má VACUUM, indexy jsou super, dotazy mohou být pomalé, atd atd). Doporučení typu – proč ne Redis, proč ne Elastic, na všechno použijte Postgres. Viděl jsem hromadu clickbaitových článků typu – multigenerační architektura Postgresu je tragická, ale s naší AI nebo s naším supportem nebudete mít problém, atd atd. Postgresu dnes rozumí každý, kdo dokáže do AI napsat Postgres. Měl jsem tu možnost vidět naprosto neadekvátní doporučení ohledně konfigurace (dnes nevíte jestli důvodem je neznalost autora nebo naopak jeho vychytralost a snaha o co nejvíc komentářů případně o troling).

Historicky popularita Postgresu rostla ve stínu MySQL, Monga a dalších databází a roky byl Postgres ušetřený mediálního balastu. To bohužel už není pravda. Pořád platí, že Postgres je dobrá databáze, stabilní, relativně rychlá, která se ale rozhodně nehodí na vše. Má svoje limity, které je dobré znát, a rozhodně nemůže ve všech ohledech (ve větším rozsahu) nahradit Redis, MySQL nebo Mongo případně Kafku. To je nesmysl, a není to ani cílem komunity. Základem jakékoliv práce je znát limity, chování, a správně v souladu s celkem, používat správné nástroje.

Autor článku

Pavel Stěhule je odborníkem na relační databázový systém PostgreSQL, pracuje jako školitel a konzultant.



Nejnovější články