PostgreSQL 13 (2020)

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

Autor: Pavel Stěhule, 2020

PostgreSQL 13

Při vývoji databáze jdou proti sobě dvě síly. Tou první je snaha o co "nejchytřejší" optimalizátor, a co nejrobustnější, nejspolehlivější provoz podepřený co nejpodrobnějším monitoringem. Chytřejší optimalizátor si lépe poradí s komplikovanými dotazy. Bohužel chytřejší optimalizátor je pomalejší a náročnější na zdroje, a tudíž zpomalí vykonávání jednoduchých dotazů, kde se jeho možnosti neuplatní. Podrobnější monitoring je také náročnější na zdroje(IO, CPU).

Aby výkon databáze s každou novou verzí výrazně neklesal (nebo jen neklesal) neustále se reviduje kód a neustále se hledají optimalizace na všech úrovních. A to je ta druhá síla. Není až tak úplně komplikované vymyslet novou optimalizaci dotazu, ale je docela komplikované ji implementovat ve stávajícím systému, tak aby se drtivá většina dotazů nezpomalila (pro velké množství aplikací je 90% kvantil dotazů pod 10ms).

Mně osobně přijde, že letošní verze není není charakteristická nějakou výraznou novou funkcionalitou. Ale nejde si nevšimnout desítek poměrně důležitých interních optimalizací (na některých se pracovalo několik let - rozhodně se tedy nejedná pouze o mikro optimalizace).

Optimalizace

Deduplikace klíčů v indexu

Zvláště u indexů cizích klíčů se vyskytuje velké množství duplicitních hodnot. Postgres 13 podporuje deduplikaci klíčů uložených v indexu. K deduplikaci (k redukci duplicitních klíčů) dochází ve chvíli dělení koncové stránky indexu (neprovádí se okamžitě). Díky tomu by měla mít mít minimální režii. Na mých testovacích datech (obce ČR) je velikost indexu nad cizím klíčem (obce.okres_id) méně než poloviční 64kB (ve verzi 13) versus 160kB (ve verzi 12).

Incremental sort

Pokud jsem chtěl ve starších verzích urychlit více sloupcové řazení indexem, musel jsem mít odpovídající více sloupcový index. A pokud jsem měl více kombinací, tak jsem musel mít víc více-sloupcových indexů. Více indexů znamená pomalejší INSERT, UPDATE, pomalejší VACUUM. Nová verze umí načíst data v určitém pořadí z indexu, a pak je do-seřadit.

Jelikož nemám velká data, tak si musím snížit work_mem (a tím si prodražít sort):

SET work_mem TO '64kB';

-- Postgres 12
postgres=> EXPLAIN SELECT * 
                     FROM obce
                    ORDER BY okres_id, pocet_muzu + pocet_zen DESC;
┌────────────────────────────────────────────────────────────────┐
│                           QUERY PLAN                           │
╞════════════════════════════════════════════════════════════════╡
│ Sort  (cost=916.18..931.80 rows=6250 width=45)                 │
│   Sort Key: okres_id, ((pocet_muzu + pocet_zen)) DESC          │
│   ->  Seq Scan on obce  (cost=0.00..137.12 rows=6250 width=45) │
└────────────────────────────────────────────────────────────────┘
(3 rows)

postgres=> EXPLAIN SELECT * 
                     FROM obce 
                    ORDER BY okres_id, nazev;
┌────────────────────────────────────────────────────────────────┐
│                           QUERY PLAN                           │
╞════════════════════════════════════════════════════════════════╡
│ Sort  (cost=900.55..916.18 rows=6250 width=41)                 │
│   Sort Key: okres_id, nazev                                    │
│   ->  Seq Scan on obce  (cost=0.00..121.50 rows=6250 width=41) │
└────────────────────────────────────────────────────────────────┘
(3 rows)

-- Postgres 13
postgres=> EXPLAIN SELECT * 
                     FROM obce 
                    ORDER BY okres_id, pocet_muzu + pocet_zen DESC;
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│                                        QUERY PLAN                                        │
╞══════════════════════════════════════════════════════════════════════════════════════════╡
│ Incremental Sort  (cost=7.17..618.34 rows=6250 width=45)                                 │
│   Sort Key: okres_id, ((pocet_muzu + pocet_zen)) DESC                                    │
│   Presorted Key: okres_id                                                                │
│   ->  Index Scan using obce_okres_id_idx on obce  (cost=0.28..206.12 rows=6250 width=45) │
└──────────────────────────────────────────────────────────────────────────────────────────┘
(4 rows)

postgres=> EXPLAIN SELECT * 
                     FROM obce 
                    ORDER BY okres_id, nazev;
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│                                        QUERY PLAN                                        │
╞══════════════════════════════════════════════════════════════════════════════════════════╡
│ Incremental Sort  (cost=6.97..602.71 rows=6250 width=41)                                 │
│   Sort Key: okres_id, nazev                                                              │
│   Presorted Key: okres_id                                                                │
│   ->  Index Scan using obce_okres_id_idx on obce  (cost=0.28..190.49 rows=6250 width=41) │
└──────────────────────────────────────────────────────────────────────────────────────────┘
(4 rows)

Tím, že se neřadí celá tabulka naráz, ale blokově, tak se zvyšuje šance, že se řazený blok vejde do paměti, a bude možné použít quick sort místo pomalejšího, náročnějšího external sortu.

HashAgg respektuje nastavení work_mem

Konfigurační proměnná work_mem se používá při plánování dotazu jako limit pro rozhodování mezi paměťově náročnějšími nebo naopak úspornějšími algoritmy. Používá se také při vykonávání dotazů jako limit paměťových bufferů. Po naplnění bufferů se začne zapisovat do dočasných souborů. Tak by se měly chovat veškeré operace, které startuje executor dotazů. Výjimkou byla hash agregace (agregace, kdy mezivýsledky agregace jsou uložené v hashovací tabulce). Popsané chování nebylo ve starších verzích implementováno. Několikrát jsem se dostal do situace, kdy jsem musel penalizovat hash_agg, protože jinak docházelo ke swapování nebo k zabití Postgresu OOM kilerem. Jednalo se často o SELECT DISTINCT nad extrémně velkými tabulkami, kde muselo dojít ke špatnému ndistinct odhadu. V nové verzi již hash_agg není výjimkou, a agregace by měla být bezpečná (a v některých případech bohužel pomalejší, jako celek ale mnohem stabilnější):

Např

postgres=> SET work_mem to '200MB';
SET
postgres=> EXPLAIN ANALYZE SELECT DISTINCT 'AHOJ' || i FROM generate_series(1,1000000) g(i);
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                QUERY PLAN                                                                 │
╞═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ HashAggregate  (cost=20000.00..20003.50 rows=200 width=32) (actual time=686.754..884.658 rows=1000000 loops=1)                            │
│   Group Key: ('AHOJ'::text || (i)::text)                                                                                                  │
│   Peak Memory Usage: 98321 kB                                                                                                             │
│   ->  Function Scan on generate_series g  (cost=0.00..17500.00 rows=1000000 width=32) (actual time=100.446..304.289 rows=1000000 loops=1) │
│ Planning Time: 0.178 ms                                                                                                                   │
│ Execution Time: 935.348 ms                                                                                                                │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(6 rows)

Zde je vidět, že tento dotaz spotřeboval cca 100MB paměti. Zrovna tak by se choval ve starších verzích Postgresu a to bez ohledu na nastavení work_mem.

postgres=> SET work_mem to '4MB';
SET
postgres=> EXPLAIN ANALYZE SELECT DISTINCT 'AHOJ' || i FROM generate_series(1,1000000) g(i);
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                QUERY PLAN                                                                 │
╞═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ HashAggregate  (cost=20000.00..20003.50 rows=200 width=32) (actual time=594.709..1158.370 rows=1000000 loops=1)                           │
│   Group Key: ('AHOJ'::text || (i)::text)                                                                                                  │
│   Peak Memory Usage: 5465 kB                                                                                                              │
│   Disk Usage: 28848 kB                                                                                                                    │
│   HashAgg Batches: 116                                                                                                                    │
│   ->  Function Scan on generate_series g  (cost=0.00..17500.00 rows=1000000 width=32) (actual time=110.250..334.606 rows=1000000 loops=1) │
│ Planning Time: 0.176 ms                                                                                                                   │
│ Execution Time: 1201.208 ms                                                                                                               │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

Pokud omezím work_mem, tak stejně dojde k přetečení, nicméně skutečná hodnota se bezproblémově vejde do dvojnásobku s kterým se tak nějak počítá. Na druhou stranu agregace je o 265ms pomalejší (cca 25%). Určitě může dojít k situacím, kdy agregace (na verzi 13) bude o 25% pomalejší, ale nemělo by dojít k situaci, kdy Vám agregace odrovná server (protože vyžere veškerou paměť). Pokud byl odhad počtu unikátních hodnot v pořádku, tak i starší verze nepoužívaly hash_agg:

postgres=> SET work_mem to '64kB'; -- 12
SET
postgres=> EXPLAIN ANALYZE SELECT DISTINCT nazev 
                             FROM foo;
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                       QUERY PLAN                                                       │
╞════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Unique  (cost=16385.34..16916.59 rows=5344 width=10) (actual time=291.662..326.179 rows=5342 loops=1)                  │
│   ->  Sort  (cost=16385.34..16650.96 rows=106250 width=10) (actual time=291.660..315.851 rows=106250 loops=1)          │
│         Sort Key: nazev                                                                                                │
│         Sort Method: external merge  Disk: 2192kB                                                                      │
│         ->  Seq Scan on foo  (cost=0.00..2065.50 rows=106250 width=10) (actual time=0.021..13.926 rows=106250 loops=1) │
│ Planning Time: 0.099 ms                                                                                                │
│ Execution Time: 326.833 ms                                                                                             │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(7 rows)

-- zde se ukazuje Postgres 13 jako mnohem rychlejší
postgres=> SET work_mem to '64kB';
SET
postgres=> EXPLAIN ANALYZE SELECT DISTINCT nazev
                             FROM foo;
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                    QUERY PLAN                                                    │
╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ HashAggregate  (cost=6481.52..7572.52 rows=5341 width=10) (actual time=56.153..93.098 rows=5342 loops=1)         │
│   Group Key: nazev                                                                                               │
│   Planned Partitions: 4                                                                                          │
│   Peak Memory Usage: 93 kB                                                                                       │
│   Disk Usage: 6264 kB                                                                                            │
│   HashAgg Batches: 20                                                                                            │
│   ->  Seq Scan on foo  (cost=0.00..2065.50 rows=106250 width=10) (actual time=0.012..11.785 rows=106250 loops=1) │
│ Planning Time: 0.982 ms                                                                                          │
│ Execution Time: 94.789 ms                                                                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(9 rows)

Sort je výrazně pomalejší už kvůli použití implicitního collate , což je v mém případě "cs_CZ". V tomto ohledu je implementace v Postgresu 13 ohromný skok kupředu (a jak to vypadá finální vyřešení roky trvajícího problému, který nebyl příliš častý, ale pokud se vyskytl, tak byl velmi nepříjemný).

Vícesloupcové statistiky

Připravil jsem si pomocnou tabulku obce2 se sloupcem popisující velikost obce v textové podobě, a sloupcem, který určuje jestli je obec okresní město nebo nikoliv.

postgres=> SELECT *
             FROM obce2
            LIMIT 10;
┌────┬──────────┬─────────────┬────────────┬───────────┬─────────────┬────────────────────┐
│ id │ okres_id │    nazev    │ pocet_muzu │ pocet_zen │  velikost   │ je_okresnim_mestem │
╞════╪══════════╪═════════════╪════════════╪═══════════╪═════════════╪════════════════════╡
│  1 │ CZ0100   │ Praha       │     608316 │    640710 │ extra velka │ t                  │
│  2 │ CZ0201   │ Benešov     │       7875 │      8507 │ stredni     │ t                  │
│  3 │ CZ0201   │ Bernartice  │        108 │       115 │ mala        │ f                  │
│  4 │ CZ0201   │ Bílkovice   │         93 │        89 │ mala        │ f                  │
│  5 │ CZ0201   │ Blažejovice │         52 │        48 │ mala        │ f                  │
│  6 │ CZ0201   │ Borovnice   │         39 │        37 │ mala        │ f                  │
│  7 │ CZ0201   │ Bukovany    │        364 │       372 │ mala        │ f                  │
│  8 │ CZ0201   │ Bystřice    │       2124 │      2096 │ stredni     │ f                  │
│  9 │ CZ0201   │ Ctiboř      │         55 │        50 │ mala        │ f                  │
│ 10 │ CZ0201   │ Čakov       │         65 │        60 │ mala        │ f                  │
└────┴──────────┴─────────────┴────────────┴───────────┴─────────────┴────────────────────┘
(10 rows)

Zkusím napsat dotaz, ve kterém by se měl projevit efekt vícesloupcových statistik :

postgres=> EXPLAIN ANALYZE SELECT * 
                             FROM obce2 
                            WHERE velikost = 'stredni' AND je_okresnim_mestem;
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                             QUERY PLAN                                             │
╞════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Seq Scan on obce2  (cost=0.00..133.12 rows=16 width=35) (actual time=0.032..2.371 rows=40 loops=1) │
│   Filter: (je_okresnim_mestem AND ((velikost)::text = 'stredni'::text))                            │
│   Rows Removed by Filter: 6210                                                                     │
│ Planning Time: 0.140 ms                                                                            │
│ Execution Time: 2.412 ms                                                                           │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
(5 rows)

CREATE STATISTICS obce_velikost_je_okresnim_mestem(mcv) ON velikost, je_okresnim_mestem FROM obce2; 
ANALYZE obce2;

postgres=> EXPLAIN ANALYZE SELECT * 
                             FROM obce2 
                            WHERE velikost ='stredni' AND je_okresnim_mestem;
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                             QUERY PLAN                                             │
╞════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Seq Scan on obce2  (cost=0.00..133.12 rows=40 width=35) (actual time=0.027..2.440 rows=40 loops=1) │
│   Filter: (je_okresnim_mestem AND ((velikost)::text = 'stredni'::text))                            │
│   Rows Removed by Filter: 6210                                                                     │
│ Planning Time: 0.481 ms                                                                            │
│ Execution Time: 2.480 ms                                                                           │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
(5 rows)

V nové verzi se více sloupcové statistiky uplatní i pro filtr seznamem hodnot:

postgres=> EXPLAIN ANALYZE SELECT * 
                             FROM obce2 
                            WHERE velikost IN ('stredni', 'velka') AND je_okresnim_mestem;
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                             QUERY PLAN                                             │
╞════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Seq Scan on obce2  (cost=0.00..133.12 rows=67 width=35) (actual time=0.025..2.225 rows=67 loops=1) │
│   Filter: (je_okresnim_mestem AND ((velikost)::text = ANY ('{stredni,velka}'::text[])))            │
│   Rows Removed by Filter: 6183                                                                     │
│ Planning Time: 0.495 ms                                                                            │
│ Execution Time: 2.266 ms                                                                           │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
(5 rows)

nebo filtr podle existence prvku v poli:

postgres=> EXPLAIN ANALYZE SELECT * 
                             FROM obce2 
                            WHERE velikost = ANY (ARRAY['stredni','velka']) and je_okresnim_mestem;
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                             QUERY PLAN                                             │
╞════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Seq Scan on obce2  (cost=0.00..133.12 rows=67 width=35) (actual time=0.028..2.241 rows=67 loops=1) │
│   Filter: (je_okresnim_mestem AND ((velikost)::text = ANY ('{stredni,velka}'::text[])))            │
│   Rows Removed by Filter: 6183                                                                     │
│ Planning Time: 0.178 ms                                                                            │
│ Execution Time: 2.282 ms                                                                           │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
(5 rows)

V nové verzi je také možné explicitně nastavit přesnost vícesloupcových statistik příkazem:

ALTER STATISTICS stat_name SET STATISTICS target_value;

Optimalizace evaluace výrazů v PLpgSQL

Letos se podařila docela výrazná optimalizace v interpretu PLpgSQL . Tento jazyk (prostředí) slouží (měl by sloužit) primárně jako lepidlo pro SQL dotazy, a sám o sobě téměř nikdy nepředstavuje úzké hrdlo. Nedoporučuje se provádět výpočetně náročné úlohy v tomto jazyku. Na druhou stranu toto prostředí není až tak pomalé, a po letošní optimalizaci už se nějaký ten výpočet dá zvládnout i v PLpgSQL. Pro ukázku jsem zvolil implementaci odhadu hodnoty π (což je zrovna to, co bych v PLpgSQL za normálních okolností nikdy nedělal):

CREATE OR REPLACE FUNCTION pi_est_1(n int)
RETURNS numeric AS $$
DECLARE
  accum double precision DEFAULT 1.0;
  c1 double precision DEFAULT 2.0;
  c2 double precision DEFAULT 1.0;
BEGIN
  FOR i IN 1..n
  LOOP
    accum := accum * ((c1 * c1) / (c2 * (c2 + 2.0)));
    c1 := c1 + 2.0;
    c2 := c2 + 2.0;
  END LOOP;
  RETURN accum * 2.0;
END;
$$ LANGUAGE plpgsql;

postgres=> select pi_est_1(1000000);
┌──────────────────┐
│     pi_est_1     │
╞══════════════════╡
│ 3.14159186819215 │
└──────────────────┘
(1 row)

Time: 332,436 ms

Milion iterací trvalo cca 0.3 sec na mé 9 let staré T520 (podobný kód v Lue trval 110ms a 290ms v Pythonu (v Pythonu s vyšší přesností)). V předchozí verzi ten samý kód běžel 835ms.

V běžném kódu takové zrychlení určitě nebude - úzkým hrdlem budou vždy operace s daty v databázi, načíst a připravit data z databáze je samozřejmě výrazně náročnější než spočítat pár výrazů.

psql

Ve verzi 13 jsou v psql pouze kosmetické změny. Nově je to příkaz \warn, kterým se zapíše řetězec do stderr. Nově máme možnost parametrizovat zobrazení u variant příkazu \g. Použitá syntax pro zápis seznamu parametrů formátu je docela specifická (oddělovačem je mezera) a náhodou koreluje se zápisem connection stringu.

postgres=> SELECT 10, 'ahoj', 20;
┌──────────┬──────────┬──────────┐
│ ?column? │ ?column? │ ?column? │
╞══════════╪══════════╪══════════╡
│       10 │ ahoj     │       20 │
└──────────┴──────────┴──────────┘
(1 row)

postgres=> SELECT 10, 'ahoj', 20 \g (format=csv tuples_only=on)
10,ahoj,20

K čemu se to hodí? Můžeme si snáz vytvářet psql makra, jako v následujícím příkladu. Makro, které výsledek dotazu zobrazí v gplot(u):

postgres=> \set gnuplot '\\g (format=csv) |gnuplot -p -e "set datafile separator \',\'; set key autotitle columnhead; set terminal dumb enhanced;" -e' 

postgres=> SELECT i, sin(i) FROM generate_series(0, 6.3, 0.05) g(i) :gnuplot "plot '-'with lines ls 1"

                                                                               
    1 +--------------------------------------------------------------------+   
      |         ***      ***        +        +         +         +         |   
  0.8 |-+     ***          **                                  sin *******-|   
      |      **              **                                            |   
  0.6 |-+   **                **                                         +-|   
      |    *                    *                                          |   
  0.4 |-+ *                      *                                       +-|   
      |  *                        *                                        |   
  0.2 |-*                          *                                     +-|   
      |*                            *                                      |   
    0 |-+                            *                              *    +-|   
      |                               *                            *       |   
 -0.2 |-+                              *                          *      +-|   
      |                                 *                        *         |   
 -0.4 |-+                                *                      *        +-|   
      |                                   *                    *           |   
 -0.6 |-+                                  *                 **          +-|   
      |                                     **              **             |   
 -0.8 |-+                                    ***          **             +-|   
      |         +         +         +        + ***     +***      +         |   
   -1 +--------------------------------------------------------------------+   
      0         1         2         3        4         5         6         7   

Bohužel Gnome Terminal neumí jemnou grafiku (ReGIS nebo SIXEL ), takže žádná velká paráda se zde dělat nedá (třeba jednou v budoucnu se možnosti Gnome terminálu zlepší - terminál na mac Os by jemnou grafiku měl zvládat).

Zajímavou novinkou, která je vidět ve výchozí konfiguraci je zobrazení stavu transakce v promtu.

postgres=> \conninfo -- autocommit on
You are connected to database "postgres" as user "pavel" via socket in "/tmp" at port "5432".
postgres=> BEGIN; 
BEGIN
postgres=*> SELECT 1; -- otevřená transakce - symbol *
┌──────────┐
│ ?column? │
╞══════════╡
│        1 │
└──────────┘
(1 row)

postgres=*> SELECT x;
ERROR:  column "x" does not exist
LINE 1: SELECT x;
               ^
postgres=!> ROLLBACK; -- poškozená transakce - symbol !
ROLLBACK
postgres=> 

A když už popisuji nastavení promptu, počínaje touto verzí lze nastavit neviditelnou položku o stejné šířce jako jako je PROMPT1 do PROMPT2 (vztahuje se na druhý řádek a následující řádky). Pokud ve výchozí konfiguraci napíši dotaz přes více řádků, pak se bude aplikovat PROMPT2 a v psql uvidím následující řádky:

postgres=> SELECT *
postgres->   FROM obce;

PROMPT2 docela překáží při kopírování (z okna terminálu). Pokud bych si zase PROMPT2 nastavil na prázdný řetězec, tak budu mít rozhozené formátování. V nové verzi lze nastavit neviditelný PROMPT2 s šířkou která vždy odpovídá PROMPT1:

postgres=> \set PROMPT2 %w
postgres=> SELECT *
              FROM obce;

Administrace

DROP DATABASE FORCE

I tak banální operace jako je dropnutí (odstranění) databáze může být někdy problém. Postgres neumožní odstranění databáze, pokud je k ní někdo připojený. U malé aplikace nebude problém dohledat a zavřít příslušné spojení. V systémech, kde do databází přistupují desítky různých aplikací navíc ještě s poolovanými spojeními, to už může být těžší. Přikaz DROP DATABASE v nové verzi umožňuje další parametrizaci. Zatím jediným parametrem je parametr FORCE. Tento parametr zajistí uzavření všech spojení a zajistí odstranění databáze bez čekání na zavření spojení. Pozor na to. To, že DROP DATABASE ve starší verzi vždy čekalo, až k databázi nebude nikdo připojený (5 sec a pak skončilo chybou), určitě mnohým DBA zachránilo kůži.

Možnost modifikace systémových tabulek a jejich parametrů

Je obecně přijímaným pravidlem, že na systémové tabulky se nesahá. Jako vždy a všude existují vyjímky. Někdy je nutné nastavit mnohem agresivnější autovacuum a to i u systémových tabulek. Ve starších verzích se muselo restartovat do single modu. V této verzi už může potřebný zásah udělat superuser po nastavení proměnné allow_system_table_mods. Opět pozor - nevhodnou úpravou systémové tabulky můžete zrušit celou databázi (možná opravitelnou v single modu) a s určitou dávkou štěstí nebo smůly i celou instanci.

postgres=# ALTER TABLE pg_catalog.pg_depend SET (autovacuum_vacuum_scale_factor = 0);
ERROR:  permission denied: "pg_depend" is a system catalog
postgres=# ALTER TABLE pg_catalog.pg_depend SET (autovacuum_vacuum_threshold = 1000);
ERROR:  permission denied: "pg_depend" is a system catalog
postgres=# set allow_system_table_mods = on;
SET
postgres=# ALTER TABLE pg_catalog.pg_depend SET (autovacuum_vacuum_scale_factor = 0);
ALTER TABLE
postgres=# ALTER TABLE pg_catalog.pg_depend SET (autovacuum_analyze_threshold = 1000);
ALTER TABLE

Nyní bude tabulka pg_depend vacuovaná vždy po modifikaci jednoho tisíce řádků.

Bezpečné extenze (trusted extensions)

Drtivou většinu extenzí mohl aktivovat pouze superuser. Zřejmě u většiny extenzí je to zbytečně přísné omezení, jelikož instalace samotných extenzí se provádí pod adminem operačního systému, který tím pádem za instalované extenze zodpovídá. V nové verzi se proto zavádí nový pojem - bezpečná (trusted) extenze, kterou může instalovat i běžný uživatel, který má právo CREATE pro danou databázi.

SELECT * 
  FROM pg_available_extension_versions() e(name, version, superuser, trusted, relocatable, schema, requires, comment) 
 WHERE trusted;

Aktuálně se za bezpečné považují extenze: btree_gin, btree_gist, citext, cube, dict_int, earthdistance, fuzzystrmatch, hstore, intarray, isn, lo, ltree, pg_trgm, pgcrypto, plpgsql, seg, tablefunc, tcn, tsm_system_rows, tsm_system_time, unaccent.

Vzorkování logování dotazů

Vzorkování (sampling) dotazů se už dostalo do masteru předchozí verze, ale na poslední chvíli se tato funkce revertovala. Jak se ukázalo, originální návrh šel proti stávajícímu logování (pomalých) dotazů. Nově máme dvě konfigurační proměnné: log_min_duration_sample a log_statement_sample_rate (0.0 - 1.0).

Platilo, a bude platit, že pokud příkaz trvá déle než je hodnota log_min_duration_statement, tak se vždy zaloguje. Takže např. pro log_min_duration_statement='200ms' jakýkoliv dotaz nad 200ms bude zalogován. Logováním všech rychlejších dotazů, která jsou většinou častější (na soudobých serverech s OLTP provozem je 95% kvantil dost často 20ms), bych si mohl způsobit vážné provozní problémy zahlcením IO (ať už by šlo o výkon nebo kapacitu). Naopak, při zvážení rizik, je dobré, alespoň občas zalogovat vše, nebo téměř vše, aby vývojáři měli přehled jaké dotazy generuje aplikace.

Riziko zahlcení IO můžeme redukovat vzorkováním (zalogujeme zhruba každý n-tý dotaz (filtrování je založené na generování náhodného čísla od 0 do 1 a jeho porovnání s hodnotou log_statement_sample_rate)).

Předpokládá se, že log_min_duration_sample < log_min_duration_statement. Pokud by tomu bylo naopak, tak log_min_duration_sample nemá žádný efekt.

# pro zalogování cca setiny všech dotazů do 200ms, a všech nad 200ms
log_min_duration_sample = 0
log_min_duration_sample_rate = 0.01
log_min_duration_statement = '200ms'

Logování vázaných parametrů

V případě chyby dotazu, který používá vázané proměnné, může být problém, že se zaloguje chybný dotaz, ale už se nezalogují parametry dotazu tzv. vázané parametry (bind parameters):

2020-04-14 14:14:31.500 CEST [132468] ERROR:  value too long for type character varying(4)
2020-04-14 14:14:31.500 CEST [132468] STATEMENT:  insert into foo values($1)

Sice si nezahltíme logy, ale těžko budeme zjišťovat, co se vlastně stalo. V nové verzi můžeme poměrně pohodlně si nastavit logování parametrů tak, že nastavíme nenulový maximální limit pro zalogovaný paremetr (pokud je jeho hodnota větší, tak se ořízne):

SET log_parameter_max_length_on_error = 64

2020-04-14 14:34:19.769 CEST [132932] ERROR:  value too long for type character varying(4)
2020-04-14 14:34:19.769 CEST [132932] CONTEXT:  extended query with parameters: $1 = 'Ahoj svete'
2020-04-14 14:34:19.769 CEST [132932] STATEMENT:  insert into foo values($1)

Pokud bych chtěl zalogovat parametry i u dotazů, které neskončily chybou, pak mohu nastavit log_parameter_max_length. Výchozí hodnota je -1, což znamená, že runtime nemá řešit velikost a parametr má vždy zobrazit. 0 v tomto parametru vypne logování parametrů a číslo vyšší než 0 udává maximální velikost zalogované hodnoty v bajtech (po oříznutí).

log_min_duration_statement = 0 # zaloguj vše
log_parameter_max_length = -1 # zaloguj kompletní obsah (default)

V logu:

2020-04-14 14:42:45.968 CEST [133091] LOG:  duration: 0.444 ms  parse <unnamed>: insert into foo values($1)
2020-04-14 14:42:45.968 CEST [133091] LOG:  duration: 0.115 ms  bind <unnamed>: insert into foo values($1)
2020-04-14 14:42:45.968 CEST [133091] DETAIL:  parameters: $1 = 'Ahoj'
2020-04-14 14:42:45.968 CEST [133091] LOG:  duration: 0.106 ms  execute <unnamed>: insert into foo values($1)
2020-04-14 14:42:45.968 CEST [133091] DETAIL:  parameters: $1 = 'Ahoj'

Progres report

Postupně, ke všem déle trvajícím úlohám, jsou k dispozici odpovídající progress pohledy. Ve verzi 13 je to pro příkaz ANALYZE pohled pg_stat_progress_analyze, a pro příkaz pg_base_backup pohled pg_stat_progress_basebackup. Bylo by hezké, kdyby někdo se začínajících programátorů prointegrovali tyto pohledy s psqlkem.

Zneplatnění (invalidace) replikačních slotů

Relativně častým důvodem nedostatku místa na disku je zapomenutý replikační slot. Replikační slot v Postgresu je pojmenovaný databázový objekt, na který se odkazuje sekundární server, a který udržuje informaci o komunikaci mezi primárním serverem (replikovaným serverem) a sekundárním serverem (replikou). Pokud replikace běží vůči replikačnímu slotu, tak máme garantováno, že jde vždy obnovit přerušenou replikaci. Replikační slot na primárním serveru zabraňuje rotaci nezreplikovaných transakčních logů.

Určitě se několikrát stalo, že uživatel zastavil repliku a neodstranil replikační slot. Od toho momentu se mu na primárním serveru začaly hromadit transakční logy. Buďto si toho všiml, a replikační slot odstranil, nebo mu došlo místo na disku. V nové verzi je možné nastavit maximální velikost transakčních logů chráněných před rotací replikačním slotem a to konfigurační proměnnou max_slot_wal_keep_size. Po dosažení této velikosti je slot zneplatněn, a rezervované transakční logy budou odstraněné. Stav slotu je vidět ve sloupci wal_status v pohledu pg_replication_slots. Pozor, ve výchozí konfiguraci je tato pojistka neaktivní.

pg_rewind

Pomocí nástroje (aplikace) pg_rewind můžeme "opravit" instanci, která se používala jako primární server, a poté ji použít jako hot standby server. V nové verzi tato aplikace automaticky zjistí, jestli byl server čistě vypnut. Pokud nebyl, tak jej nastartuje v single modu, aby na instanci mohla proběhnout obnova po havárii (crash recovery).

Nově také implementuje přepínač -R (se stejným významem jako v pg_basebackup). Ve starších verzích tato volba způsobila vygenerování souboru recovery.conf, který rovnou obsahoval konfiguraci, která nám umožnila nahodit instanci jako standby server. Nově se už recovery.conf nepoužívá. Potřebná konfigurace se zapíše do postgresql.auto.conf a vygeneruje se signální soubor.

Od verze 13 pg_rewind si také může stáhnout chybějící transakční logy (potřebné pro modifikaci instance) z archívu transakčních logů (umí použít konfigurační proměnnou restore_command). Ve starších verzích se transakční logy musely kopírovat ručně.

Ostatní

S volbou PARALLEL příkaz VACUUM může souběžně vacuuovat indexy nad vacuovanou tabulkou. Tuto volbu nelze použít v příkazu VACUUM FULL.

V tabulce pg_stat_activity je nový sloupec leader_pid. Zde je aktuální číslo procesu nebo číslo rodičovského procesu v případě pomocných procesů nastartovaných pro paralelní zpracování dotazu.

Snad všichni známe funkci a smysl servisního procesu autovacuum. Monitoruje změny v tabulce, a při dosažení určitého poměru změněných řádků vůči celkovému počtu řádků určité tabulky spustí VACUUM nebo ANALYZE nad touto tabulkou. Na tabulkách, do kterých se pouze vkládalo, se ve starších verzích příkaz VACUUM nespouštěl. Po dosažení určitého počtu transakcí se automaticky pustil příkaz VACUUM FREEZE, který mohl nad takovými tabulkami nepříjemně dlouho intenzivně zatěžovat IO, právě proto, že tyto tabulky nikdy nebyly vakuované běžným (líným) příkazem VACUUM. Od této verze se bude příkaz VACUUM pouštět automaticky i nad tabulkami, kde se pouze vkládá.

Pro zajištění konzistence databáze, implementace transakcí (ACID) si databáze udržuje určitá interní data. Rychlý přístup k těmto interním (perzistentním) datům umožňuje LRU cache (v Postgresu se používá zkratka SRLU "S" jako Simple). Práce s těmito daty je pro uživatele transparentní. V případě, že se tato data zvětší nad únosnou mez, může dojít k výkonnostním problémů. Doposud se tato situace dost obtížně detekovala. S novým stat pohledem pg_stat_slru by to mělo být výrazně jednodušší.

Přes nový pohled pg_shmem_allocations se lze pohodlně podívat na alokace ve sdílené paměti.

postgres=> SELECT * FROM pg_shmem_allocations ORDER BY allocated_size DESC;
┌─────────────────────────────────────┬───────────┬───────────┬────────────────┐
│                name                 │    off    │   size    │ allocated_size │
╞═════════════════════════════════════╪═══════════╪═══════════╪════════════════╡
│ Buffer Blocks                       │   6442752 │ 134217728 │      134217728 │
│ <anonymous>                         │           │   4726400 │        4726400 │
│ XLOG Ctl                            │     53504 │   4208272 │        4208384 │
│                                     │ 147147008 │   1922816 │        1922816 │
│ Buffer Descriptors                  │   5394176 │   1048576 │        1048576 │
│ clog                                │   4262272 │    529184 │         529280 │
│ Buffer IO Locks                     │ 140660480 │    524288 │         524288 │
│ Checkpointer Data                   │ 146669056 │    393280 │         393344 │
│ Checkpoint BufferIds                │ 141184768 │    327680 │         327680 │
│ subtrans                            │   4925312 │    267040 │         267136 │
│ oldserxid                           │ 146098176 │    133600 │         133632 │
│ multixact_member                    │   5259392 │    133600 │         133632 │
│ commit_timestamp                    │   4791552 │    133600 │         133632 │
│ Backend Activity Buffer             │ 146453376 │    132096 │         132096 │
│ shmInvalBuffer                      │ 146590208 │     69464 │          69504 │
│ async                               │ 147080064 │     66848 │          66944 │

Některé operace s daty mohou být akcelerovány přednačítáním (prefetch). Míru přednačítání určuje proměnná effective_io_concurrency. Adekvátní nastavení této hodnoty je mně neznámá alchymie vyššího zasvěcení (doporučuje se nastavit od hodnot 4, 200, 1000, ..). Prefetch má smysl i pro servisní operace. Ve starších verzích se používala pofidérní heuristika, kterou v nové verzi nahradila konfigurační proměnná maintenance_io_concurrency.

Pomocí pg_dumpu můžeme nově můžeme exportovat i obsah cizích tabulek (přepínač --include-foreign-data=název_cizího_serveru).

pg_basebackup nově generuje tzv backup manifest. To je soubor formátu JSON s informacemi popisujícími záložní soubory (obsah zálohy). Na základě tohoto souboru pak nový nástroj pg_verifybackup může provést kontrolu zálohy.

Logická replikace je v Postgresu třetím rokem (počínaje verzí 10), a nyní se pracuje na větší robustnosti provozu. Jedním z problémů byla vysoká spotřeba paměti na primárním serveru v případě replikace transakcí s velkým množstvím subtransakcí. V Postgresu k tomu stačí blok se zachytáváním výjimky v PLpgSQL. V nové verzi by problém s pamětí alokovanou replikací (konkrétně procesem walsender) měl být redukován limitem logical_decoding_work_mem. Po naplnění bufferů o této velikosti se pracovní data pro replikaci začnou serializovat na disk. Výchozí hodnota je 64MB.

Upravila se reakce serveru na signál promote (přepnutí z režimu hot standby do režimu primárního). V předchozích verzích mohl tento signál čekat na ukončení pauzy (stav pause). Pokud byla replikace zapauzovaná, tak ani promote replikaci nerozjel, tudíž se server nemohl doreplikovat a nemohl změnit na primární server. V nové verzi promote zruší pauzu, server se doreplikuje a promote se provede bez v drtivé většině nechtěného odkladu.

Podpora Unicode

Podpora escape sekvencí v databázích s jiným než UTF8 kódováním

Escape zápis unicode znaků by je nyní možný i v těch případech, kdy databáze je v jiném kódování než UTF8

postgres=> CREATE DATABASE pokus WITH template='template0' encoding='latin2' locale='cs_CZ.latin2';
CREATE DATABASE
postgres=> \c pokus
You are now connected to database "pokus" as user "pavel".

pokus=# INSERT INTO foo VALUES(U&'\010D'); -- č
INSERT 0 1
pokus=# INSERT INTO foo VALUES(U&'\0424'); -- Ф 
ERROR:  character with byte sequence 0xd0 0xa4 in encoding "UTF8" has no equivalent in encoding "LATIN2"
LINE 1: INSERT INTO foo VALUES(U&'\0424');
                                  ^

Normalizace Unicode znaků

V znakové sadě Unicode může být jeden znak kódován různě. Např. dlouhé velké A "Á" může být zakódováno jako U00C1 nebo sekvence U0041, U0301 (pozn. naštěstí se druhá forma příliš nepoužívá). Před porovnáním řetězců na shodu by se řetězce měly tzv normalizovat.

postgres=> SELECT U&'\00C1' c1,U&'\0041\0301' c2, U&'\00C1' = U&'\0041\0301' eq;
┌────┬────┬────┐
│ c1 │ c2 │ eq │
╞════╪════╪════╡
│ Á  │ Á  │ f  │
└────┴────┴────┘
(1 row)

Jak vidíte, dva stejné znaky jsou pro Postgres různé znaky. V nové verzi můžeme použít funkci normalize a test IS NORMALIZED:

postgres=> SELECT U&'\00C1' c1,
                  U&'\0041\0301' c2,
                  U&'\00C1' = U&'\0041\0301' eq, 
                  U&'\00C1' IS NORMALIZED,
                  U&'\0041\0301' IS NORMALIZED,
                  normalize(U&'\00C1') = normalize(U&'\0041\0301') eq2;
┌────┬────┬────┬───────────────┬───────────────┬─────┐
│ c1 │ c2 │ eq │ is_normalized │ is_normalized │ eq2 │
╞════╪════╪════╪═══════════════╪═══════════════╪═════╡
│ Á  │ Á  │ f  │ t             │ f             │ t   │
└────┴────┴────┴───────────────┴───────────────┴─────┘
(1 row)

Partitioning

Oproti loňské verzi letos nejsou žádné dramatické změny v implementaci partitioningu. Určitě krokem kupředu je podpora BEFORE triggerů nad partitiovanými tabulkami. Zde zůstává jedno omezení. Záznam i po úpravě v before triggeru musí zůstat ve stejné partition.

Pro logickou replikace byly partitions samostatné tabulky a replikovaly se samostatně. Nově je možné nastavit publikační sadě příznak publish_via_partition_root. Tím se replikace změn obsahu partitions přesměruje na kořenovou partitiovanou tabulku. U takto nakonfigurované replikace může mít příjemce (konzument) jinou strukturu partitions než má producent.

Postgres už dříve podporoval partitionwise join (snahou je spojovat menší partitions místo větších partitiovaných tabulek). Ve starší verzi partitionwise join byl možný pouze tehdy, když počet partitions spojovaných partitiovaných tabulek byl stejný. Nový algoritmus pouze vyžaduje, aby každá partition partitiované tabulky byla joinována přesně s jednou partition druhé partitiované tabulky. Nově je možné partitionwise join použít i pro FULL JOIN.

SQL

FETCH FIRST WITH TIES

Klauzule WITH TIES volitelně doplňuje klauzuli FETCH FIRST n, která je ANSI SQL analogií proprietární kluzule LIMIT, která jak jistě víte, omezí počet řádků na výstupu. FETCH FIRST n WITH TIES omezí počet řádků na n plus ještě ty řádky, kde hodnota podle které se řadí je stejná jako u n tého řádku.

Dejme tomu, že bych chtěl vidět 3 nejmenší obce z české republiky. Napíši jednoduchý dotaz

postgres=> SELECT *
             FROM obce
            ORDER BY pocet_zen + pocet_muzu
            FETCH FIRST 3 ROW ONLY;
┌──────┬──────────┬─────────┬────────────┬───────────┬──────────┬─────────┐
│  id  │ okres_id │  nazev  │ pocet_muzu │ pocet_zen │ vek_muzu │ vek_zen │
╞══════╪══════════╪═════════╪════════════╪═══════════╪══════════╪═════════╡
│ 5028 │ CZ0646   │ Březina │          3 │         0 │     60.2 │     0.0 │
│ 1244 │ CZ0311   │ Vlkov   │          9 │         9 │     55.3 │    53.7 │
│ 2158 │ CZ0326   │ Čilá    │          8 │        14 │     46.6 │    65.9 │
└──────┴──────────┴─────────┴────────────┴───────────┴──────────┴─────────┘
(3 rows)

Nicméně právě u tohoto dotazu výsledek není úplný. Existuje obec Vysoká Lhota se stejným počtem obyvatel jako obec Čilá. V závislosti na fyzickém uložení, algoritmu řazení se nějak zvolila jedna obec a druhá se nezobrazila. Pokud bych chtěl zabránit této situaci, tak mohu použít klauzuli WITH TIES:

postgres=> SELECT *
             FROM obce 
            ORDER BY pocet_zen + pocet_muzu 
            FETCH FIRST 3 ROW WITH TIES;
┌──────┬──────────┬──────────────┬────────────┬───────────┬──────────┬─────────┐
│  id  │ okres_id │    nazev     │ pocet_muzu │ pocet_zen │ vek_muzu │ vek_zen │
╞══════╪══════════╪══════════════╪════════════╪═══════════╪══════════╪═════════╡
│ 5028 │ CZ0646   │ Březina      │          3 │         0 │     60.2 │     0.0 │
│ 1244 │ CZ0311   │ Vlkov        │          9 │         9 │     55.3 │    53.7 │
│ 2158 │ CZ0326   │ Čilá         │          8 │        14 │     46.6 │    65.9 │
│ 4225 │ CZ0633   │ Vysoká Lhota │         10 │        12 │     58.0 │    65.5 │
└──────┴──────────┴──────────────┴────────────┴───────────┴──────────┴─────────┘
(4 rows)

Nestane se mi, že bych v důsledku oříznutí přišel o nějaký řádek. Na druhou stranu nemusím dostat přesně n řádků (v mezních případech mohu dostat signifikantně více řádků, což by bylo řešitelné zanořením a dalším oříznutím). Čím víc nad tím přemýšlím, tak mi to přijde jako šikovná věc, a konečně důvod, abych se naučil (a občas použil) ANSI SQL syntax (proprietární klauzule LIMIT má, přeci jen, výrazně kratší zápis).

Nové funkce a rozšíření stávajících funkcí

Nově jsou v Postgresu k dispozici funkce gcd (greatest common divisor) největší společný dělitel a lcm (least common multiple) nejmenší společný násobek. Tyto funkce zatím nikomu nechyběly. Do Postgresu se implementovaly spíš z důvodu úplnosti knihovny funkcí.

Naopak implementace funkcí trim_scale a num_scale byla dlouho na seznamu Postgresového ToDo (komunitní ToDo je spíš skrumáž nápadů, co by se někdy mohlo, než seriózně míněný seznam toho, co by se mělo udělat). trim_scale ořezává zbytečné nuly za desetinou čárkou u čísel typu numeric:

postgres=> SELECT 10.20 * 10.40;
┌──────────┐
│ ?column? │
╞══════════╡
│ 106.0800 │
└──────────┘
(1 row)

postgres=> SELECT trim_scale(10.20 * 10.40), min_scale(10.20 * 10.40);
┌────────────┬───────────┐
│ trim_scale │ min_scale │
╞════════════╪═══════════╡
│     106.08 │         2 │
└────────────┴───────────┘
(1 row)

min_scale vrací počet nezbytných desetinných čísel z čísla, tak aby nedošlo ke ztrátě informace.

Funkce to_char, to_timestamp nově pracují se formátovacím vzorem FF1-FF6. Tyto nové vzory slouží k určení desetiné části sekundy (desetina sec, setina sec, tisícina sec, ...)

postgres=> SELECT to_char(current_timestamp, 'hh:mm:ss.ff1');
┌────────────┐
│  to_char   │
╞════════════╡
│ 07:04:44.7 │
└────────────┘
(1 row)

Nově se také zavádí SSSSS, který vyjadřuje počet vteřin po půlnoci.

postgres=> SELECT to_char(current_timestamp, 'YYYY-MM-DD SSSSS');
┌──────────────────┐
│     to_char      │
╞══════════════════╡
│ 2020-04-14 25378 │
└──────────────────┘
(1 row)

Funkce to_char roky podporuje vzor TM (translated mode), který zobrazuje hodnoty v národním prostředí. Počínaje verzí 13 můžeme tento vzor použít i ve funkcích to_date a to_timestamp:

postgres=> SELECT to_char(current_date, 'DD.MM.YYYY TMDAY DD TMMONTH');
┌───────────────────────────┐
│          to_char          │
╞═══════════════════════════╡
│ 14.04.2020 ÚTERÝ 14 DUBNA │
└───────────────────────────┘
(1 row)

postgres=> SELECT to_date('15 dub 2020', 'DD TMMON YYYY'), to_date('15 dubna 2020', 'DD TMMONTH YYYY');
┌────────────┬────────────┐
│  to_date   │  to_date   │
╞════════════╪════════════╡
│ 2020-04-15 │ 2020-04-15 │
└────────────┴────────────┘
(1 row)

Je to dost citlivé na zápis, takže pro češtinu to bude použitelné pouze pro parsování strojově generovaných dat, kde je dodržená konvence.

Vloni se vedla diskuze ohledně chování funkce jsonb_set. Pro některé uživatele není jednoduché rozlišit mezi SQL NULLem (a odpovídajícím chováním) a JSON nullem a jeho jiným chováním.

U STRICT funkcí jako je např. jsonb_set se neřeší typ NULL hodnoty - NULL je NULL.

-- typový NULL, nicméně stále SQL NULL
postgres=> SELECT jsonb_set('{"f1":1,"f2":2}', '{f2}', null::jsonb);
┌───────────┐
│ jsonb_set │
╞═══════════╡
│           │
└───────────┘
(1 row)

-- použití JSONB nullu
postgres=> SELECT jsonb_set('{"f1":1,"f2":2}', '{f2}', 'null'::jsonb);
┌───────────────────────┐
│       jsonb_set       │
╞═══════════════════════╡
│ {"f1": 1, "f2": null} │
└───────────────────────┘
(1 row)

Změnit nekompatibilním způsobem chování funkce je i pro vývojáře Postgresu i pro uživatele nepříjemná záležitost, a tudíž jediným řešením je zavedení nové funkce. Tou je funkce jsonb_set_lax, která není STRICT (a tudíž si ošetřuje NULL vstup sama), a kde uživatel může (skrze volitelné parametry funkce) určit chování funkce v případě, že vstupní argument je NULL (výchozí je "use_json_null", alternativy "raise_exception", "delete_key" a "return_target").

postgres=> select jsonb_set_lax('[{"f1":99,"f2":null},2]', '{0,f3}', null);
┌─────────────────────────────────────────┐
│              jsonb_set_lax              │
╞═════════════════════════════════════════╡
│ [{"f1": 99, "f2": null, "f3": null}, 2] │
└─────────────────────────────────────────┘
(1 row)

Změnil se algoritmus pro výpočet odmocniny logaritmu (nově se používá Karatsuba), který je přesnější (správně se zaokrouhluje poslední číslice) a rychlejší (3-5x u malých čísel a cca 10x u velkých čísel). Tuto změnu možná ocení uživatele PostGISu.

Nová třída polymorfních datových typů - anycompatible

Polymorfní typy v PostgreSQL jsou analogií generickým typům v programovacích jazycích, které podporu generik mají. V předchozích verzích Postgresu byly tři základní třídy: "any" (musí se zapisovat v uvozovkách, protože any je klíčové slovo), anyenum a anyelement. "any" je možné použít pouze pro vstupní argumenty a pouze pro funkce implementované v jazyku C. Na typ anyelement navazují typy anyarray, anynonarray a anyrange. Funkce, které používají typ anyelement jsou typově velice striktní, což je někdy chtěná vlastnost, ale také často vlastnost nechtěná, omezující (např. většina vestavěných funkcí se tak nechová):

CREATE OR REPLACE FUNCTION public.make_array(anyelement, anyelement)
RETURNS anyarray
AS $$
  SELECT ARRAY[$1, $2];
$$
LANGUAGE sql IMMUTABLE;

postgres=> SELECT make_array(1, 1);
┌────────────┐
│ make_array │
╞════════════╡
│ {1,1}      │
└────────────┘
(1 row)

postgres=> SELECT make_array(.1, .1);
┌────────────┐
│ make_array │
╞════════════╡
│ {0.1,0.1}  │
└────────────┘
(1 row)

postgres=> SELECT make_array(.1, 1);
ERROR:  function make_array(numeric, integer) does not exist
LINE 1: SELECT make_array(.1,1);
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

Na příkladu je vidět, že poslední volání funkce selhalo, protože typ numeric není stejným typem jako typ integer. Pokud by funkce použila novou třídu anycompatible (pozor, je úplně nezávislá na anyelement), tak se nejprve otestuje, zda-li jsou skutečné typy anycompatible argumentů ze stejné kategorie (text, číslo, datum, ..) a pokud ano, tak se hodnoty těchto parametrů převedou na společný typ:

postgres=> SELECT make_array(.1, 1);
┌────────────┐
│ make_array │
╞════════════╡
│ {0.1,1}    │
└────────────┘
(1 row)

postgres=> \gdesc
┌────────────┬───────────┐
│   Column   │   Type    │
╞════════════╪═══════════╡
│ make_array │ numeric[] │
└────────────┴───────────┘
(1 row)

postgres=> SELECT make_array(.1::double precision, 1);
┌────────────┐
│ make_array │
╞════════════╡
│ {0.1,1}    │
└────────────┘
(1 row)

postgres=> \gdesc
┌────────────┬────────────────────┐
│   Column   │        Type        │
╞════════════╪════════════════════╡
│ make_array │ double precision[] │
└────────────┴────────────────────┘
(1 row)

Pomocí této třídy polymorfních typů se jednoduše implementují funkce jako nvl (z Oracle):

CREATE OR REPLACE FUNCTION nvl(anycompatible, anycompatible)
RETURNS anycompatible AS $$
  SELECT coalesce($1, $2)
$$ LANGUAGE sql IMMUTABLE;

Nebude to sice absolutně kompatibilní s Oraclem, ale pro téměř všechny rozumné kombinace to kompatibilní bude. Pozn. Oracle u této a podobných funkcích převede všechny parametry na text, pokud je první parametr text. Postgres naopak implicitní přetypování na text nikdy nedělá. Orafce si pomáhá přetížením funkcí (overloadingem):

CREATE OR REPLACE FUNCTION nvl(text, anyelement)
RETURNS text AS $$
  -- vynucené přetypování
  SELECT coalesce($1, $2::text)
$$ LANGUAGE sql IMMUTABLE;

-- první parametr určuje datový typ
CREATE OR REPLACE FUNCTION nvl(numeric, text)
RETURNS numeric AS $$
  -- vynucené přetypování
  SELECT coalesce($1, $2::numeric)
$$ LANGUAGE sql IMMUTABLE;

CREATE OR REPLACE FUNCTION nvl(date, text)
RETURNS date AS $$
  -- vynucené přetypování
  SELECT coalesce($1, $2::date)
$$ LANGUAGE sql IMMUTABLE;

To ale není úplně korektní (používá se nezdokumentovaná (negarantovaná) vlastnost Postgresu). Pro minimalizaci režie migrace z Oracle do Postgresu je to bohužel nutné (a nečekám, že by Postgres o tuto nezdokumentovanou vlastnost přišel). Typový systém Oracle je pro nejčastěji používané datové typy (varchar2, number a date) výrazně jednodušší a dynamičtější než u Postgresu, a nelze jej bez výjimky emulovat v Postgresu (kde se používá výrazně statičtější striktnější složitější typový systém).

Obecně se nedoporučuje přetěžovat polymorfní parametry funkcí. Pokud se přetížení funkcí neudělá správně, tak pak při volání funkce runtime nemůže (nemusí) jednoznačně určit instanci funkce, což vždy skončí runtime chybou (záměrně vytvořené nevhodné přetížení funkce nvl):

ERROR:  function nvl(integer, numeric) is not unique
LINE 1: select nvl(1, 1.1);
               ^
HINT:  Could not choose a best candidate function. You might need to add explicit type casts.

Je bezpečnější se nespoléhat na implicitní přetypování, a v Postgresu si nekorektní (alespoň z pohledu Postgresu) situace ošetřit explicitním přetypováním (tudíž mít pouze polymorfní funkci), tj výraz nvl('AHOJ', 1) přepsat na nvl('AHOJ', 1::text).

Explain

Poměrně značného rozšíření se dočkal příkaz EXPLAIN (a odpovídajícím způsobem i extenze auto_explain). S volbou SETTINGS se do explainu propíše veškeré nedefaultní nastavení, které ovlivňuje optimalizátor Postgresu.

postgres=> EXPLAIN (ANALYZE, SETTINGS)
            SELECT *
              FROM obce 
             ORDER BY pocet_muzu + pocet_zen DESC
             LIMIT 10;
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                    QUERY PLAN                                                     │
╞═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Limit  (cost=272.19..272.21 rows=10 width=45) (actual time=11.244..11.246 rows=10 loops=1)                        │
│   ->  Sort  (cost=272.19..287.81 rows=6250 width=45) (actual time=11.241..11.242 rows=10 loops=1)                 │
│         Sort Key: ((pocet_muzu + pocet_zen)) DESC                                                                 │
│         Sort Method: top-N heapsort  Memory: 27kB                                                                 │
│         ->  Seq Scan on obce  (cost=0.00..137.12 rows=6250 width=45) (actual time=0.037..4.854 rows=6250 loops=1) │
│ Settings: work_mem = '64kB'                                                                                       │
│ Planning Time: 0.268 ms                                                                                           │
│ Execution Time: 11.323 ms                                                                                         │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(8 rows)

Naopak s volbou WAL se zobrazí informace o zápisu do transakčního logu:

postgres=> EXPLAIN (ANALYZE,WAL,BUFFERS)
            INSERT INTO test SELECT generate_series(1,1000);
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                             QUERY PLAN                                             │
╞════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Insert on test  (cost=0.00..15.02 rows=1000 width=4) (actual time=1.675..1.675 rows=0 loops=1)     │
│   Buffers: shared hit=1010 dirtied=5 written=5                                                     │
│   WAL:  records=1000  bytes=59000                                                                  │
│   ->  ProjectSet  (cost=0.00..5.02 rows=1000 width=4) (actual time=0.004..0.148 rows=1000 loops=1) │
│         ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.002..0.002 rows=1 loops=1)     │
│ Planning Time: 0.053 ms                                                                            │
│ Execution Time: 1.703 ms                                                                           │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
(7 rows)

Zajímavou novou funkcí je zobrazení počtu přístupu k bufferům v době plánování dotazu (planning time). Několikrát jsem se setkal s tím, že v důsledku nafouklých (bloated) indexů se silně zpomalilo plánování dotazů. Postgres už v době plánování dotazů pracuje s potenciálními indexy a například dohledává minimum, maximum (a zejména pokud se jedná o dotazy na začátku session, tak si plní lokální (session) cache systémového katalogu).

postgres=> EXPLAIN (BUFFERS, ANALYZE) 
            SELECT *
              FROM obce
             WHERE okres_id = 'CZ0201';
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                          QUERY PLAN                                                          │
╞══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╡
│ Index Scan using obce_okres_id_idx on obce  (cost=0.28..14.49 rows=114 width=41) (actual time=0.026..0.057 rows=114 loops=1) │
│   Index Cond: ((okres_id)::text = 'CZ0201'::text)                                                                            │
│   Buffers: shared hit=4                                                                                                      │
│ Planning Time: 1.188 ms                                                                                                      │
│   Buffers: shared hit=124                                                                                                    │
│ Execution Time: 0.106 ms                                                                                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(6 rows)

Tak jak rozšířily možnosti příkazu EXPLAIN, tak obdobným způsobem se rozšířily možnosti extenzí pg_stat_statements a auto_explain. V pohledu pg_stat_statements jsou nyní nově metriky kvantifikující zápis do transakčního logu způsobený monitorovaným příkazem. Pokud se zapne pg_stat_statements.track_planning, pak je možné v pg_stat_statements vidět režii planeru.

Pro vývojáře postgresu

Voláním funkce MemoryContextMemAllocated lze zjistit velikost alokované paměti v daném kontextu včetně jeho potomků. StringInfo API - sada funkcí pro práci s dynamicky alokovanými řetězci lze používat i v klientských aplikacích (frontend - pg_waldump, ..). Nová konfigurační proměnná backtrace_functions umožňuje nastavit seznam funkcí, u kterých se v případě chyby vypíše backtrace do logu Postgresu.

set backtrace_functions = 'int4div';
postgres=> select 10/0;
ERROR:  division by zero

2020-04-14 09:26:07.925 CEST [121307] ERROR:  division by zero
2020-04-14 09:26:07.925 CEST [121307] BACKTRACE:
    postgres: pavel postgres [local] SELECT(int4div+0x60) [0x84ec3b]
    postgres: pavel postgres [local] SELECT() [0x65ffef]
    postgres: pavel postgres [local] SELECT(ExecInterpExprStillValid+0x2e) [0x65d069]
    postgres: pavel postgres [local] SELECT(evaluate_expr+0x80) [0x7324dc]
    postgres: pavel postgres [local] SELECT() [0x732697]
    postgres: pavel postgres [local] SELECT() [0x7345e8]
    postgres: pavel postgres [local] SELECT() [0x7329cf]
    postgres: pavel postgres [local] SELECT(expression_tree_mutator+0xa63) [0x6d062c]
    postgres: pavel postgres [local] SELECT() [0x733abb]
    postgres: pavel postgres [local] SELECT(expression_tree_mutator+0xb8c) [0x6d0755]
    postgres: pavel postgres [local] SELECT() [0x733abb]
    postgres: pavel postgres [local] SELECT(eval_const_expressions+0x40) [0x733c0c]
    postgres: pavel postgres [local] SELECT() [0x71632d]
    postgres: pavel postgres [local] SELECT(subquery_planner+0x373) [0x71ef8e]
    postgres: pavel postgres [local] SELECT(standard_planner+0x181) [0x71fc06]
    postgres: pavel postgres [local] SELECT(planner+0x19) [0x7201cb]
    postgres: pavel postgres [local] SELECT(pg_plan_query+0x44) [0x7ec79a]
    postgres: pavel postgres [local] SELECT(pg_plan_queries+0x40) [0x7ec849]
    postgres: pavel postgres [local] SELECT() [0x7ecd1e]
    postgres: pavel postgres [local] SELECT(PostgresMain+0x74e) [0x7eec5e]
    postgres: pavel postgres [local] SELECT() [0x7644aa]
    postgres: pavel postgres [local] SELECT() [0x7672bd]
    postgres: pavel postgres [local] SELECT() [0x7674c1]
    postgres: pavel postgres [local] SELECT(PostmasterMain+0xfa8) [0x76889a]
    postgres: pavel postgres [local] SELECT(main+0x1e9) [0x6b8a52]
    /lib64/libc.so.6(__libc_start_main+0xf2) [0x7f0d9a937042]
    postgres: pavel postgres [local] SELECT(_start+0x2e) [0x47f2de]
2020-04-14 09:26:07.925 CEST [121307] STATEMENT:  select 10/0;

Backtrace se zaloguje i v případě neplatného assertu. gdb backtrace to určitě nenahradí, určitě ale nějaká informace v tom je.


Co se nestihlo

Myslím si, že ve čtrnáctce bychom se mohli dočkat podpory JSONu podle ANSI/SQL včetně funkce json_table. Doufám, že snad se už do upstreamu dostane můj patch na schema variables (analogie globálních proměnných v Oracle). Dost nadějně vypadá patch implementující globální dočasné tabulky. Pořád se pracuje na index skip scanu. Tomáš pracuje na funkcionálních statikách. Dost daleko je patch implementující online materializované pohledy. V další verzi by se měl objevit nový datový typ multirange.

Závěr

Na bývalém OHLOHu (dnes Synopsys) mne zaujalo hodnocení Postgresu . Pracnost kódu je odhadnuta na 601 člověko-let. Dnes má Postgres dva milióny řádek kódu. Při hrubém odhadu (a pokud bych vzal za základ platy v ČR) může být cena vývoje Postgresu přes miliardu korun.

Ve třináctce je pro uživatele viditelných nových funkcí minimum. Drtivá většina změn je pod povrchem. Čtrnáctka by mohla být, co se týče uživatelsky viditelných funkcí, opačná. Tudíž bude se na co těšit.