Hlavní navigace

PostgreSQL 12 – bude rok 2019 rokem Postgresu?

Pavel Stěhule

Po roce vychází nová verze PostgreSQL s číslem 12. Nejsem si úplně jistý, jak bych měl tuto verzi charakterizovat. Například SQL se ve verzi 12 prakticky vůbec nezměnilo. Přibylo ale několik malých (ale zásadních) funkcí.

Doba čtení: 27 minut

Sdílet

Více se začala používat infrastruktura pro zobrazení informací o průběhu zpracování příkazů ( CLUSTER, VACUUM FULL, REINDEX). Zásadně se změnil výchozí způsob zpracování nerekurzivních CTE a a také se výrazně optimalizoval mechanizmus výběru partitions. Integrace podpory JSONPath byla prvním a nezbytným krokem k budoucí úplné podpoře SQL/JSON. Primárně uživatelé PostGISu budou těžit z lepší optimalizace dotazů postavených nad funkcemi, které vrací tabulky

 Zásadní změnou, která je ovšem běžnému uživateli naprosto skrytá, je rozšíření interní infrastruktury, které umožní psát extenze implementující vlastní formát datových souborů. To je a bude klíčová vlastnost PostgreSQL, která bude umožňovat používat Postgres jako databázový framework a nad Postgresem si implementovat vlastní specializované databáze. Už nyní se testuje úložiště ZHeap, který implementuje MVCC podobně jako Oracle nebo MySQL s využitím UNDO logu. Dovedu si představit formát pro append only time series data, atd.

Postgres těží ze stabilního vývojového procesu (roční release cyklus, commitfesty), stabilního a hlavně relativně velkého týmu vývojářů a i ze zájmů uživatelů (což se vrací v motivaci vývojářů a ve zdrojích na vývoj). Je to vidět i v České republice. Před pěti roky se Postgres v korporátním prostředí vyskytoval sporadicky. To už teď neplatí. Vůbec tím netvrdím, že Postgres všude nahradí Oracle nebo MSSQL. Nicméně pro množství aplikací je a bude PostgreSQL reálnou funkční alternativou těchto komerčních databází. Mimo korporátní prostředí je Postgres roky etablovanou databází. Je také super, že Postgres má PostGIS. PostGIS těží ze stability a rozšiřitelnosti Postgresu, naopak Postgres z agility vývojářů PostGISu, kteří přicházejí s novými a rozumnými požadavky, jejichž realizace přispívá k větší rozšiřitelnosti (a potažmo k většímu rozšíření) Postgresu.

Administrace

V nové verzi můžeme použít několik nových SQL funkcí: pg_ls_tmpdir() pro výpis adresáře s dočasnými soubory (pro detekci zapomenutých dočasných souborů, monitorování velikosti), pg_promote() pro povýšení hot standby serveru na mastera. Pro monitoring může být zajímavá informace o času (reply_time) poslední přijaté zprávy v pohledu pg_stat_replication. Postgres nyní umožňuje nastavit session timeout TCP/IP spojení ( tcp_user_timeout). Zajímavou vlastností pg_dump(u) je možnost vyexportovat data ve formátu INSERT příkazů. Některé databáze (MySQL, Postgres) podporují tzv multiinsert. pg_dump nově umí vyexportovat data v tomto formátu:

pg_dump -t okresy -a --inserts --rows-per-insert 10  postgres

INSERT INTO public.okresy VALUES
  ('CZ0201', 'Benešov'),
  ('CZ0202', 'Beroun'),
  ('CZ0203', 'Kladno'),
  ('CZ0204', 'Kolín'),
  ('CZ0205', 'Kutná Hora'),
  ('CZ0206', 'Mělník'),
  ('CZ0207', 'Mladá Boleslav'),
  ('CZ0208', 'Nymburk'),
  ('CZ020B', 'Příbram'),
  ('CZ020C', 'Rakovník');
INSERT INTO public.okresy VALUES
  ('CZ0311', 'České Budějovice'),
  ('CZ0312', 'Český Krumlov'),
  ('CZ0313', 'Jindřichův Hradec'),
  ('CZ0314', 'Písek'),
  ('CZ0315', 'Prachatice'),
  ('CZ0316', 'Strakonice'),
  ('CZ0317', 'Tábor'),
  ('CZ0321', 'Domažlice'),
  ('CZ0322', 'Klatovy'),
  ('CZ0326', 'Rokycany');
...

Multiinsertem se redukuje počet transakcí – a pokud databáze by běžela v autocommit režimu, tak se výrazně zrychlí import (samozřejmě, že pro import lepším řešením je vypnout autocommit).

Integrace recovery.conf do postgresql.conf

Konfigurace replikace byla uložena v konfiguračním souboru recovery.conf. Samotná existence tohoto souboru zajistila, že databáze zůstala v permanentním recovery režimu, který je v Postgresu základem fyzické replikace. Toto jednoduché řešení ovšem není příliš praktické z důvodu nutnosti existence dalšího konfiguračního souboru.

Ve verzi 12 došlo k dlouho plánované integraci konfiguračních proměnných z recovery.conf  do postgresql.conf. Recovery režim se pak vynutí trigger souborem  recovery.signal.

REINDEX CONCURRENTLY

Z pohledu administrátora databáze je pro mne nejzajímavější možnost použít klauzuli CONCURRENTLY u příkazů REINDEX. Dost to zjednodušuje reindexaci (bez použití agresivnějších zámků) u drtivé většiny uživatelů Postgresu. Bez klauzule CONCURRENTLY příkaz REINDEX zamkne tabulku proti zápisu. Se zmíněnou klauzulí se index vytvoří ze snapshotu tabulky. Po vytvoření indexu se vytvoří nový snapshot, a na základě rozdílu vůči předchozímu snapshotu se index aktualizuje. Tento postup se opakuje, dokud těmito aktualizacemi indexu obsahově nedohoníme tabulku. Pozor – při dlouhodobě intenzivní zátěži popsaný algoritmus (a reindexace s klauzulí CONCURRENTLY) může běžet „věčně“.

Logování

Asi každý zažil, nebo alespoň o tom slyšel, situaci, kdy si příliš aktivním logováním způsobil menší nebo větší provozní problémy (místo na disku, přetížení IO). U jednoho zákazníka jsem řešil stížnost na rychlost databáze a jeho obavy ohledně vánoční zátěže, kdy má špičku. Logoval všechny SQL příkazy, a aby to bylo ještě pikantnější, nikdo z té firmy se ani jednou na ty logy nepodíval. Po zvednutí log_min_duration_statement z 0 na 50 (50ms) s databází neměli vůbec žádné problémy.

Někdy je ale nutné zalogovat všechno, a musí se to provést na produkční databázi. Je to taková procházka přes lavinové pole. Víte, že musíte přejít, ale také víte, že je to dost o ústa. Ve 12 je možné vzorkovat pomalé příkazy ukládané do logu viz log_statement_sample_rate (uloží se každý desátý, stý, ..). Díky tomu můžeme snížit log_min_duration_statement a nemusíme tolik řešit IO. Implementace je možná příliš jednoduchá (preferoval bych ještě jeden parametr, který by mi zajistil 100% logování každého dotazu bežícího déle než by byla hodnota tohoto parametru). Pro začátek to ale stačí.

Podobnou funkci má postgres i pro transakce – umožňuje zalogovat všechny příkazy z každé n-té transakce.

log_transaction_sample_rate = 0.01 # zaloguje každou stou transakci

Příkaz VACUUM

Příkaz VACUUM optimalizuje obsah (strukturu) tabulky a nad ní postavených indexů. Tento příkaz provádí celou řadu operací v několika iteracích. Nyní pomocí několika nových klauzulí, přepínačů můžeme přesněji specifikovat operace, které tento příkaz má provést, a tím např. redukovat dobu běhu, redukovat zápisu dat na disk, atd.

Volba SKIP_LOCKED způsobí přeskočení vakuování objektů (tabulek, index), kde by došlo ke konfliktu zámků. Volbou TRUNCATE FALSE můžeme zkrátit dobu běhu příkazu VACUUM o jeden krok, kde se snaží redukovat datový soubor ze zadu zrušením prázdných datových stránek. Nastavením INDEX_CLEANUP na TRUE, nebo FALSE můžeme opravdu výrazně zrychlit vakouvání tabulky za cenu zhoršení kvality indexu a tudíž následnému zpomalení přístupu k datům skrze index scan (o to více potřebný bude REINDEX indexů). V určitých situacích to má smysl – pokud například potřebujete zrychlit VACUUM FREEZE na velké tabulce:

VACUUM (INDEX_CLEANUP FALSE, FREEZE TRUE) tablename;

Nově VACUUM umí efektivněji pracovat s GiST indexy a uvolňovat koncové prázdné stránky.

Změnila se také konfigurace automatického vakuování. Oproti starším verzím běží VACUUM spuštěné procesem na pozadí (autovacuum) mnohem agresivněji. Zkrátila se pauza mezi zpracováním skupin datových stránek z 20ms na 2ms. Na moderních počítačích by s tímto nastavením neměl být problém, a VACUUM, které běží rychleji, dříve doběhne. Je menší riziko stopnutí, a do jisté míry se snižuje i nebezpečí bobtnání větších tabulek (bloating).

Monitoring dlouho běžících příkazů

Vloni, předloni jsem si postěžoval, že někdy nově vytvořená infrastruktura není dostatečně využitá. Konkrétně se jednalo o infrastrukturu pro online monitorování běhu dlouho trvajících příkazů. Nová verze zmíněnou infrastrukturu využívá více. Tzv progres report je k nově dispozici pro příkazy CREATE INDEX, REINDEX, CLUSTER a VACUUM FULL (z minula pro příkaz VACUUM).

postgres=# \d pg_stat_progress_create_index
         View "pg_catalog.pg_stat_progress_create_index"
┌────────────────────┬─────────┬───────────┬──────────┬─────────┐
│       Column       │  Type   │ Collation │ Nullable │ Default │
╞════════════════════╪═════════╪═══════════╪══════════╪═════════╡
│ pid                │ integer │           │          │         │
│ datid              │ oid     │           │          │         │
│ datname            │ name    │           │          │         │
│ relid              │ oid     │           │          │         │
│ index_relid        │ oid     │           │          │         │
│ command            │ text    │           │          │         │
│ phase              │ text    │           │          │         │
│ lockers_total      │ bigint  │           │          │         │
│ lockers_done       │ bigint  │           │          │         │
│ current_locker_pid │ bigint  │           │          │         │
│ blocks_total       │ bigint  │           │          │         │
│ blocks_done        │ bigint  │           │          │         │
│ tuples_total       │ bigint  │           │          │         │
│ tuples_done        │ bigint  │           │          │         │
│ partitions_total   │ bigint  │           │          │         │
│ partitions_done    │ bigint  │           │          │         │
└────────────────────┴─────────┴───────────┴──────────┴─────────┘

Bylo by hezké, kdyby tuto informaci automaticky zobrazovalo psql (nějaký dobrovolník?).

Kontrolní součty datových stránek

Příkaz pg_basebackup se používá pro vytvoření tzv fyzické zálohy, případně pro vytvoření klonů fyzické replikace. Dá se ale použít i pro kontrolu kontrolních součtů datových stránek objektů. Nově v pohledu pg_stat_database nalezneme počet detekovaných chyb.

Nová verze Postgres obsahuje příkaz pg_checksums speciálně navržený pro operace s kontrolními součty. Zatím běží pouze při vypnutém Postgresu (offline). Umožňuje vypnutí, zapnutí používání kontrolních součtů. Samozřejmě, že tímto příkazem můžeme provést i kontrolu integrity datových souborů (přes kontrolní součty obsahu datových stránek).

Vývoj

Podpora JSONPath

Integrací podpory JSONPath výrazů došlo k částečné kompletaci prací počatých v roce 2016, od kdy ANSI/SQL podporuje JSON. Dotazovacím jazykem je v tomto případě JSONPath (jako je XPath pro XML). Integrace trvala hodně dlouho jednak z důvodů komplexity a velikosti patche. Bohužel k dispozici není žádná existující knihovna s podporou JSONPath, kterou by vývojáři Postgresu mohli použít (licenční a technologické důvody). Navíc zde došlo k názorovým rozporům ohledně způsobu implementace.

postgres=# SELECT * FROM obce_json LIMIT 10;
┌─────────────────────────────────────────────────────────────────────────┐
│                                    j                                    │
╞═════════════════════════════════════════════════════════════════════════╡
│ {"obec": {"nazev": "Praha", "pocet_zen": 640710, "pocet_muzu": 608316}} │
│ {"obec": {"nazev": "Benešov", "pocet_zen": 8507, "pocet_muzu": 7875}}   │
│ {"obec": {"nazev": "Bernartice", "pocet_zen": 115, "pocet_muzu": 108}}  │
│ {"obec": {"nazev": "Bílkovice", "pocet_zen": 89, "pocet_muzu": 93}}     │
│ {"obec": {"nazev": "Blažejovice", "pocet_zen": 48, "pocet_muzu": 52}}   │
│ {"obec": {"nazev": "Borovnice", "pocet_zen": 37, "pocet_muzu": 39}}     │
│ {"obec": {"nazev": "Bukovany", "pocet_zen": 372, "pocet_muzu": 364}}    │
│ {"obec": {"nazev": "Bystřice", "pocet_zen": 2096, "pocet_muzu": 2124}}  │
│ {"obec": {"nazev": "Ctiboř", "pocet_zen": 50, "pocet_muzu": 55}}        │
│ {"obec": {"nazev": "Čakov", "pocet_zen": 60, "pocet_muzu": 65}}         │
└─────────────────────────────────────────────────────────────────────────┘
(10 rows)

CREATE INDEX ON obce_limit USING gin(j);

postgres=# SELECT * FROM obce_json WHERE j @? '$.obec ? (@.nazev == "Skalice" && @.pocet_zen > 300)';
┌─────────────────────────────────────────────────────────────────────┐
│                                  j                                  │
╞═════════════════════════════════════════════════════════════════════╡
│ {"obec": {"nazev": "Skalice", "pocet_zen": 318, "pocet_muzu": 247}} │
└─────────────────────────────────────────────────────────────────────┘
(1 row)

Alternativní zápis:

SELECT * FROM obce_json WHERE j @@ '$.obec.nazev == "Skalice" && $.obec.pocet_zen > 300';

Pokud je ve výrazu alespoň jedna rovnost, tak by se měl chytit i index:

postgres=# EXPLAIN SELECT * FROM obce_json WHERE j @? '$.obec ? (@.nazev == "Skalice" && @.pocet_zen > 300)';
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                           QUERY PLAN                                            │
╞═════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Bitmap Heap Scan on obce_json  (cost=36.05..55.71 rows=6 width=96)                              │
│   Recheck Cond: (j @? '$."obec"?(@."nazev" == "Skalice" && @."pocet_zen" > 300)'::jsonpath)     │
│   ->  Bitmap Index Scan on obce_json_j_idx  (cost=0.00..36.05 rows=6 width=0)                   │
│         Index Cond: (j @? '$."obec"?(@."nazev" == "Skalice" && @."pocet_zen" > 300)'::jsonpath) │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
(4 rows)

Podpora JSONPath, případně SQL/JSON je teprve na začátku. V další verzi by se mohla objevit funkce JSON_TABLE, v dalších pak sofistikovanější způsoby indexování. Pozor: I když se Postgres díky podpoře JSONu, XML může použít jako dokumentová databáze, pořád je a bude relační databází. Přístup k normalizovaným datům je a bude (z principu) výrazně rychlejší. Platí ale také, že u krátkodobých dat (logy atd) se normalizace nemusí vyplatit. Vždy doporučuji, co mohu jednoduše uložit relačně, měl bych uložit relačně – a zbytek pak do XML, JSONu atd.

Podpora XML

XML je v Postgresu roky podporováno a to poměrně dobře. Podpora ovšem odpovídá staršímu standardu SQL:2003 (kde se jako dotazovací jazyk používá XPath) a nikoliv modernějšímu SQL:2006 (kde se již používá jazyk XQuery). Běžnému uživateli pro běžnou práci je to, dovolím si tvrdit, jedno. Problémy mohou nastat při migracích aplikací, které intenzivně používají XML a funkce pro zpracování XML (kolem roku 2005 to byla módní vlna – XML databáze).

To, co Postgres s XML umí nebo neumí je určeno tím, co podporuje nebo nepodporuje knihovna libxml2. Tato knihovna dobře podporuje XPath případně XSLT, nepodporuje už ovšem XQuery. A pravděpodobně nikdy podporovat nebude, protože její vývoj je zmražen (pouze se opravují chyby). Pracovat s libxml2 je peklo (bez pochopení komplexního modelu XML a i komplexního API), ale nic jiného dostupného není. Postgres si kvůli BSD licenci nemůže dovolit integrovat komerční knihovny (vývojáři nechtějí blokovat firmy, které nad Postgresem vytvářejí komerční forky). Volat z C (Postgres) Javu si vůbec nikdo nedovede představit, a s voláním C++ knihoven (používajících C++ exceptions) nejsou nejlepší zkušenosti). Takže zbývá libxml2. Knihovna která je prověřená časem. V rámci této verze se udělala revize podpory XML. Co šlo se sesynchronizovalo se standardem (např. některé nuance zpracování výsledku XPath výběrů, backup hodnot ve formátu XML CONTENT). Co nešlo, se lépe zdokumentovalo. Java a Postgres se špatně integrují (na straně databáze) z důvodu rozdílů architektur. Své o tom vědí autoři PL/Java. Java je postavená na vláknech, Postgres na procesech.

Partitioning

V této verzi se na partitioningu intenzivně pracovalo, a přesto si toho asi většina uživatelů nevšimne. Partitioning nám umožňuje fyzicky uložit data do oddělených souborů podle hodnoty vybraného atributu. V Postgresu partition je tabulka. Úkolem optimalizátoru v případě partitioningu je redukovat partitions (tabulky), které 100% neobsahují zajímavá data. Tato úloha se označuje jako tzv partitioning pruning. Ve verzi 11 Postgres začal podporovat tzv run-time pruning – redukci partitions v době vykonávání dotazu. V době plánování dotazu nemusí být známá klíčová data pro pruning. Ta budou k dispozici až v exekuci.

Např.

-- může řešit planning-time pruning
SELECT count(*) FROM data WHERE inserted < '2019-06-06' - 100;

-- pouze runtime pruning, v planning time neznáme CURRENT_DATE
SELECT count(*) FROM data WHERE insered < CURRENT_DATE - 100;

Ve dvanáctce došlo k zásadní zrychlení operací planing-time pruningu. Díky tomu už není nutné držet relativně nízký počet partitions (doporučovalo se cca 100), a můžeme navrhovat menší partitions, kterých bude více (bezpečně kolem 1000). Optimalizován byl pouze planning-time pruning, nikoliv run-time pruning. Pokud spoléháte na run-time pruning, pořád platí doporučení udržovat počet partitions kolem stovky.

V 11 bylo možné definovat primární klíče v partitiovaných tabulkách. Nestihla se implementace cizích klíču odkazujících se na partitiované tabulky. V nové verzi jsou cizí klíče vůči partitiovaným tabulkám podporované:

CREATE TABLE pk(id int PRIMARY KEY) PARTITION BY RANGE(id);
CREATE TABLE fk(pk_id int REFERENCES pk(id));
CREATE TABLE pk1 PARTITION OF pk FOR VALUES FROM (0) TO (100);
CREATE TABLE pk2 PARTITION OF pk FOR VALUES FROM (100) TO (200);

INSERT INTO pk VALUES(88),(122);
INSERT INTO fk VALUES(88),(122);
INSERT INTO fk VALUES(90);

ERROR:  insert or update on table "fk" violates foreign key constraint "fk_pk_id_fkey"
DETAIL:  Key (pk_id)=(90) is not present in table "pk".

Generované sloupce

Poměrně zajímavou funkcionalitou jsou tzv generované sloupce (generated columns). V tuhle chvíli pro generované sloupce sám nějaké větší uplatnění nevidím. Je to ale nezbytný předpoklad pro implementaci syntaxe tzv temporálních tabulek podle ANSI/SQL 2011:

CREATE TABLE test(a int, b int GENERATED ALWAYS AS (a * 10) STORED)

Generované (počítané) sloupce, podle ANSI/SQL, mohou být uložené ( STORED) nebo virtuální ( VIRTUAL). Hodnota uloženého počítaného sloupce je uložená v tabulce jako jakákoliv jiná hodnota. Virtuální hodnota se počítá vždy znovu při každém čtení tabulky. Aby byla zajištěna konzistence mezi uloženými a virtuálními počítanými sloupci, tak (podle ANSI) jsou umožněny pouze výrazy, které jsou (terminologií Postgresu) neměnné (immutable) – např. 2 * x. To znamená, že nepříklad nemůžete použít generované sloupce pro výrazy typu random(), CURRENT_DATE, EXTRACT(month FROM CURRENT_TIMESTAMP) atd. Tomuto omezení velice dobře rozumím (což neznamená, že se mi líbí, ale dokáži se s ním smířit a nemám potřebu to dál komentovat a měnit).

Už jsem zmínil, že pro mne aktuální implementace je jen první krok k něčemu mnohem zajímavějšímu. U jiných databází je to jinak. Např. MySQL má skrze generované sloupce implementované funkcionální indexy, což je extra důležitá vlastnost.

PLpgSQL

Oproti jedenáctce jsou změny v PLpgSQL kosmetické – dodatečné kontroly kódy ( extra-checks) se rošířily o kontrolu strict_multi_assignment (počet polí kompozitní hodnoty musí být stejný jako počet polí kompozitní proměnné, do které se hodnota ukládá) a too_many_rows (pokud je cílem proměnná, tak dotaz nesmí vrátit více než jeden řádek).

Pro intezivnější vývoj v PLpgSQL doporučuji používat extenzi plpgsql_check. Za poslední rok přibylo několik nových testů – test klíčových slov v názvech proměnných, detekce SQL injection, kontrola formátu funkce formát, atd. Nově se tato extenze dá také použít jako profiler.

Zřetězené transakce

Zřetězaná transakce má v příkazech COMMIT nebo ROLLBACK klauzuli AND CHAIN. Po ukončení zřetězené transakce se automaticky startuje transakce nová se stejnou konfigurací jako předchozí transakce. Pokud to chápu správně, tak teď jsou zřetězené transakce implementovány jako posloupnost transakcí bez nějakých dalších optimalizací. Ty by snad měly následovat v dalších verzích (v tuto chvíli se určitě ušetří na síťové komunikaci, což u krátkých rychlých příkazů může být zajímavé).

BEGIN;
INSERT INTO ...
COMMIT AND CHAIN;
INSERT INTO ...
COMMIT AND CHAIN;
...

Unaccent a nedeterministická collations

Díky drobným úpravám extenze unaccent nyní funguje lépe i pro jiná písma než je latinka. Funkce dokáže odstranit diakritiku také ze znaků vytvořených kombinací.

Pokud používáte jazykovou podporu implementovanou knihovnou ICU, můžete vytvořit „nedeterministické“ collation. Deterministicke collation je takové, kdy dva řetězce jsou stejné, pokud jejich binární reprezentace je stejná. Doposud Postgres podporoval pouze deterministická collations.

K čemu je to dobré? Například chybějící funkcionalitou je podpora case insensitive collations. To jsou právě ta nederministická collations.

CREATE COLLATION case_insensitive (provider = icu, locale = 'und-u-ks-level2', deterministic = false);
CREATE COLLATION ignore_accents (provider = icu, locale = 'und-u-ks-level1-kc-true', deterministic = false);

CREATE TABLE foo(a text COLLATE case_insensitive, b text COLLATE ignore_accents);

INSERT INTO foo VALUES('Žlutý kůň','Žlutý kůň');

SELECT * FROM foo WHERE a = 'ŽLUTÝ KŮŇ';
┌───────────┬───────────┐
│     a     │     b     │
╞═══════════╪═══════════╡
│ Žlutý kůň │ Žlutý kůň │
└───────────┴───────────┘
(1 row)


SELECT * FROM foo WHERE b = 'Zluty kun';
┌───────────┬───────────┐
│     a     │     b     │
╞═══════════╪═══════════╡
│ Žlutý kůň │ Žlutý kůň │
└───────────┴───────────┘
(1 row)

Funguji i indexy:

CREATE INDEX ON foo(a);
CREATE INDEX ON foo(b);

SET enable_seqscan TO off; -- vynuceni indexu

postgres=# EXPLAIN SELECT * FROM foo WHERE b = 'Zluty kun';
┌──────────────────────────────────────────────────────────────────────┐
│                              QUERY PLAN                              │
╞══════════════════════════════════════════════════════════════════════╡
│ Index Scan using foo_b_idx on foo  (cost=0.12..8.14 rows=1 width=64) │
│   Index Cond: (b = 'Zluty kun'::text)                                │
└──────────────────────────────────────────────────────────────────────┘
(2 rows)

postgres=# EXPLAIN SELECT * FROM foo WHERE a = 'Zluty kun';
┌──────────────────────────────────────────────────────────────────────┐
│                              QUERY PLAN                              │
╞══════════════════════════════════════════════════════════════════════╡
│ Index Scan using foo_a_idx on foo  (cost=0.12..8.14 rows=1 width=64) │
│   Index Cond: (a = 'Zluty kun'::text)                                │
└──────────────────────────────────────────────────────────────────────┘
(2 rows)

COPY WHERE

Nově podporovanou klauzulí příkazu COPY je klauzule WHERE. Při importu rovnou profiltruje vkládaná data:

\COPY tab FROM ~/data.csv CSV WHERE inserted > '2019-01-01'

Nekompatibilita

Od roku 2005 se nedoporučuje používat klauzuli WITH OIDS příkazu CREATE TABLE (možnost vytvořit tabulku se skrytým sloupcem oid, který se používal jako jednoznačný identifikátor). Nyní cca po 14 letech je podpora této klauzule definitivně odstraněná.

Výkon

Nová verze obsahuje desítky různých optimalizací kódu, díky kterým by se více nebo méně měly zrychlit některé operace: zápis do partitiovaných tabulek, vyhledávání podřetězců ve více bajtových kódováních, vytváření a údržba indexů obsahujících větší množství duplicitních hodnot, odstranění většího množství tabulek, vytváření GiN, GiST indexů díky redukci objemu zapsaných WAL logů, optimalizace načítání dat z partitions, plánovaní nad tabulkami s velkým počtem partitions (tisíce) pokud lze partitions redukovat v době plánování dotazu.

Opět je o něco chytřejší optimalizace dotazů nad cizími tabulkami. „push down“ (provedení operace na vzdálené straně) je podporováno i pro operace ORDER BY NULLS a LIMIT OFFSET. Paralelismus (využití více CPU pro jeden dotaz) nyní bude fungovat i v úrovni izolace transakcí SERIALIZABLE. Oproti předchozí verzi přidání nové partition nevyžaduje exkluzivní zámek na partitiované tabulce.

Inline CTE

Nikdy jsem si nezvykl používat CTE pro běžné nerekurzivní dotazy. V tomto ohledu jsem ale spíš výjimkou potvrzující pravidlo. CTE se prosadilo i jako určitá náhrada složitějších subselectů.

WITH
  s1 AS (
    SELECT FROM t1
),
  s2 AS (
    SELECT FROM t2
)
SELECT * FROM s1 JOIN s2 ON s1.id = s2.id;

Pro takový způsob použití CTE se implementace CTE v Postgresu nehodila. Postgres každý poddotaz v CTE vždy materializoval (za tím si můžete představit interní dočasnou tabulku). Pokud by výsledkem poddotazu byla větší relace (s větším počtem řádků), tak je materializace drahá. Pokud je výsledkem pár řádků, tak naopak materializace může hodně zrychlit provedení dotazu ušetřením opakovaných výpočtů.

Jiné databáze k CTE takto nepřistupují a při portaci aplikací do Postgresu se objevil nepříjemný problém. Nerekurzivní CTE bylo v Postgresu často výrazně pomalejší než v jiných databázích. Poměrně dlouze se diskutovalo o tom, že by se Postgres měl podřídit obecnému názoru, i za cenu, že dojde ke zpomalení CTE, které jsou díky materializaci rychlé. Bylo to dost bolestivé rozhodování, protože je jasné, že jakékoliv řešení způsobí výkonnostní problémy některým uživatelům (které je možné opravit pouze zásahem do aplikace a úpravou SQL).

V nové verzi se pro nerekurzivní poddotazy v CTE (pokud jsou odkazované pouze jednou) použije inlining. Je to totéž jako kdyby autor předchozího SQL rovnou napsal:

SELECT *
  FROM (SELECT FROM t1) s1
       JOIN (SELECT FROM t2) s2
       ON s1.id = s2.id;

Pokud si chcete vynutit způsob vykonání dotazu jako ve starších verzí dotazu, je nutné použít klíčové slovo MATERIALIZED:

WITH
  s1 AS MATERIALIZED (
    SELECT FROM t1
),
  s2 AS MATERIALIZED (
    SELECT FROM t2
)
SELECT * FROM s1 JOIN s2 ON s1.id = s2.id;

Materializaci lze blokovat slovem NOT MATERIALIZED.

Více sloupcové MCV (Most-Common-Value) statistiky

Implementace více sloupcových statistik v Postgresu je výsledkem dlouhodobé práce Tomáše Vondry, kterého můžete znát jako moderátora P2D2, případně odjinud (aktuálně je předsedou CSPUGu, a jediným českým PostgreSQL commiterem). Sloupcové statistiky se v moderních relačních databázích používají k odhadu výsledku dotazu. Vůči tomuto odhadu se pak provádí optimalizace dotazu.

Tichým nicméně zásadním předpokladem implementace téměř všech SQL optimalizátorů je nezávislost ukládaných dat. Např. sloupec „příjmení“ minimálně koreluje se sloupcem „jméno“ a zrovna tak asi můžeme zanedbat korelaci se sloupcem „výška“. Dost často ale pracujeme se závislými hodnotami, a často se na ně ptáme. Například můžeme mít sloupce „výška“, „hmotnost“ nebo „ulice“, „obec“. V případě takových dat a takových dotazu výchozí předpoklad o nezávislosti hodnot neplatí a odhady nebudou správné (často mohou být dost tragické, a dost negativně ovlivňovat kvalitu prováděcích plánů dotazů).

Jako příklad použiji tabulky obec, a ulice. Data jsem stáhl z ČUZK (soubor addesses.zip) a rozbalil do 6258 csv souborů. Vytvořil jsem si tabulku adresy:

CREATE TABLE adresy(nazev_obce text, nazev_ulice text, cislo_domovni int)

a bash skriptem jsem tam naimportoval obsah CSVček:

# sed (odstrani DOS konce radku), awk rusi prazdne radky
for f in *.csv; do echo "\\copy adresy from program 'cat $f | sed -e \"s/\r//g\" | cut -d\";\" -f3,11,13 | awk NF' csv delimiter ';' header encoding 'win1250'"; done | psql postgres

Nyní mám data na kterých mohu alespoň zhruba ukázat smysl více sloupcových statistik. Ještě předtím si aktualizuji sloupcové statistiky na této tabulce:

ANALYZE adresy;

Dotazem

SELECT count(*), nazev_ulice FROM
  (SELECT DISTINCT nazev_obce, nazev_ulice
     FROM adresy) s
 WHERE nazev_ulice IS NOT NULL
 GROUP BY 2
 ORDER BY 1 DESC
 LIMIT 10;
┌───────┬─────────────┐
│ count │ nazev_ulice │
╞═══════╪═════════════╡
│   687 │ Zahradní    │
│   584 │ Krátká      │
│   508 │ Nádražní    │
│   488 │ Polní       │
│   469 │ Školní      │
│   452 │ Luční       │
│   383 │ Komenského  │
│   372 │ Nová        │
│   346 │ Husova      │
│   344 │ Družstevní  │
└───────┴─────────────┘
(10 rows)

Zkusím testovat odhady

postgres=# EXPLAIN ANALYZE SELECT * FROM adresy WHERE nazev_ulice = 'Nádražní';
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                         QUERY PLAN                                                         │
╞════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Gather  (cost=1000.00..36394.14 rows=13749 width=25) (actual time=0.694..146.998 rows=13772 loops=1)                       │
│   Workers Planned: 2                                                                                                       │
│   Workers Launched: 2                                                                                                      │
│   ->  Parallel Seq Scan on adresy  (cost=0.00..34019.24 rows=5729 width=25) (actual time=0.591..134.544 rows=4591 loops=3) │
│         Filter: (nazev_ulice = 'Nádražní'::text)                                                                           │
│         Rows Removed by Filter: 970529                                                                                     │
│ Planning Time: 0.192 ms                                                                                                    │
│ Execution Time: 147.707 ms                                                                                                 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

To je relativně přesné. Pokud zkusím se zeptat zároveň na obec, tak patrně dostanu mnohem horší výsledky. Největší nádražní ulice je v Dubňech, nejmenší v Dobříši:

postgres=# EXPLAIN ANALYZE SELECT * FROM adresy WHERE nazev_ulice = 'Nádražní' AND nazev_obce = 'Dubňany';
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                       QUERY PLAN                                                        │
╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Gather  (cost=1000.00..29221.60 rows=18 width=25) (actual time=158.272..174.094 rows=179 loops=1)                       │
│   Workers Planned: 2                                                                                                    │
│   Workers Launched: 2                                                                                                   │
│   ->  Parallel Seq Scan on adresy  (cost=0.00..28219.80 rows=8 width=25) (actual time=151.599..162.678 rows=60 loops=3) │
│         Filter: ((nazev_ulice = 'Nádražní'::text) AND (nazev_obce = 'Dubňany'::text))                                   │
│         Rows Removed by Filter: 503236                                                                                  │
│ Planning Time: 1.246 ms                                                                                                 │
│ Execution Time: 174.155 ms                                                                                              │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

Zde můžeme vidět výraznou chybu v odhadu: 2 << 179.

Tomáš do 12ky napsal podporu více-sloupcových MCV statistik (most-common-values – ANALYZE identifikuje nejčastější hodnoty a k nim se uloží přesné statistiky), které teď vyzkouším:

CREATE STATISTICS obce_ulice(mcv) ON nazev_ulice, nazev_obce FROM adresy;
ANALYZE adresy;

postgres=# EXPLAIN ANALYZE SELECT * FROM adresy WHERE nazev_ulice = 'Nádražní' AND nazev_obce = 'Dubňany';
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                        QUERY PLAN                                                        │
╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Gather  (cost=1000.00..29240.40 rows=206 width=25) (actual time=137.969..153.381 rows=179 loops=1)                       │
│   Workers Planned: 2                                                                                                     │
│   Workers Launched: 2                                                                                                    │
│   ->  Parallel Seq Scan on adresy  (cost=0.00..28219.80 rows=86 width=25) (actual time=132.186..142.982 rows=60 loops=3) │
│         Filter: ((nazev_ulice = 'Nádražní'::text) AND (nazev_obce = 'Dubňany'::text))                                    │
│         Rows Removed by Filter: 503236                                                                                   │
│ Planning Time: 2.068 ms                                                                                                  │
│ Execution Time: 153.440 ms                                                                                               │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

Vidíme, že odhad se velice zpřesnil. Ale abych se dostal k tomuto výsledku, musel jsem trochu podvádět:

  1. promazal jsem adresy, které neměly specifikovanou ulici. NULL je nejčastější název ulice v ČR, a bylo vidět, že pokud máte data, která obsahují hodně NULL hodnot, tak to ten stávající statistický model nerozchodí.
  2. zvýšil jsem velikost statistik z 100 na 1000. Výrazně jsem tím zpomalil příkaz ANALYZE. Tabulka adres má cca 147MB a ANALYZE běží cca 30 sec.

Když se ptáme na kombinace, které jsou statistikou pokryté, tak je vidět velice přesný odhad:

postgres=# EXPLAIN ANALYZE SELECT * FROM adresy WHERE nazev_ulice = 'Pionýrů' AND nazev_obce = 'Most';
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                         QUERY PLAN                                                         │
╞════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Gather  (cost=1000.00..29299.80 rows=800 width=25) (actual time=133.134..166.841 rows=779 loops=1)                         │
│   Workers Planned: 2                                                                                                       │
│   Workers Launched: 2                                                                                                      │
│   ->  Parallel Seq Scan on adresy  (cost=0.00..28219.80 rows=333 width=25) (actual time=126.367..155.339 rows=260 loops=3) │
│         Filter: ((nazev_ulice = 'Pionýrů'::text) AND (nazev_obce = 'Most'::text))                                          │
│         Rows Removed by Filter: 503036                                                                                     │
│ Planning Time: 1.469 ms                                                                                                    │
│ Execution Time: 166.935 ms                                                                                                 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

totéž bez více-sloupcových statistik:

postgres=# EXPLAIN ANALYZE SELECT * FROM adresy WHERE nazev_ulice = 'Pionýrů' AND nazev_obce = 'Most';
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                       QUERY PLAN                                                        │
╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Gather  (cost=1000.00..29220.40 rows=6 width=25) (actual time=86.236..119.740 rows=779 loops=1)                         │
│   Workers Planned: 2                                                                                                    │
│   Workers Launched: 2                                                                                                   │
│   ->  Parallel Seq Scan on adresy  (cost=0.00..28219.80 rows=2 width=25) (actual time=79.610..108.493 rows=260 loops=3) │
│         Filter: ((nazev_ulice = 'Pionýrů'::text) AND (nazev_obce = 'Most'::text))                                       │
│         Rows Removed by Filter: 503036                                                                                  │
│ Planning Time: 0.751 ms                                                                                                 │
│ Execution Time: 119.829 ms                                                                                              │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

Optimalizace dotazů nad pohledy ze schématu information_schema

U několika mých zákazníků jsem se setkal s častým a pomalým dotazem

SELECT * FROM information_schema.tables WHERE table_name = 'xxx' LIMT 1;

Tímto dotazem se zjišťuje existence nebo neexistence určité tabulky. Jedná se o pohled do systémového katalogu, který z důvodu komplexnější transformace typů PostgreSQL nedokázal dobře optimalizovat, a který vedl vždy na sekvenční scan tabulky pg_class. Za normálních okolností to nebyl problém. Pokud ale došlo k nafouknutí zmíněné tabulky (table bloat), pak zmíněný dotaz mohl běžet i několik stovek ms, a pokud se volal několikrát za sec, tak generoval většinu zátěže databáze. U 12ky tento dotaz bude perfektně zoptimalizovaný (mělo by to být totéž, jako kdyby se běželo nad zdrojovými tabulkami). Z toho mám určitě radost. Trochu mám obavy že touto opravou mi zmizí jeden signál upozorňující na závažný aplikační problém (docela jednoduše lze tento problém detekovat jinak). Tabulka pg_class se nenafoukne do absurdních rozměrů jen tak.

JIT kompilace výrazů

Minulá verze obsahovala podporu JIT kompilace výrazů, která byla ve výchozí konfiguraci vypnutá. Vloni se jednalo o opravdu experimentální funkci, která mohla mít nepříznivý vliv na stabilitu Postgresu. Aktuálně by tato funkcionalita už měla být dostatečně stabilní a je proto ve výchozí konfiguraci aktivní. JIT může přinést až 30% zrychlení dotazů u dotazů, které běží alespoň několik vteřin. U kratších dotazů může dojít naopak ke zpomalení.

JIT kompilace může trvat stovky milisekund, a jelikož zatím není pro přeložený kód k dispozici cache, tak se překládá při každém spuštění dotazu znovu. Nevhodnému použití JIT by měly zabránit minimální ceny pro použití jednotlivých JIT optimalizací ( jit_above_cost, jit_inline_above_cost, jit_optimize_cost). Může se ale stát, že při špatných odhadech se JIT použije nevhodně. I v těch nejhorších případech by ale cena za JIT měla být maximálně několik set ms.

Zobrazení hodnot ovlivňujících optimalizaci dotazu v prováděcím plánu

Optimalizaci dotazů ovlivňuje větší množství konfiguračních hodnot, které mohou být nastavené v konfiguračním souboru, pro databázi, pro uživatele nebo pro session aplikací. V Postgresu umíme zalogovat prováděcí plány. U komplexnějších aplikací byl ale problém vydedukovat, proč je ten či onen plán takový, jaký je. Nyní máme možnost zobrazit v prováděcím plánu všechny konfigurační proměnné, které mohou ovlivnit optimalizaci dotazu:

EXPLAIN (SETTINGS) SELECT * FROM foo WHERE b = 'Zluty kun';
┌──────────────────────────────────────────────────────────────────────┐
│                              QUERY PLAN                              │
╞══════════════════════════════════════════════════════════════════════╡
│ Index Scan using foo_b_idx on foo  (cost=0.12..8.14 rows=1 width=64) │
│   Index Cond: (b = 'Zluty kun'::text)                                │
│ Settings: enable_seqscan = 'off'                                     │
└──────────────────────────────────────────────────────────────────────┘
(3 rows)

Částečná dekomprimace

Pokud do Postgresu uložíte větší hodnotu (delší texty, XML, geo data), tak transparentně (aniž by o tom uživatel věděl) dojde ke komprimaci dat. Zkráceně se tento mechanizmus nazývá TOAST. Tomuto mechanismu vděčíme za příjemné limity pro maximální velikost varcharu, XML, JSONu (teoretický limit 1GB, praktický limit jsou stovky megabajtů). Při čtení dat, opět transparentně, se data dekomprimují. Funguje to docela dobře, a roky nebylo potřeba na tomto mechanizmu nic měnit.

Vývojáři PostGISu ale narazili na problém v těch případech, kdy pro určité operace potřebují pouze prvních pár desítek bajtů, nicméně kvůli dekomprimaci musí přečíst kompletní hodnotu, která v jejich případě může mít i stovky kilobajt. Nově tedy TOAST umí načíst a dekomprimovat i blok o specifikované velikosti. Patch napsal Paul Ramsey (autor PostGISu) a předpokládám, že primární užitek z tohoto patche budou mít právě uživatelé PostGISu.

Estimační funkce pro funkce vracející tabulky (support functions)

V Postgresu můžete psát funkce, které vrací tabulku. Nejznámější takovou funkcí (a nejjednodušší) je funkce generate_series. S designem tabulkových funkcí to tehdy vývojáři hodně trefili. Dá se s nimi udělat hrozně moc, a přitom se jednoduše používají. Z pohledu optimalizátoru je chování tabulkové funkce stejné (bez ohledu na její vstupní parametry). Atributy funkce ROWS, a COST jsou konstanty.

PostGIS tabulkové funkce hodně používá, a to že optimalizátor neví vůbec nic o budoucím výstupu tabulkové funkce (počet řádků, náročnost) vedlo k tomu, že některé PostGISové dotazy nebyly dobře optimalizovány (a pokud byly, tak za cenu ošklivých hacků v PostGISu).

Ve dvanáctce může autor tabulkové funkce ještě napsat tzv podpůrnou funkci, kterou, pokud existuje, může použít optimalizátor (např. aby získal informaci o předpokládané náročnosti, o předpokládaném počtu řádek).

Opět asi největší benefit budou mít uživatelé PostGISu (mělo by to být použito v PostGISu 3), ale i uživatelům, kteří používají funkce generate_series nebo unnest se mohou zlepšit prováděcí plány dotazů:

-- PostgreSQL 11
ides_jmmaj_prac=# EXPLAIN SELECT * FROM generate_series(1,10);
┌────────────────────────────────────────────────────────────────────────┐
│                               QUERY PLAN                               │
╞════════════════════════════════════════════════════════════════════════╡
│ Function Scan on generate_series  (cost=0.00..10.00 rows=1000 width=4) │
└────────────────────────────────────────────────────────────────────────┘
(1 řádka)

ides_jmmaj_prac=# EXPLAIN SELECT * FROM UNNEST(ARRAY[1,2]);
┌─────────────────────────────────────────────────────────────┐
│                         QUERY PLAN                          │
╞═════════════════════════════════════════════════════════════╡
│ Function Scan on unnest  (cost=0.00..1.00 rows=100 width=4) │
└─────────────────────────────────────────────────────────────┘
(1 řádka)

-- PostgreSQL 12
postgres=# EXPLAIN SELECT * FROM generate_series(1,10);
┌─────────────────────────────────────────────────────────────────────┐
│                             QUERY PLAN                              │
╞═════════════════════════════════════════════════════════════════════╡
│ Function Scan on generate_series  (cost=0.00..0.10 rows=10 width=4) │
└─────────────────────────────────────────────────────────────────────┘
(1 row)

postgres=# EXPLAIN SELECT * FROM UNNEST(ARRAY[1,2]);
┌───────────────────────────────────────────────────────────┐
│                        QUERY PLAN                         │
╞═══════════════════════════════════════════════════════════╡
│ Function Scan on unnest  (cost=0.00..0.02 rows=2 width=4) │
└───────────────────────────────────────────────────────────┘
(1 row)

psql

Integrace podpory CSV

Určitě zajímavá bude integrace podpory CSV formátu do psql. Roky je CSV v Postgresu podporováno příkazem COPY na straně serveru. V psql  CSV jako takové podporováno nebylo, a z toho důvodu některé úlohy bylo nutné řešit jakoby přes ruku. V nové verzi je CSV podporováno jako další tzv výstupní formát:

postgres=# \pset format csv
Output format is csv.
postgres=# select * from okresy limit 10;
id,nazev
CZ0201,Benešov
CZ0202,Beroun
CZ0203,Kladno
CZ0204,Kolín

postgres=# \pset csv_fieldsep ;
Field separator for CSV is ";".
postgres=# select * from okresy limit 10;
id;nazev
CZ0201;Benešov
CZ0202;Beroun
CZ0203;Kladno
CZ0204;Kolín

Případně se CSV nechá jednoduše vygenerovat v neinteraktivním režimu:

psql -c "select * from okresy limit 10" --csv | ...

Pro komplikovanější kombinace escapeování, vložení do uvozovek je nutné stále použít příkaz COPY. Jednoduché zadání se nyní ale dají řešit jednoduše.

Režim minimalistického zobrazení chyb

Běžně psql zobrazí kód chyby ( SQLSTATE) a odpovídající chybové hlášení. Kód chyby je relativně stabilní hodnota. Naproti tomu texty chybových hlášení se mění. Dochází k opravám (úpravám) jak po věcné, tak jazykové stránce. Tato nestabilita může působit problémy při údržbě regresních testů založených na volání a zpracování výstupu psql. Řešením může být nová úroveň zobrazení chyb „sqlstate“.

postgres=# SELECT 1/0;
ERROR:  division by zero

postgres=# \set VERBOSITY sqlstate
postgres=# SELECT 1/0;
ERROR:  22012

Zobrazení partitiovaných tabulek, partitions

Partitioning je super. V psql  mi k němu chyběl report, který by zobrazoval partitiovaná data kumulovaně. V novém psql můžete použít příkaz \dP(+), který zobrazí partitiované tabulky (a případně souhrn velikostí všech partitions).

postgres=# \dP+
                           List of partitioned relations
┌────────┬─────────┬───────┬───────────────────┬───────┬────────────┬─────────────┐
│ Schema │  Name   │ Owner │       Type        │ Table │ Total size │ Description │
╞════════╪═════════╪═══════╪═══════════════════╪═══════╪════════════╪═════════════╡
│ public │ pk      │ pavel │ partitioned table │       │ 16 kB      │             │
│ public │ pk_pkey │ pavel │ partitioned index │ pk    │ 32 kB      │             │
└────────┴─────────┴───────┴───────────────────┴───────┴────────────┴─────────────┘
(2 rows)

postgres=# \dt+ pk*
                           List of relations
┌────────┬──────┬───────────────────┬───────┬────────────┬─────────────┐
│ Schema │ Name │       Type        │ Owner │    Size    │ Description │
╞════════╪══════╪═══════════════════╪═══════╪════════════╪═════════════╡
│ public │ pk   │ partitioned table │ pavel │ 0 bytes    │             │
│ public │ pk1  │ table             │ pavel │ 8192 bytes │             │
│ public │ pk2  │ table             │ pavel │ 8192 bytes │             │
└────────┴──────┴───────────────────┴───────┴────────────┴─────────────┘
(3 rows)

postgres=# \di+ pk*
                                List of relations
┌────────┬──────────┬───────────────────┬───────┬───────┬─────────┬─────────────┐
│ Schema │   Name   │       Type        │ Owner │ Table │  Size   │ Description │
╞════════╪══════════╪═══════════════════╪═══════╪═══════╪═════════╪═════════════╡
│ public │ pk1_pkey │ index             │ pavel │ pk1   │ 16 kB   │             │
│ public │ pk2_pkey │ index             │ pavel │ pk2   │ 16 kB   │             │
│ public │ pk_pkey  │ partitioned index │ pavel │ pk    │ 0 bytes │             │
└────────┴──────────┴───────────────────┴───────┴───────┴─────────┴─────────────┘
(3 rows)

Závěr

I když se toho za poslední rok udělalo docela dost, nestihlo se vše. Některé patche čekají na merge několik let. Stále je a ještě pár let bude, co dělat. Pracuje se na logické replikaci. Už se pracuje i na dalších speciálních extenzích implementující další formáty datových souborů. Před 5 roky (možná už to bylo dříve) jeden zdejší výrazný diskutér predikoval Postgresu 5 let. Postgres tu je stále, a jen to, co je rozděláno, vidím na další 3–4 roky práce.

Před 5 roky si nikdo pořádně nedokázal představit, že Postgres bude mít podporu paralelismu pro jeden dotaz, různé typy replikace, integrovanou JIT kompilaci výrazů, více sloupcové statistiky, cizí (foreign) tabulky. Postgres toho dneska umí hodně. Pořád je to ale databáze, která se nainstaluje za pár vteřin, a za pár minut ji lze používat. Hlavně, je to databáze, která se pro svoje typické spektrum úloh, používá jednoduše. To je hlavní cíl vývojářů Postgresu. Napsat relační SQL databázi, která se používá jako databáze (nikoliv jako storage), a která se používá jednoduše.