PostgreSQL 17 (2024)

Z PostgreSQL
Skočit na navigaci Skočit na vyhledávání

Autor: Pavel Stěhule, 2023

PostgreSQL 17 (2024)

Už jsem to psal posledně, tím jak na Postgresu dělá víc a víc vývojářů z různých společností a zemí (firem), tak je vývoj Postgresu méně a méně čitelný a prakticky se nedá najít viditelnější trend. Velkou výhodou projektu jsou jasně nastavená, fungující a respektovaná pravidla, plus stabilní komunita vývojářů. Díky tomu se během roku udělá hromada práce, která je vidět, a kterou ocení (oceňují) i uživatelé.

Bohužel letos komunita o jednoho dlouholetého člena přišla. 26. 3. tragicky zahynul Simon Riggs (při pilotáži malého letadla). Simona jsme mohli několikrát potkat i v Praze na konferencích P2D2, PGConf.Eu a i jindy. Firma, kterou v roce 2001 založil - 2ndQuadrant, měla v Praze pobočku. Simon byl fajn člověk, výborný řečník, ale i programátor. Hodně posunul Postgres směrem k nasazení ve firemních kritických aplikacích - napsal podporu pro Point In Time Recovery, Hot Standby, synchronní replikaci (vývojáři ze 2ndQuadrantu poté napsali podporu logické replikace do Postgresu).

Tento vývojový cyklus byl bez jakýchkoliv kontroverzí. Je vidět snaha o dotáhnutí velkých patchů (jako např. SQL/JSON), a zároveň si nepodrazit zbytečně nohy rychlou integrací nového kódu. Neotevírají se nová velké témata (v rámci komunitního Postgresu). Výjimkou byl návrh Heikkiho Linnakangase na přechod z architektury postavené na procesech na architekturu používající vlákna. To je dost kontroverzní téma, které mělo přijít na přetřes před 10 roky. Architekturu Postgres přebírá ještě z databáze INGRES, což znamená, že je pomalu 50 let stará. V Postgresu je proces spojen s konkrétní databází a s konkrétním přihlášeným uživatelem. Bez ukončení procesu (a vytvoření nového) nejde změnit uživatele, nejde změnit databázi. Když máte intenzivně využívané aplikace se vyššími stovkami uživatelů, připojujících se do vyšších stovek databází, tak s Postgresem můžete mít problém. Řešení by mohla přinést změna architektury (využívání vláken). Před implementací podpory využití více CPU pro jeden dotaz by ale tento přechod byl o dost jednodušší, a také by se asi dost ušetřilo práce při implementaci komunikace mezi procesy workerů dotazu. Myslím si, že Postgres na vlákna přejde, ale bude to trvat roky. Problém není vlastní implementace (existují dva prototypy). Problém je, jak tuto velkou změnu rozložit v čase, a zároveň každý rok vydat verzi v očekávané kvalitě (a zároveň aby byl Postgres funkční a stabilní na všech systémech, které jsou podporovány).

Heikkiho iniciativa pravděpodobně souvisí s projektem Neon, v jehož týmu pracuje, a který spolu založil. Jde o open source velmi zajímavou alternativu Amazoního Aurora Postgresu. Jedná se o fork PostgreSQL upravený pro běh v cloudu. Oproti běžnému Postgresu, kde server řeší jak manipulaci s daty a správu cache (storage), tak vykonávaní dotazů (compute), jsou v Neonu storage a compute striktně odděleny, a běží v nezávislých VM. Management compute nody pak může být výrazně dynamičtější než s klasickým Postgresem (protože tam není ta část, která se stará o storage). Pro storage se používá speciální cloud multi-tenant storage systém, který podporuje i zálohování a archivaci.

Datové typy

Konstantu infinity lze nově použít pro datový typ interval. To se hodí pro implementaci různých rozsahů, kde infinity je praktičtější než NULL (není nutné v podmínkách používat COALESCE), a čitelnější než je speciální konstanty:

(2024-03-25 06:23:17) postgres=# SELECT timestamp 'infinity' - timestamp '1995-08-06 12:12:12';
┌──────────┐
│ ?column? │
╞══════════╡
│ infinity │
└──────────┘
(1 row)

(2024-03-25 06:23:24) postgres=# SELECT timestamp '-infinity' - timestamp '1995-08-06 12:12:12';
┌───────────┐
│ ?column?  │
╞═══════════╡
│ -infinity │
└───────────┘
(1 row)

Jednou z posledních chybějících funkcí standardu SQL/XML byla funkce xmltext. Tato funkce řeší escapeování textů. Tím je podpora SQL/XML v Postgresu rámcově dokončena. Bohužel vývoj knihovny libxml2 je zmrazen (nad touto knihovnou je podpora SQL/XML v Postgresu postavena), a tudíž se do této knihovny (stejně tak do Postgresu) nedostane podpora XPath vyšších verzí než 1.0. Což je limit podpory SQL/XML v pg. Plná podpora by byla samozřejmě lepší, na druhou stranu pro práci s XML v databázi je XPath 1.0 víc než dostačující, a PostgreSQL rozhodně nemá ambice konkurovat tzv XML databázím:

postgres=# SELECT xmltext('foo & <"bar">');
┌───────────────────────────────────┐
│              xmltext              │
╞═══════════════════════════════════╡
│ foo &amp; &lt;&quot;bar&quot;&gt; │
└───────────────────────────────────┘
(1 row)

postgres=# SELECT xmltext('x'|| '<P>73</P>'::xml || .42 || true || 'j'::char);
┌─────────────────────────────────┐
│             xmltext             │
╞═════════════════════════════════╡
│ x&lt;P&gt;73&lt;/P&gt;0.42truej │
└─────────────────────────────────┘
(1 row)

Podpora SQL/JSON

Ve výrazech v JSONPath lze nyní používat konverzní metody: .bigint(), .boolean(), .date(), .decimal([precision [, scale]]), .integer(), .number(), .string(), .time(), .time_tz(), .timestamp(), a .timestamp_tz():

postgres=# SELECT jsonb_path_query('"2023-08-15"', '$.time_tz()');
ERROR:  time_tz format is not recognized: "2023-08-15"
postgres=# SELECT jsonb_path_query('"12:34:56 +05:30"', '$.time_tz()');
┌──────────────────┐
│ jsonb_path_query │
╞══════════════════╡
│ "12:34:56+05:30" │
└──────────────────┘
(1 row)

Postupně se dokončuje podpora standardu SQL/JSON. K dispozici jsou tři nové funkce JSON, JSON_SCALAR a JSON_SERIALIZE. Praktický přínos těchto funkcí mi možná uniká (jedná se o jiný zápis přetypování), nicméně z důvodu úplnosti implementace standardu jsou k dispozici. V některých prostředích může být zajímavé protlacit JSON jako bytea z důvodu eliminace escapeování.

postgres=# SELECT JSON_SERIALIZE('{"a" : 10}' RETURNING bytea);
┌────────────────────────┐
│     json_serialize     │
╞════════════════════════╡
│ \x7b226122203a2031307d │
└────────────────────────┘
(1 row)

postgres=# SELECT JSON(JSON_SERIALIZE('{"a" : 10}' RETURNING bytea));
┌────────────┐
│    json    │
╞════════════╡
│ {"a" : 10} │
└────────────┘
(1 row)

Jako důležitější mi přijdou funkce JSON_EXISTS, JSON_QUERY a JSON_VALUE. Tyto funkce jsou standardním API pro práci s JSONPath výrazy.

Parametrem dotazovacích funkcí je JSON hodnota a JSONPath výraz. JSON_EXISTS vrací true, pokud výraz vrací alespoň jednu hodnotu. json_value vrací přesně jednu hodnotu, JSON_QUERY vrací objekt nebo pole. Dalšími klauzulemi lze reagovat na prázdný výsledek nebo chybu:

postgres=# SELECT JSON_EXISTS(jsonb '{"a": 1, "b": 2}', '$.* ? (@ > $x && @ < $y)' PASSING 0 AS x, 2 AS y);
┌─────────────┐
│ json_exists │
╞═════════════╡
│ t           │
└─────────────┘
(1 row)

postgres=# SELECT JSON_VALUE(jsonb '"ahoj"', '$' RETURNING text);
┌────────────┐
│ json_value │
╞════════════╡
│ ahoj       │
└────────────┘
(1 row)

postgres=# SELECT JSON_VALUE(jsonb '"2017-02-20"', '$' RETURNING date) + 9;
┌────────────┐
│  ?column?  │
╞════════════╡
│ 2017-03-01 │
└────────────┘
(1 row)

postgres=# SELECT JSON_VALUE(jsonb '{"a": 1}', '$.a' RETURNING int);
┌────────────┐
│ json_value │
╞════════════╡
│          1 │
└────────────┘
(1 row)

postgres=# SELECT JSON_VALUE(jsonb '{"a": 1}', '$.b' RETURNING int);
┌────────────┐
│ json_value │
╞════════════╡
│            │
└────────────┘
(1 row)

postgres=# SELECT JSON_VALUE(jsonb '{"a": 1}', '$.b' RETURNING int DEFAULT 10 ON EMPTY);
┌────────────┐
│ json_value │
╞════════════╡
│         10 │
└────────────┘
(1 row)

postgres=# SELECT JSON_VALUE(jsonb '{"a": 1}', '$.b' RETURNING int ERROR ON EMPTY ERROR ON ERROR);
ERROR:  no SQL/JSON item

Syntaxe těchto funkcí je dost košatá. Je to vidět u funkce JSON_QUERY a tuplem pak u JSON_TABLE:

postgres=# SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text);
┌────────────┐
│ json_query │
╞════════════╡
│ "aaa"      │
└────────────┘
(1 row)

postgres=# SELECT JSON_QUERY(jsonb '"aaa"', '$' RETURNING text OMIT QUOTES);
┌────────────┐
│ json_query │
╞════════════╡
│ aaa        │
└────────────┘
(1 row)

postgres=# SELECT JSON_QUERY(jsonb '[{"a":10, "b": 20}, {"a": 30, "b":100}]', '$[*].a' );
┌────────────┐
│ json_query │
╞════════════╡
│            │
└────────────┘
(1 row)

postgres=# SELECT JSON_QUERY(jsonb '[{"a":10, "b": 20}, {"a": 30, "b":100}]', '$[*].a' ERROR ON ERROR );
ERROR:  JSON path expression in JSON_QUERY should return singleton item without wrapper
HINT:  use WITH WRAPPER clause to wrap SQL/JSON item sequence into array

postgres=# SELECT JSON_QUERY(jsonb '[{"a":10, "b": 20}, {"a": 30, "b":100}]', '$[*].a' WITH WRAPPER );
┌────────────┐
│ json_query │
╞════════════╡
│ [10, 30]   │
└────────────┘
(1 row)

Pokud JSONPath výraz vrací více než jednu hodnotu, tak výsledkem je chyba, která je ale by default ignorována (v laxním režimu). Pro vytvoření pole z vrácených hodnot se musí použít klauzule WITH WRAPPER (zkrácená forma pro WITH ARRAY WRAPPER). V tuto chvíli návratovou hodnotou může být pouze JSON pole, nikoliv pole v PostgreSQL.

Konečně se do upstreamu dostává i funkce JSON_TABLE:

CREATE TABLE my_films ( js jsonb );
INSERT INTO my_films VALUES (
'{ "favorites" : [
   { "kind" : "comedy", "films" : [
     { "title" : "Bananas",
       "director" : "Woody Allen"},
     { "title" : "The Dinner Game",
       "director" : "Francis Veber" } ] },
   { "kind" : "horror", "films" : [
     { "title" : "Psycho",
       "director" : "Alfred Hitchcock" } ] },
   { "kind" : "thriller", "films" : [
     { "title" : "Vertigo",
       "director" : "Alfred Hitchcock" } ] },
   { "kind" : "drama", "films" : [
     { "title" : "Yojimbo",
       "director" : "Akira Kurosawa" } ] }
  ] }');

SELECT jt.* FROM my_films,
                 JSON_TABLE (js, '$.favorites[*]'
                            COLUMNS (id FOR ORDINALITY,
                                     kind text PATH '$.kind',
                                     title text PATH '$.films[*].title' WITH WRAPPER,
                                     director text PATH '$.films[*].director' WITH WRAPPER)) AS jt;
┌────┬──────────┬────────────────────────────────┬──────────────────────────────────┐
│ id │   kind   │             title              │             director             │
╞════╪══════════╪════════════════════════════════╪══════════════════════════════════╡
│  1 │ comedy   │ ["Bananas", "The Dinner Game"] │ ["Woody Allen", "Francis Veber"] │
│  2 │ horror   │ ["Psycho"]                     │ ["Alfred Hitchcock"]             │
│  3 │ thriller │ ["Vertigo"]                    │ ["Alfred Hitchcock"]             │
│  4 │ drama    │ ["Yojimbo"]                    │ ["Akira Kurosawa"]               │
└────┴──────────┴────────────────────────────────┴──────────────────────────────────┘
(4 rows)

Atributy zanořených objektů lze získat použitím klauzule NESTED PATH:

SELECT jt.* FROM my_films,
                 JSON_TABLE ( js,
                             '$.favorites[*] ? (@.films[*].director == $filter)'
                               PASSING 'Alfred Hitchcock' AS filter
                               COLUMNS (id FOR ORDINALITY,
                                        kind text PATH '$.kind',
                                        NESTED PATH '$.films[*]'
                                          COLUMNS (title text FORMAT JSON PATH '$.title' OMIT QUOTES,
                                                   director text PATH '$.director' KEEP QUOTES))) AS jt;
                                                   
┌────┬──────────┬─────────┬────────────────────┐
│ id │   kind   │  title  │      director      │
╞════╪══════════╪═════════╪════════════════════╡
│  1 │ horror   │ Psycho  │ "Alfred Hitchcock" │
│  2 │ thriller │ Vertigo │ "Alfred Hitchcock" │
└────┴──────────┴─────────┴────────────────────┘
(2 rows)

JSON_TABLE je analogická funkci XMLTABLE. Bohužel, syntax i chování se někdy málo, někdy docela dost liší. Implementace SQL/JSON ještě není úplná - chybí podpora tzv. plánů, ale ta se téměř 100% dostane do PostgreSQL 18. Už nyní je PostgreSQL databáze s velice dobrou podporou standardu. Po dokončení implementace JSON_TABLE by implementace standardu SQL/JSON v PostgreSQL měla být nejúplnější ze všech známých SQL databází.

Pro skalární datové typy stávající implementace funguje perfektně. Co není dořešeno, a co nefunguje úplně dobře, je například konverze z JSON pole do PostgreSQL pole. Mám pocit, že to není ani pokryté standardem, ale určitě to bude potřeba v budoucnu řešit. Drobnou vadou na kráse (standardu ANSI/SQL) je skutečnost, že původně JSON nebyl navržen jako samostatný datový typ. Byl to jen řetězec (uložený například ve varcharu). Díky tomu implicitní konverze v SQL/JSON funkcích postrádají určitou logiku, a například plnou konverzi do "normálního" řetězce si musíme vynutit speciální klauzulí OMIT QUOTES. PostgreSQL má datový typ pro JSON, nejnovější aktualizace ANSI/SQL také. Nicméně z důvodu zachování zpětné kompatibility originální koncept návrhu SQL/JSON funkcí zůstává (a z mého pohledu zbytečně komplikuje (v detailech) práci).

SQL

DDL

Nově lze upravit výraz generovaného sloupce (přidáno v Postgresu 12) příkazem ALTER TABLE ALTER COLUMN SET EXPRESSION. Pozor, příkaz způsobí přepis tabulky (na velkých tabulkách bude pomalý - bohužel Postgres nepodporuje virtuální (nematerializované) generované (počítané) sloupce).

DML

Příkazem MERGE lze měnit i modifikovatelné (updatable) pohledy. Nově také lze v příkazu MERGE použít klauzuli RETURNING. V ní lze použít funkci merge_action(), která vrací řetězec INSERT, UPDATE, nebo DELETE v závislosti jakým způsobem byl řádek upraven:

CREATE TABLE target(a int);
INSERT INTO target VALUES(10);

postgres=# MERGE INTO target
             USING (VALUES(10),(20)) s(b)
             ON a = b 
             WHEN MATCHED THEN
                  UPDATE SET a = b
             WHEN NOT MATCHED THEN
                  INSERT VALUES(b)
             RETURNING merge_action(), target.*;
┌──────────────┬────┐
│ merge_action │ a  │
╞══════════════╪════╡
│ UPDATE       │ 10 │
│ INSERT       │ 20 │
└──────────────┴────┘
(2 rows)

Počínaje verzí 17 můžeme použít podmínku WHEN NOT MATCHED BY SOURCE THEN a jako reakci použít DELETE nebo DO NOTHING:

TRUNCATE target;
INSERT INTO target VALUES(30);
INSERT INTO target VALUES(10);
postgres=# MERGE INTO target
             USING (VALUES(10),(20)) s(b)
             ON a = b
             WHEN MATCHED THEN
                  UPDATE SET a = b
             WHEN NOT MATCHED THEN
                  INSERT VALUES(b)
             WHEN NOT MATCHED BY SOURCE THEN
                  DELETE;
MERGE 3
postgres=# SELECT * FROM target;
┌────┐
│ a  │
╞════╡
│ 10 │
│ 20 │
└────┘
(2 rows)

V parametrech FORCE_NULL a FORCE_NOT_NULL příkazu COPY můžeme použít * ve smyslu použití na všechny sloupce. Nový parametr příkazu COPY ON_ERROR s volbou ignore umožňuje ignorovat chyby formátu při importu (ignoruje se celý řádek s nevalidní hodnotou):

CREATE TABLE check_ign_err (n int, m int[], k int);

postgres=# COPY check_ign_err FROM STDIN WITH (ON_ERROR ignore);
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.
>> 1, {10}, 10
>> 2, bbbb, uuu
>> \.
NOTICE:  2 rows were skipped due to data type incompatibility
COPY 0

S parametrem LOG_VERBOSITY verbose zobrazí podrobnější gnorovaný řádek:

(2024-04-03 14:33:01) postgres=# COPY check_ign_err FROM STDIN WITH (ON_ERROR ignore, LOG_VERBOSITY verbose);
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.
>> 1, {10}, 20
>> 1, {10}, zzz
>> sjsj,sksk, ksksk
>> \.
NOTICE:  skipping row due to data type incompatibility at line 1 for column n: "1, {10}, 20"
NOTICE:  skipping row due to data type incompatibility at line 2 for column n: "1, {10}, zzz"
NOTICE:  skipping row due to data type incompatibility at line 3 for column n: "sjsj,sksk, ksksk"
NOTICE:  3 rows were skipped due to data type incompatibility
COPY 0

Stávající implementace je jen vykopnutí tolerantního importu, který je často k dispozici v OLAP databázích.

SELECT

Klauzule AT LOCAL je zkratka AT TIME ZONE aktuální časová zóna session

postgres=# SELECT CURRENT_TIMESTAMP, CURRENT_TIMESTAMP AT LOCAL;
┌───────────────────────────────┬────────────────────────────┐
│       current_timestamp       │          timezone          │
╞═══════════════════════════════╪════════════════════════════╡
│ 2024-04-01 10:00:36.969875+02 │ 2024-04-01 10:00:36.969875 │
└───────────────────────────────┴────────────────────────────┘
(1 row)

postgres=# SELECT timestamp '2001-02-16 20:38:40' AT LOCAL, timestamp '2001-02-16 20:38:40';
┌────────────────────────┬─────────────────────┐
│        timezone        │      timestamp      │
╞════════════════════════╪═════════════════════╡
│ 2001-02-16 20:38:40+01 │ 2001-02-16 20:38:40 │
└────────────────────────┴─────────────────────┘
(1 row)

Optimalizace

Optimalizace dotazů

Implementace UNIONu bude o něco chytřejší - kromě operace append bude možné použít i operaci merge append (pokud jsou vstupní data již seřazená, tak není nutný další sort a unique). Planner nyní vidí statistiky zanořené CTE. Inkrementální řazení (implementováno v PostgreSQL 13) je možné použít i vůči GiST a SP-GiST indexům. To může být užitečné pro KNN dotazy, kde se řadí i podle neoindexovaného sloupce. V případě více sloupcové klauzule GROUP BY optimalizátor vyzkouší více kombinací řazení, tak aby minimalizoval jeho náročnost (aby bylo možné použít inkrementální řazení).

Postgres nyní podporuje tzv index skip scan. To znamená, že pokud v indexu vyhledáváte seznam hodnot, tak oproti dřívější implementaci, která n krát vyhledávala konkrétní hodnotu, a n krát opakovala traverzování napříč indexem. Nově se dohledaných listech (koncové uzly indexu) zkouší dohledat další hodnoty ze seznamu.

V nové verzi je vylepšena podpora operace hledání položky v poli indexem. V předchozích verzích nebylo možné tuto operaci kombinovat s jinými operacemi (při hledávání v indexu), a a tak se přesouvala do filtru.

-- PostgreSQL 16
(2024-04-07 07:35:40) postgres=# explain select * from foo3 where b = any(array[10,20,30]) and a < 100;
┌───────────────────────────────────────────────────────────────────────────────┐
│                                  QUERY PLAN                                   │
╞═══════════════════════════════════════════════════════════════════════════════╡
│ Index Only Scan using foo3_a_b_idx on foo3  (cost=0.42..33.44 rows=1 width=8) │
│   Index Cond: (a < 100)                                                       │
│   Filter: (b = ANY ('{10,20,30}'::integer[]))                                 │
└───────────────────────────────────────────────────────────────────────────────┘
(3 rows)

-- PostgreSQL 17
postgres=# explain select * from foo3 where b = any(array[10,20,30]) and a < 100;
┌───────────────────────────────────────────────────────────────────────────────┐
│                                  QUERY PLAN                                   │
╞═══════════════════════════════════════════════════════════════════════════════╡
│ Index Only Scan using foo3_a_b_idx on foo3  (cost=0.42..21.99 rows=1 width=8) │
│   Index Cond: ((a < 100) AND (b = ANY ('{10,20,30}'::integer[])))             │
└───────────────────────────────────────────────────────────────────────────────┘
(2 rows)

Operátory <@ a @> pro konstantní range jsou transformovány na operace porovnání:

postgres=# EXPLAIN (VERBOSE, COSTS OFF)
SELECT daterange('2002-09-25'::date, 'Infinity', '[]') @> current_date
postgres-# ;
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│                                       QUERY PLAN                                        │
╞═════════════════════════════════════════════════════════════════════════════════════════╡
│ Result                                                                                  │
│   Output: ((CURRENT_DATE >= '2002-09-25'::date) AND (CURRENT_DATE <= 'infinity'::date)) │
└─────────────────────────────────────────────────────────────────────────────────────────┘
(2 rows)

Optimalizace práce s pamětí

Jedna z důležitých interních struktur je tzv ResourceOwner. Pomocí sad proměnných tohoto typu se sleduje použití a zejména uvolnění paměti, zámků v Postgresu. Pokud je resource owner ukončen, a nějaký zdroj v jeho evidenci není uvolněn, tak se vypíše varovaní. Ve verzi 17 byla práce s resource ownery refaktorována, tak aby bylo možné snáze rozšiřovat evidované zdroje a aby tuto strukturu mohly používat extenze i pro vlastní zdroje.

Funkce BumpContextCreate() vytvoří nový paměťový kontext typu "Bump". Jestli by to bylo možné přeložit jako "redukovaný". Tento kontext podporuje pouze alokaci, a nepodporuje uvolnění paměti jiným způsobem než zrušením kontextu. S ukazatelem na paměť z kontextu tohoto typu nelze zavolat funkci pfree nebo repalloc. Díky "redukci" je alokace paměti efektivnější (odpadá hlavička) a rychlejší (jelikož se nezkouší hledat v seznamu uvolněné paměti (freelist)). Pro některé úlohy jako řazení je ideální - a také řazení v PostgreSQL 17 tento typ paměťového kontextu používá.

Optimalizace (možnost konfigurace velikosti) SLRU cache

V SLRU se ukládají perzistentní provozní data - id transakcí, id subtransakcí, id rodičovských transakcí, notifikace. Tato data jsou perzistentní. Ukládají se na disk. Z důvodu výkonu se naposledy použitá ještě drží v dedikované cache ve sdílené paměti. V některých případech (např. extrémně zatížená aplikace generuje delší transakce s velkým počtem subtransakcí (časté volání příkazu SAVEPOINT)) se ukázalo, že tato cache velikostně nestačí, a častou invalidací dat z cache se hodně ztrácí výkon (a hodně zatěžuje CPU).

Historicky ten problém byl ještě o to větší, že byl obtížně detekovatelný. Počínaje Postgresem 13 máme k dispozici statistiku v pohledu pg_stat_slru, takže nějaké problémy se dají celkem snadno detekovat (ale nedaly se řešit na úrovni databáze):

(2024-03-28 08:06:22) postgres=# SELECT * FROM pg_stat_slru ;
┌──────────────────┬─────────────┬──────────┬───────────┬──────────────┬─────────────┬─────────┬───────────┬───────────────────────────────┐
│       name       │ blks_zeroed │ blks_hit │ blks_read │ blks_written │ blks_exists │ flushes │ truncates │          stats_reset          │
╞══════════════════╪═════════════╪══════════╪═══════════╪══════════════╪═════════════╪═════════╪═══════════╪═══════════════════════════════╡
│ commit_timestamp │           0 │        0 │         0 │            0 │           0 │       6 │         0 │ 2024-03-28 06:01:46.854646+01 │
│ multixact_member │           1 │        0 │         0 │            1 │           0 │       6 │         0 │ 2024-03-28 06:01:46.854646+01 │
│ multixact_offset │           1 │        1 │         2 │            3 │           0 │       6 │         0 │ 2024-03-28 06:01:46.854646+01 │
│ notify           │           0 │        0 │         0 │            0 │           0 │       0 │         0 │ 2024-03-28 06:01:46.854646+01 │
│ serializable     │           0 │        0 │         0 │            0 │           0 │       0 │         0 │ 2024-03-28 06:01:46.854646+01 │
│ subtransaction   │           5 │        0 │         0 │            3 │           0 │       6 │         6 │ 2024-03-28 06:01:46.854646+01 │
│ transaction      │           2 │     3824 │         2 │            5 │           0 │       6 │         0 │ 2024-03-28 06:01:46.854646+01 │
│ other            │           0 │        0 │         0 │            0 │           0 │       0 │         0 │ 2024-03-28 06:01:46.854646+01 │
└──────────────────┴─────────────┴──────────┴───────────┴──────────────┴─────────────┴─────────┴───────────┴───────────────────────────────┘
(8 rows)

V PostgreSQL 17 se dřívější jedna cache rozdělila, a každá její separátní část (bank) má vlastní zámek.

Výchozí velikosti jsou plus mínus podobné jako dříve, ale lze je konfigurovat nastavením proměnných commit_timestamp_buffers, multixact_member_buffers, multixact_offset_buffers, notify_buffers, serializable_buffers, subtransaction_buffers, transaction_buffers.


Ostatní

O něco rychleji by se měla provádět změna proměnné search_path díky cache oid schémat. Datový typ ltree (z extenze ltree) má nově tzv hashovací funkci - tudíž se na něm může provést hashjoin, hashagg. Měla by se výrazně redukovat potřeba CPU způsobená režii fronty čekajících procesů (v praxi se jednalo o hraniční případy, kdy fronta čekajících procesů byla extrémně dlouhá).

Nově lze jako PARALLEL SAFE označit i PL/pgSQL funkce obsahující zachycení chyby.

Administrace

Zálohování

Doposud inkrementální zálohu umožňovaly pouze backup systémy třetích stran. Počínaje verzí 17 lze udělat inkrementální zálohu i s nástrojem pg_basebackup s volbou --incremental=MANIFEST. Rozdílová záloha je prakticky standardní záloha s výjimkou některých datových souborů, které jsou (v backupu) pojmenované INCREMENTAL.${původní název}. Novým příkazem pg_combinebackup lze zrekonstruovat datový adresář z prvního fullbackupu a posloupnosti inkrementálních backupů.

pg_basebackup -cfast -Dx
pg_basebackup -cfast -Dy --incremental x/backup_manifest
pg_combinebackup x y -o z

Díky inkrementálním backupům můžeme dělat častější periodické backupy i na větších a zápisem zatížených databázích - např. 1x za hodinu přičemž nejsou takové nároky na diskovou kapacitu pro archivaci transakčních logů. Také obnova by měla být výrazně rychlejší.

Patch pro parametrizaci pg_dumpu, který jsem 40x přepisoval, se konečně dostal do upstreamu. Od verze 17 můžete filtrovací parametry pg_dumpu uložit do souboru a z něj načíst. Výhodou je neomezená velikost oproti relativně hodně omezené velikosti příkazové řádky. Práce na této funkcionalitě je dobrou ukázkou někdy extrémně náročného nalezení řešení v komunitě. Problém není ani tak vlastní implementace (i když zde částečně ano). Patch je primitivní. Víceméně všichni souhlasili, že stávající parametrizace pg_dumpu je problém. Na to jak ten problém vyřešit už byly dost rozdílné názory - od použití JSONu, TOML, vlastní syntaxe. Jestli se má implementovat pouze filtrování nebo genericky konfigurace všech utilit ze souboru. Jestli se má použít ručně psaný parser nebo použít generátor parserů. Prototyp, který se bezproblémově používal u zákazníka jsem měl napsán za 1 den. Do upstreamu se ntá verze dostala možná po pěti letech (, která se zas až tak nelišila (designem) od mého prototypu). To je občas frustrující. Zadání je jasné, dobře ohraničené - a ve chvíli, kdy už by se měly řešit implementační detaily, tak někdo přijde s novou ideou, a začnou se řešit vzdušné zámky. Naštěstí, komunita, v tomto ohledu, funguje a rychle přijde obrana "dej mi reálný smysluplný use-case", takže se až tolik času nepropálí, nicméně implementace některých jednoduchých funkcí trvá zbytečně dlouho. Někdy je problém i v tom, že je k dispozici víc adekvátních řešení, ale musí se vybrat jedno.

echo "include table foo" | /usr/local/pgsql/master/bin/pg_dump --filter -

Nově pg_dump podporuje parametr --exclude-extension pro vyřazení extenze ze zálohy. Backup blobů (large objects) by měl být výrazně rychlejší.


Právo MAINTAIN a role pg_maintain

S právem MAINTAIN (pro relaci) nebo s rolí pg_maintain (na všechny relace) můžeme provádět údržbu tabulek (indexů a pohledů - obecně relací), a nemusíme být vlastníky nebo superusery (jedná se o příkazy VACUUM, ANALYZE, REINDEX, REFRESH, CLUSTER a LOCK). Jednoduchá, šikovná, praktická a hlavně bezpečná funkce.

Vestavěné C.UTF8 locale

Řazení řetězců se řeší v PostgreSQL pomocí funkcí externích knihoven. Buďto starší glibc nebo novější libicu. Jelikož pořadí řetězců je "materializováno" v indexech, tak je Postgres extrémně citlivý na zpětnou kompatibilitu těchto knihoven. V tomto ohledu je problém hlavně s glibc, kde jednak došlo během posledních let k nekompatibilním změnám, a kde to, že došlo k porušení zpětné kompatibility není jednoduše detekovatelné. U libicu je možné zjistit verzi implementace řazení a detekovat změnu. To, a že libicu poskytuje mnohem větší funkcionalitu, je důvod proč se postupně komunita odklání od glibc (pouze v podpoře collation). Řazení je dost častá operace (může se použít pro agregaci, pro joinování) a dost často nepotřebuje řazení podle jazykových norem. Takové řazení je výrazně rychlejší a lze jej jednoduše implementovat, a z důvodu snížení závislosti na externí implementaci se C collation (tak se označují) implementovalo interně (nicméně lze stále používat externí implementaci):

CREATE COLLATION _pg_c_utf8 (
  provider = builtin, locale = 'C.UTF8');

Případně lze použít připravené collation pg_c_utf8 nebo ucs_basic pro ascii (alternativa pro C):

postgres=# CREATE TABLE foo(a varchar COLLATE "pg_c_utf8");
CREATE TABLE
postgres=# INSERT INTO foo VALUES('Příliš žluťoučký kůň se napil žluté vody');
INSERT 0 1
postgres=# SELECT upper(a), initcap(a) FROM foo;
┌──────────────────────────────────────────┬──────────────────────────────────────────┐
│                  upper                   │                 initcap                  │
╞══════════════════════════════════════════╪══════════════════════════════════════════╡
│ PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ SE NAPIL ŽLUTÉ VODY │ Příliš Žluťoučký Kůň Se Napil Žluté Vody │
└──────────────────────────────────────────┴──────────────────────────────────────────┘
(1 row)

Lze použít i C collate, s C.UTF8 fungují funkce upper, lower a initcap pokud používáte kódování UTF8.

Replikace

V pohledu pg_replication_slots se ve sloupci conflict_reason zobrazuje důvod konfliktu. Aktuálně se zobrazuje pouze důvod vedoucí k zneplatnění slotu (wal_removed, rows_removed, wal_level_insufficient). Dalším novým sloupcem je inactive_since, kde je čas, kdy byl slot naposledy aktivně použit.

Nově sloty pro logickou replikaci přežijí upgrade (dříve je příkaz pg_upgrade zrušil).

Logický replikační slot na standby serveru může být nastavený jako synchronizovaný voláním funkce pg_sync_replication_slots. Tento replikační slot pak lze použít failoveru nebo plánovaném switoveru. Slot je synchronizován novým typem background workeru slot sync worker.

Standby replikační sloty uvedené v konfigurační proměnné standby_slot_names budou synchronizované vždy před synchronizací logické replikace. Tím by se mělo garantovat, že budoucí primární server bude vždy napřed před konzumentem (subscriber) logické replikace. Sloty vyjmenované v standby_slot_names musí být synchronizované

Nový příkaz pg_createsubsriber konvertuje hotstandby server (fyzická replikace) do primárního serveru s nastavenou logickou replikací. Důvodem je skutečnost, že inicializace logické replikace (zvláště u velkých databází) je náročnější a pomalejší než vytvoření hotstandby klonu (fyzické repliky). Konverzí z klonu by se výrazně měla redukovat náročnost na vytvoření logické repliky.

Výrazně by se měla zrychlit implementace aplikace zpracování subtransakcí (eviction) při dekódování logické replikace. Stávající implementace způsobovala velké prodlevy (lagy) pokud transakce měla velké množství subtransakcí. V popisu patche je uvedeno cca 30 násobné zrychlení u transakce s 100 tisíci subtransakcemi.

Partitioning

V PostgreSQL 17 můžeme partišny rozdělit příkazem ALTER TABLE SPLIT PARTITION nebo sloučit příkazem ALTER TABLE MERGE PARTITIONS. Příkazy vyžadují exkluzivní zámek ACCESS EXCLUSIVE nad partitiovanou tabulkou, tudíž pro vykonání těchto příkazů je nutná servisní odstávka aplikace. Dá se předpokládat, že v ne úplně vzdálené budoucnosti, dojde k podpoře paralelismu a k snížení úrovně zamykání.

ALTER TABLE sales_range SPLIT PARTITION sales_all INTO
  (PARTITION sales_apr2022 FOR VALUES FROM ('2022-04-01') TO ('2022-05-01'),
   PARTITION sales_feb2022 FOR VALUES FROM ('2022-02-01') TO ('2022-03-01'),
   PARTITION sales_others DEFAULT);

Vylučovací omezení (exclusion constraints) je možné aplikovat nyní i na partišnách (pokud omezení obsahuje všechny sloupce řídící partitioning). Nově také mohou partitions podědit sloupce s identitou partišnované tabulky:

CREATE TABLE pitest1 (f1 int, f2 text, f3 bigint generated always as identity) PARTITION BY RANGE (f1);
CREATE TABLE pitest1_p1 PARTITION OF pitest1 FOR VALUES FROM (10) TO (20);
CREATE TABLE pitest1_p2 PARTITION OF pitest1 FOR VALUES FROM (21) TO (30);
INSERT INTO pitest1 VALUES(10, 'ahoj');
INSERT INTO pitest1 VALUES(25, 'Nazdar');

postgres=# SELECT * FROM pitest1;
┌────┬────────┬────┐
│ f1 │   f2   │ f3 │
╞════╪════════╪════╡
│ 10 │ ahoj   │  1 │
│ 25 │ Nazdar │  2 │
└────┴────────┴────┘
(2 rows)

Ostatní

V Postgresu bylo možné nastavit dvanáct různých timeoutů. Ve verzi 17 přibyl transaction_timeout. Tento timeout se nevztahuje na 2PC transakce. Tímto tiemeoutem je např. možné nastavit maximální čas držení zámku. Pozor - tento timeout provede disconnect.

Nastavením proměnné allow_alter_system na false (v postgresql.conf), lze zablokovat příkaz ALTER SYSTEM (tudíž konfigurace PostgreSQL bude možná pouze skrze postgresql.conf).

V příkazu EXPLAIN lze volbou MEMORY zobrazit spotřebu paměti planneru:

(2024-04-01 11:30:14) postgres=# EXPLAIN (MEMORY) SELECT * FROM pg_class ;
┌─────────────────────────────────────────────────────────────┐
│                         QUERY PLAN                          │
╞═════════════════════════════════════════════════════════════╡
│ Seq Scan on pg_class  (cost=0.00..18.15 rows=415 width=263) │
│ Planning:                                                   │
│   Memory: used=22960 bytes  allocated=32768 bytes           │
└─────────────────────────────────────────────────────────────┘
(3 rows

Sledovat spotřebu paměti plannerem může mít smysl u extrémně komplikovaných dotazů s velkým počtem relací (např. vetším počtem partitions) - nebo při investigaci memory leaků.

Volbou SERIALIZE v příkazu EXPLAIN ANALYZE si lze vynutit transformaci dat do formátu protokolu (nicméně data se neposílají na klienta). Výsledkem jsou (v některých případech) realističtější data z důvodu vynuceného deTOASTu.

V pg_stat_statements jsou nové sloupce. jit_deform_count zobrazuje počet "tuple deform" funkcí kompilovaných JIT. "tuple deform" je proces, který převádí řádková data z formátu v jakém jsou uložena na disku do formátu s kterým se pracuje interně v Postgresu. jit_deform_time je celkový čas JIT kompilace tuple deform funkcí. JIT deform funkcí by měl pomoct u dotazů do širokých tabulek s velkým počtem řádků. Pokud se použije nevhodně (díky špatným odhadům), tak naopak má dost nepříjemnou režii JIT kompilace (pro výsledek JIT kompilace zatím neexistuje cache). stat_since je čas, kdy se pro SQL příkaz vytvořila řádek v pg_stat_statements (kdy se daný SQL příkaz poprve spustil po resetu statistik). minmax_stat_since je čas, kdy resetovaly min/max statistiky. Nově lze nastavením parametrů funkce pg_stat_statements_reset vyresetovat pouze statistiky konkrétního dotazu (případně min/max statistiky min_plan_time, max_plan_time, min_exec_time a max_exec_time).

Statistiky ohledně checkpointů se přesunuly do vlastního pohledu pg_stat_checkpointer.

Extenze pg_buffercache nově obsahuje funkci pg_buffercache_evict(bufferid), která odstraní zadaný buffer ze sdílené paměti.

Pelmel

login trigger

Novinkou v uložených procedurách je možnost vytvářet login triggery. Některé konkurenční databáze tuto funkcionalitu měly desetiletí, a pokud se z těchto databází migrovalo do Postgresu, tak to byl trochu problém (mimo cloud relativně jednoduše řešitelný extenzí např. <a href="https://github.com/okbob/session_exec">session_exec</a>). S vestavěnou podporou extenze nejsou potřeba. Pozor, i integrované řešení má režii, a zpomalí connect. Ale s tím se musí počítat. V rámci login triggeru (patří do kategorie event triggerů) můžete dynamicky nastavit práva, vytvořit dočasné tabulky pro session, nastavit GUC proměnné:

CREATE OR REPLACE FUNCTION public.init_session()
 RETURNS event_trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
  RAISE NOTICE 'login % at %', session_user, current_timestamp;
END;
$function$

CREATE EVENT TRIGGER init_session ON LOGIN EXECUTE FUNCTION init_session();

Chyba v triggeru může způsobit nemožnost přihlášení se, a nemožnost tuto chybu opravit. Pojistkou může být blokace triggeru ALTER EVENT TRIGGER init_session DISABLE anebo nastavením proměnné event_triggers na off v connection stringu. Takže pozor, ten kdo nastavuje connection string, má možost vyblokovat login trigger (tudíž login trigger nemůže (a není to cílem) zastoupit zabezpečení databáze).

Nové funkce

Vestavěná funkce random() vrací desetinné číslo 0.0 <= x < 1.0. Prakticky vždy se vrácené číslo mapuje na nějaký interval. Pro jednodušší práci se přidaly dvouparametrické funkce random(min, max), které generují číslo min <= x <= max. Použitý typ určuje typ výsledku (int, bigint, numeric):

postgres=# SELECT random(1, 10);
┌────────┐
│ random │
╞════════╡
│      9 │
└────────┘
(1 row)

Popis typu v Postgresu se skládá ze dvou aktributů - type a typmod. V typmodu může být zakódována maximální délka (varchar) nebo přesnost (numeric). Poznámka - u varcharu je to počet znaků zvýšený o hodnotu 4 (z historických důvodů). Interně má Postgres funkce převod typu na text a text na typ. První se používá například v chybových hlášeních, druhé v parsování SQL. K těmto interním funkcím dosud neexistovalo SQL API, tudíž je nebylo možné použít z PL/pgSQL. Od verze 17 už je tato funkcionalita k dispozici i z SQL:

postgres=# SELECT format_type(to_regtype('varchar(32)'), to_regtypemod('varchar(32)'));
┌───────────────────────┐
│      format_type      │
╞═══════════════════════╡
│ character varying(32) │
└───────────────────────┘
(1 row)

postgres=# SELECT to_regtypemod('varchar(32)');
┌───────────────┐
│ to_regtypemod │
╞═══════════════╡
│            36 │
└───────────────┘
(1 row)

postgres=# SELECT to_regtype('varchar(32)');
┌───────────────────┐
│    to_regtype     │
╞═══════════════════╡
│ character varying │
└───────────────────┘
(1 row)

Historií dost podobná je funkce pg_basetype, která vrací základní typ doménového typu. Tyto funkce mají dost úzkou oblast nasazení - myslím si, že je budou používat pouze autoři extenzí.

Nejspíš pro úplnost se implementovaly funkce to_bin (převod do binarní soustavy) a to_oct (převod do osmičkové soustavy):

postgres=# SELECT to_oct(-1234) AS "37777775456";
┌─────────────┐
│ 37777775456 │
╞═════════════╡
│ 37777775456 │
└─────────────┘
(1 row)

Interně jsou tyto funkce v Postgresu odjakživa. Nyní se pouze zpřístupnily z SQL.

Nové funkce lze použít i pro datový typ uuid, uuid_extract_timestamp vrátí čas vytvoření uuid typu 1. uuid_extract_version vrací pro uuid generované funkcí gen_random_uuid vrací 4 (pro ostatmí NULL).

Registr dynamické sdílené paměti

Perzistentní data (které mají přežít session) nebo sdílená data v paměti musí být v Postgresu uložená v tzv. sdílené paměti. Extenze, které chtějí používat sdílenou paměť musí být aktivované při startu Postgresu, jindy už není možné sdílenou paměť alokovat. Extenze, která nepotřebuje sdílenou paměť se aktivuje až v případě potřeby. To, že mohu zavést extenzi pouze při startu Postgresu je potíž - znamená to nutnost restartu, a to v některých provozech je problém (minimálně v tom, že se musí čekat na servisní okno). Je to klasika, řešíte hořák u klienta, a potřebovali byste urgentně extenzi pg_stat_statements, která ale není instalovaná a jejíž instalace vyžaduje restart, který ale z provozních důvodů není možný (rada - instalujte extenzi pg_stat_statements) by default).

Při paralelní zpracování dotazu se implementovala tzv. dynamická sdílená paměť DSM. DSM se používá jako fronta pro komunikaci mezi rodičovským procesem a obslužnými procesy dotazu (předávají se parametry, zpět se přebírají výsledky). Omezením DSM byla jednosměrnost jeho použití. Rodičovský proces vytvořil DSM, které mohly používat procesy potomků, a po zániku rodičovského procesu došlo k zrušení i použitého DSM segmentu. To je pro drtivou většinu extenzí nepraktické. Řešením je registr dynamické sdílené paměti. Voláním funkce GetNamedDSMSegment dostaneme ukazatel na dynamickou sdílenou paměť (můžeme si o ni požádat v libovolném čase). Parametrem je název. Určitě je to šikovná funkce, kterou použiji v profileru v plpgsql_checku.

libpq

libpq je knihovna, která se stará o komunikaci (obsluhu - implementaci protokolu) mezi klientem a serverem. Nově lze funkcemi PQclosePrepared, PQclosePortal, PQsendClosePrepared, a PQsendClosePortal explicitně zavřít vykonávání příkazu (nejedná se o rozšíření protokolu, pouze o rozšíření API). Pokud se tyto funkce použijí, tak pravděpodobně při implementaci nějaký proxy nebo poolerů místo volání příkazu DEALLOCATE.

Nově knihovna libpq podporuje neblokující (asynchronní) přerušení příkazu díky novému API tvořeného funkcemi PQcancelCreate, PQcancelStart, PQcancelPoll a PQcancelReset.

Knihovna také nově podporuje blokové načítání řádků (chunked rows mode). V tomto režimu se načítají data do paměti v blocích po n řádcích (n je určeno voláním funkce PQsetChunkedRowsMode). Tento režim se použil pro reimplementaci postupného načítání dat v psql, které se aktivuje nastavením proměnné FETCH_COUNT. Cílem fetchování je redukovat potřebu operační paměti zejména v neinteraktivních skriptech, kde se nepoužívá formát formátované tabulky.

Ostatní

Ve formátovacích řetězcích (funkce to_char, to_timestamp) lze použít formátovací kódy TZ (časová zóna nebo offset) nebo OF (offset).

Konfigurační proměnné huge_pages_status zobrazí stav použítí huge pages:

postgres=# SHOW huge_pages_status;
┌───────────────────┐
│ huge_pages_status │
╞═══════════════════╡
│ off               │
└───────────────────┘
(1 row)

Při deklaraci proměnné v PL/pgSQL můžeme typ specifikovat referencí (na jinou proměnnou). Nově lze použít syntax [] nebo ARRAY[] a ze reference na skalární typ vytvořit referenci na pole:

do $$
declare`
  v int;
  v1 v%type;
  v2 v%type[];
  v3 v%type array;
  v10 pg_catalog.pg_class%rowtype[];
begin
  raise notice '%', pg_typeof(v1);
  raise notice '%', pg_typeof(v2);
  raise notice '%', pg_typeof(v3);
  raise notice '%', pg_typeof(v10);
end;
$$;

NOTICE:  integer
NOTICE:  integer[]
NOTICE:  integer[]
NOTICE:  pg_class[]
DO

O podobné funkcionalitě se již mluvilo několik let, do upstreamu se dostává teď. Za 25 let, co dělám s PL/pgSQL jsem asi 2x potřeboval něco podobného, a musel jsem použít workaround. Není to nic extra, co by v Postgresu chybělo, ale je hezké (z důvodu úplnosti), že tato syntaxe je podporována.

Závěr

Oproti předchozím verzím je viditelných změn v PostgreSQL 17 o dost víc (dokončují se práce zahájené i před rokem 2020). Osobně si myslím, že tempo vývoje se během několika let zmírní - příští rok to ale nebude, rozdělaných patchů ve frontě je stále hodně. Pracuje se na logické replikaci, optimalizacích, možnosti použití streaming IO.