PostgreSQL 9.4 (2014): transakční sql json databáze

Z PostgreSQL
Verze z 4. 2. 2015, 10:52, kterou vytvořil imported>Pavel
(rozdíl) ← Starší verze | zobrazit aktuální verzi (rozdíl) | Novější verze → (rozdíl)
Skočit na navigaci Skočit na vyhledávání

Postgres má ve svém genetickém kódu tři základní vlastnosti ACID, SQL a rozšiřitelnost. To umožňuje jednoduše použít Postgres pro správu dat v extrémně různých oborech - pro zpracování socioekonomických dat, pro zpracování vědeckých dat, pro řízení procesů. S kvalitní podporou formátu JSON (ve verzi 9.4) může Postgres konkurovat etablovaným databázím určeným pro správu dokumentů (Document Management Systems).

Přemýšlím, co mám o 9.4 a o vývoji 9.4 napsat. Je to verze, na které asi zatím pracovalo nejvíce vývojářů v historii Postgresu, a je to znát na počtu a složitosti nově implementovaných funkcí. Dokončila se implementace některých funkcí (např. editovatelné pohledy), jiné funkce konečně dostaly smysl (např. díky snížení úrovně zámku možnost odložené validace podmínek), a na jiné se opět nedostalo (Row-level security nebo moje implementace vylepšené kontroly SQL příkazů v PLpgSQL). Konečně, s vestavěnou implementací nerelačního typu jsonb se otevřel nový prostor pro nasazení PostgreSQL. Background workery (jak to přeložit?) jsou už ve stavu, kdy lze nad nimi psát smysluplné extenze a ještě v 9.4 se stihl implementovat základ pro logickou replikaci (zatím s omezenou funkcionalitou, která je zatím zajímavá jen pro autory extenzí). Udělalo se toho docela dost a nebojím se tvrdit, že proces založený na commitfestech funguje a rozumně vytyčuje mantinely pro komunitní vývoj tak, aby nedocházelo k větším konfliktům a k rozhádání komunity. To je dáno i tím, že relativně hodně vývojářů pracuje s/na Postgresu už skoro 10 let (a někteří už téměř 20 let), znají své hranice a vědí, že jiný než praktický a pragmatický přístup k vývoji a k diskuzím v mailing listu nemá smysl.

Na vývoji 9.4 se podepsala (pozdržením možná i o dva měsíce) kritická chyba (vlastně několik souvisejících chyb) v 9.3. Pro mne jednou z nejzajímavějších funkcí v 9.3 (a pro uživatele Postgresu v ČR se kterými jsem v kontaktu) byla optimalizace zamykání spojeného s implementací referenční integrity (což jsou takové ty zámky, o kterých člověk netuší, kde se mu v aplikaci berou). V důsledku této chyby mohl transakční log obsahovat některé informace ve špatném pořadí. Chyba se projevila jen při vyšší zátěži a určité souhře okolností. V každém případě ale mohla vést k poškození dat na masteru (po obnově) nebo k poškození repliky (což několik uživatelů zahlásilo). To by už samo o sobě bylo dost nepříjemné, ale přišel ještě větší problém. Opravou této chyby se zanesla nová kritická chyba do nepostižené 9.2 (opět s impaktem na uživatele vestavěné binární replikace). V krátké době se muselo vydat několik kritických oprav a teprve verze 9.3.4 je bezpečná.

Kdybych si měl vybrat svého favorita, tak ať přemýšlím jak přemýšlím, myslím si, že nejvíce uživatelů v tuto chvílí ocení podporu tzv ordered aggregates - agregačních funkcí definovaných nad seřazenými daty (určitě znáte percentily - medián je 50% percentil). Nerelační datové typy (HStore, JSON a XML) zas až tak tady rozšířené nejsou, jakkoliv jsou užitečné (např. dobře mohou nahradit EAV) a adaptace na jsonb bude trvat pár let. A pokud by pro uživatele nebyla možnost kalkulace percentilů zajímavá, pak si možná vybere z následujícího seznamu:

Rozšíření SQL

Podmíněné agregační funkce

Nově můžeme u agregační funkce použít klauzuli FILTER (WHERE ). Podobné chování jsem už dříve mohl dosáhnout pomocí vloženého výrazu CASE. Nový způsob je o něco expresivnější, přímočařejší.

postgres=# SELECT count(*), count(*) FILTER (WHERE i > 5) FROM generate_series(1,10) g(i);
 count | count
-------+-------
    10 |     5
(1 row)

Agregační funkce na seřazených datech

Některé agregační funkce má smysl počítat pouze na seřazených datech. Speciální syntaxe těchto agregačních funkcí tuto vlastnost zdůrazňuje. Funkce generate_series vrací posloupnost od 1 do 10. 50% percentil - medián je hodnota 5.5:

postgres=# SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY i) FROM generate_series(1,10) g(i);
 percentile_cont
-----------------
             5.5
(1 row)

Proprietární rozšíření funkce percentile_cont umožňuje jednorázově zadat (v poli) a naráz spočítat více percentilů (výsledkem je pole percentilů):

postgres=# SELECT percentile_cont(ARRAY[0.1,0.5,0.9]) WITHIN GROUP (ORDER BY i) FROM generate_series(1,10) g(i);
 percentile_cont
-----------------
 {1.9,5.5,9.1}
(1 row)

Agregační funkce rank (existuje také mírně odlišná analytická funkce rank) vrací hypotetické pořadí prvku v množině:

postgres=# SELECT rank(1.75) WITHIN GROUP (ORDER BY v) FROM unnest(ARRAY[2,1.3,1.4,1.6,1.8]) g(v);
 rank
------
    4
(1 row)

Slučování výsledků funkcí vracejících tabulku na základě pořadí a klauzule WITH ORDINALITY

Pomocí konstruktoru tabulky "rows from" můžeme spojovat tabulky získané jako výsledek volání SRF funkcí (Set Returning Functions) a to na základě pořadí řádků. Speciální klauzulí WITH ORDINALITY připojíme k výsledné tabulce sloupec s čísly řádků.

postgres=# SELECT * FROM ROWS FROM (generate_series(1,2), generate_series(1,3)) g(a1, a2);
 a1 | a2
----+----
  1 |  1
  2 |  2
    |  3
(3 rows)

Víceparametrický unnest je speciálním případem použití konstruktoru "rows from".

postgres=# SELECT * FROM unnest(array[1,3], array[3,4,5,6], array[0,1]) g(a,b,c);
 a | b | c
---+---+---
 1 | 3 | 0
 3 | 4 | 1
   | 5 | 
   | 6 | 
(4 rows)

postgres=# SELECT * FROM unnest(array[1,3], array[3,4,5,6], array[0,1]) WITH ORDINALITY g(a,b,c);
 a | b | c | ordinality
---+---+---+------------
 1 | 3 | 0 |          1
 3 | 4 | 1 |          2
   | 5 |   |          3
   | 6 |   |          4
(4 rows)

Klauzuli WITH ORDINALITY lze použít i pro stávající SRF funkce (a explicitně joinovat):

postgres=# SELECT * FROM generate_series(1,10,2) WITH ORDINALITY g(i, ord);
 i | ord
---+-----
 1 |   1
 3 |   2
 5 |   3
 7 |   4
 9 |   5
(5 rows)

Refaktoring implementace klauzule IF EXISTS v příkazu DROP

Všechny příkazy typu DROP nyní podporují klauzuli IF EXISTS, přičemž implementace této klauzule je vždy tolerantní vůči chybám (např. použití neexistujícího schématu, tabulky). V předchozích verzích někde klauzule IF EXISTS chyběla úplně, někde byla implementovaná, ale vyhodila vyjímku pokud obsahovalo odkaz na neexistující schéma (např. při dropnutí tabulky). Počínaje 9.4 je ve všech příkazech DROP a všude se chová stejně.

Této vlastnosti využívá implementace přepínače --if-exists příkazu pg_dump. Ten zajistí vložení klauzule IF EXISTS do příkazů DROP, které jsou vložené před příkazy CREATE TABLE (což aktivuje přepínač --clean). Motivací je eliminovat zavádějící (falešné) chyby typu "object doesn't exists", které by se zobrazily při obnově databáze.

Materializovavané a aktualizovatelné pohledy

Materializované pohledy se v PostgreSQL aktualizují explicitně příkazem REFRESH. Tento příkaz vyžaduje exkluzivní zámek i pro čtení a blokuje čtení pohledu. Příkaz REFRESH MATERIALIZED VIEW CONCURRENTLY umožní aktualizaci pohledu bez zamknutí proti čtení. Nic ale není zadarmo - aktualizace může běžet déle a vyžaduje alespoň jeden unikátní index nad materializovaným pohledem

postgres=# CREATE MATERIALIZED VIEW xxx AS SELECT current_timestamp AS refreshed FROM pg_sleep(20) x;
SELECT 1
postgres=# CREATE UNIQUE INDEX ON xxx(refreshed);
CREATE INDEX
postgres=# REFRESH MATERIALIZED VIEW xxx; -- blokuje čtení
REFRESH MATERIALIZED VIEW
postgres=# REFRESH MATERIALIZED VIEW  CONCURRENTLY xxx ; -- neblokuje čtení
REFRESH MATERIALIZED VIEW

Další změny se týkají aktualizovatelných pohledů (updateable views). V 9.4 byla dokončena jejich implementace - implementací klauzule CHECK OPTION. Skrze tuto klauzuli si vynucujeme test viditelnosti záznamu po provedení DML příkazu (INSERT, UPDATE). Pokud pohled má klauzuli CHECK OPTION, tak každý přidaný/upravený záznam musí být vždy viděn v pohledu (pohled se nesmí chovat jako černá díra). Predikáty, které se použijí v pohledu, který má klauzuli CHECK OPTION se kontrolují vždy a jsou analogií tabulkových omezení (table constraints).

postgres=# CREATE TABLE data(a int);
CREATE TABLE
postgres=# CREATE VIEW less20 AS SELECT * FROM data WHERE a < 20;
CREATE VIEW
postgres=# CREATE VIEW great0_less20 AS SELECT * FROM less20 WHERE a > 0 WITH LOCAL CHECK OPTION;
CREATE VIEW
postgres=# CREATE VIEW _great0_less20 AS SELECT * FROM less20 WHERE a > 0 WITH CASCADED CHECK OPTION;
CREATE VIEW
postgres=# INSERT INTO great0_less20 VALUES(10);
INSERT 0 1
postgres=# INSERT INTO great0_less20 VALUES(20); -- efekt LOCAL CHECK OPTION
INSERT 0 1
postgres=# INSERT INTO _great0_less20 VALUES(20);
ERROR:  new row violates WITH CHECK OPTION for view "less20"
DETAIL:  Failing row contains (20).

CASCADED CHECK OPTION zajistí, že se kontrolují i "zděděné" predikáty z všech pohledů (i těch, které nemají klauzuli CHECK OPTION). Toto chování je implicitní pro klauzuli CHECK OPTION (tj pokud neuvedete klíčové slovo CASCADED). LOCAL CHECK OPTION vynutí kontrolu predikátů pouze u pohledů, které mají klauzuli CHECK OPTION. LOCAL CHECK OPTION nepřepíše zděděné CASCADED CHECK OPTION:

postgres=# CREATE VIEW yyy AS SELECT * FROM _great0_less20 WITH LOCAL CHECK OPTION;
CREATE VIEW
postgres=# INSERT INTO yyy VALUES(20);
ERROR:  new row violates WITH CHECK OPTION for view "less20"
DETAIL:  Failing row contains (20).

a z definice CASCADED CHECK OPTION přepíše LOCAL CHECK OPTION:

postgres=# CREATE VIEW zzz AS SELECT * FROM great0_less20 WITH CASCADED CHECK OPTION;
CREATE VIEW
postgres=# INSERT INTO zzz VALUES(20);
ERROR:  new row violates WITH CHECK OPTION for view "less20"
DETAIL:  Failing row contains (20).

Více lidí si při testování této funkce stěžovalo na neintuitivnost návrhu této funkce - je to ale naprogramované, tak jak vyžaduje standard a ve shodě s implementací v Oracle a DB2.

Aktualizovatelné pohledy nyní mohou obsahovat výrazy.

postgres=# CREATE VIEW test1 AS SELECT a, sin(a) FROM data;
CREATE VIEW
postgres=# INSERT INTO test1 VALUES(10,0); -- neprojde (výraz nelze přepsat)
ERROR:  cannot insert into column "sin" of view "test1"
DETAIL:  View columns that are not columns of their base relation are not updatable.
postgres=# INSERT INTO test1(a) VALUES(10) RETURNING *;
+----+-------------------+
| a  |        sin        |
+----+-------------------+
| 10 | -0.54402111088937 |
+----+-------------------+
(1 row)

INSERT 0 1

V předešlých verzích pohledy, které měli flag security barrier (sloužící k zajištění bezpečného omezení přístupu k datům) nebyly aktualizovatelné. Opět počínaje 9.4 se toto omezení ruší.

Nové funkce a datové typy

Datové typy pro uložení síťových adres inet a cidr mají nyní vestavěnou podpporu GiST indexu:

CREATE INDEX inet_idx2 ON inet_tbl using gist (i inet_ops);
SET enable_seqscan TO off;

SELECT * FROM inet_tbl WHERE i >>= '192.168.1.0/24'::cidr ORDER BY i;
       c        |        i        
----------------+------------------
 192.168.1.0/24 | 192.168.1.0/24
 192.168.1.0/24 | 192.168.1.226/24
 192.168.1.0/24 | 192.168.1.255/24
(3 rows)

Nerelační datový typ jsonb

Bezpochyby největším marketingovým trhákem 9.4 bude vestavěný datový typ jsonb. V Postgresu navazuje na typ HStore s několika rozdíly. Je integrován do jádra, podporuje rekurzi, umožňuje indexaci a vstup/výstup je ve formátu JSON (technologicky je blíže typu HStore než typu JSON z 9.3). Vyzkoušel jsem si načíst, fragmentovat a oindexovat jeden větší JSON:

[pavel@localhost ~]$ ls -lh citylots.json 
-rw-rw-r--. 1 pavel pavel 181M May 20 19:35 citylots.json

[pavel@localhost ~]$ head citylots.json 
{
"type": "FeatureCollection",
"features": [
{ "type": "Feature", "properties": { "MAPBLKLOT": "0001001", "BLKLOT": "0001001", "BLOCK_NUM": "0001", "LOT_NUM": "001", "FROM_ST": "0", "TO_ST": "0", "STREET": "UNKNOWN", "ST_TYPE": null, "ODD_EVEN": "E" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.422003528252475, 37.808480096967251, 0.0 ], [ -122.422076013325281, 37.808835019815085, 0.0 ], [ -122.421102174348633, 37.808803534992904, 0.0 ], [ -122.421062569067274, 37.808601056818148, 0.0 ], [ -122.422003528252475, 37.808480096967251, 0.0 ] ] ] } }
,
{ "type": "Feature", "properties": { "MAPBLKLOT": "0002001", "BLKLOT": "0002001", "BLOCK_NUM": "0002", "LOT_NUM": "001", "FROM_ST": "0", "TO_ST": "0", "STREET": "UNKNOWN", "ST_TYPE": null, "ODD_EVEN": "E" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.42082593937107, 37.808631474146033, 0.0 ], [ -122.420858049679694, 37.808795641369592, 0.0 ], [ -122.419811958704301, 37.808761809714007, 0.0 ], [ -122.42082593937107, 37.808631474146033, 0.0 ] ] ] } }
,
{ "type": "Feature", "properties": { "MAPBLKLOT": "0004002", "BLKLOT": "0004002", "BLOCK_NUM": "0004", "LOT_NUM": "002", "FROM_ST": "0", "TO_ST": "0", "STREET": "UNKNOWN", "ST_TYPE": null, "ODD_EVEN": "E" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.415701204606876, 37.808327252671461, 0.0 ], [ -122.415760743593196, 37.808630700240904, 0.0 ], [ -122.413787891332404, 37.808566801319841, 0.0 ], [ -122.415701204606876, 37.808327252671461, 0.0 ] ] ] } }
,
{ "type": "Feature", "properties": { "MAPBLKLOT": "0005001", "BLKLOT": "0005001", "BLOCK_NUM": "0005", "LOT_NUM": "001", "FROM_ST": "206", "TO_ST": "286", "STREET": "JEFFERSON", "ST_TYPE": "ST", "ODD_EVEN": "E" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.417346670944355, 37.808121127609709, 0.0 ], [ -122.417457443198046, 37.808685627252729, 0.0 ], [ -122.416003128921787, 37.808638547938997, 0.0 ], [ -122.41593547450509, 37.808293744156337, 0.0 ], [ -122.417346670944355, 37.808121127609709, 0.0 ] ] ] } }
[pavel@localhost ~]$ 

Pro import do Postgresu použiji LO API:

postgres=# \lo_import ~/citylots.json 
lo_import 16510

A dále jej převedu do jednořádkových tabulek

CREATE OR REPLACE FUNCTION public.bytea_to_text(bytea)
 RETURNS text
 LANGUAGE sql
AS $function$
SELECT convert_from($1, current_setting('server_encoding'))
$function$

CREATE TABLE city_text(data text);
INSERT INTO city_text SELECT bytea_to_text(lo_get(16510));
CREATE TABLE city_json(data json);
CREATE TABLE city_jsonb(data jsonb);
INSERT INTO city_json SELECT data::json FROM city_text;
INSERT INTO city_jsonb SELECT data::jsonb FROM city_text;

Můžete si všimnout, že velikost tabulek je cca 1/4 vstupního souboru (díky transparentní komprimaci dat v Postgresu) a že jsonb má větší režii (ale nikterak velkou) než typ JSON a typ text (ve skutečnosti je JSON ukládán úplně stejně jako text).

postgres=# \dt+ city*
                     List of relations
 Schema |    Name    | Type  | Owner | Size  | Description 
--------+------------+-------+-------+-------+-------------
 public | city_json  | table | pavel | 48 MB | 
 public | city_jsonb | table | pavel | 51 MB | 
 public | city_text  | table | pavel | 48 MB | 

-- Aktualizace pro finální verzi Postgresu (po redesignu formátu jsonb)
                     List of relations
 Schema |    Name    | Type  | Owner | Size  | Description 
--------+------------+-------+-------+-------+-------------
 public | city_json  | table | pavel | 48 MB | 
 public | city_jsonb | table | pavel | 42 MB | 
 public | city_text  | table | pavel | 48 MB | 

V dalším kroku se pokusím rozložit hlavní pole v JSONu na jednotlivé prvky a uložit je jako samostatné řádky:

postgres=# INSERT INTO city_json_lines SELECT json_array_elements(json_object_field(data, 'features')) FROM city_json;
INSERT 0 206560
Time: 14778.181 ms
postgres=# INSERT INTO city_jsonb_lines SELECT jsonb_array_elements(jsonb_object_field(data, 'features')) FROM city_jsonb;
INSERT 0 206560
Time: 13085.024 ms

Zde je rychlost operací nad typy JSON a jsonb zhruba stejná a opět je vidět mírně větší režie (objemově) typu jsonb:

postgres=# \dt+ *_lines
                        List of relations
 Schema |       Name       | Type  | Owner |  Size  | Description 
--------+------------------+-------+-------+--------+-------------
 public | city_json_lines  | table | pavel | 155 MB | 
 public | city_jsonb_lines | table | pavel | 175 MB | 

-- Aktualizace pro finální verzi
                        List of relations
 Schema |       Name       | Type  | Owner |  Size  | Description 
--------+------------------+-------+-------+--------+-------------
 public | city_json_lines  | table | pavel | 160 MB | 
 public | city_jsonb_lines | table | pavel | 170 MB | 

Z dat ve formátu JSON, jsonb mohu extrahovat data:

postgres=# SELECT DISTINCT json_extract_path_text(data, 'properties', 'STREET') FROM city_json_lines LIMIT 5;
 json_extract_path_text 
------------------------
 
 HUBBELL
 FLOOD
 BADGER
 BALDWIN
(5 rows)

Při vyhledávání v neoindexovavých datech se už pak ukazují rozdíly mezi těmito typy (jsonb je cca 2-5x rychlejší):

postgres=# SELECT count(DISTINCT json_extract_path_text(data, 'properties', 'STREET')) FROM city_json_lines;
 count 
-------
  1716
(1 row)

Time: 3016.364 ms
postgres=# SELECT count(DISTINCT jsonb_extract_path_text(data, 'properties', 'STREET')) FROM city_jsonb_lines;
 count 
-------
  1716
(1 row)

Time: 1685.399 ms

postgres=# SELECT count(*) FROM city_json_lines WHERE json_extract_path_text(data, 'properties', 'STREET') = 'FRANCE';
 count 
-------
    37
(1 row)

Time: 1597.302 ms

postgres=# SELECT count(*) FROM city_jsonb_lines WHERE jsonb_extract_path_text(data, 'properties', 'STREET') = 'FRANCE';
 count 
-------
    37
(1 row)

Time: 322.104 ms

-- další možný zápis
postgres=# SELECT count(*) FROM city_jsonb_lines WHERE data->'properties'->>'STREET' = 'FRANCE';
 count 
-------
    37
(1 row)

Time: 357.258 ms

Typ jsonb umožňuje vytvořit univerzální (nemusím dopředu vyjmenovat klíče) GIN index:

CREATE INDEX ON city_jsonb_lines USING gin (data);
CREATE INDEX
Time: 44938.092 ms
postgres=# CREATE INDEX ON city_jsonb_lines ((data->'properties'->>'STREET'));
CREATE INDEX
Time: 2217.319 ms

postgres=# \di+ city_jsonb_*
                                       List of relations
 Schema |           Name            | Type  | Owner |      Table       |  Size   | Description 
--------+---------------------------+-------+-------+------------------+---------+-------------
 public | city_jsonb_lines_data_idx | index | pavel | city_jsonb_lines | 137 MB  | 
 public | city_jsonb_lines_expr_idx | index | pavel | city_jsonb_lines | 5008 kB | 
(2 rows)

Vytvořený index je docela velký a tak pokud nepotřebujeme univerzální řešení, může být praktičtější používat funkcionální index:

-- použití univerzálního indexu
postgres=# EXPLAIN ANALYZE SELECT count(*) FROM city_jsonb_lines WHERE data @> '{"properties":{"STREET":"FRANCE"}}';
                                                                 QUERY PLAN                                                                 
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=815.29..815.30 rows=1 width=0) (actual time=0.819..0.820 rows=1 loops=1)
   ->  Bitmap Heap Scan on city_jsonb_lines  (cost=53.60..814.78 rows=207 width=0) (actual time=0.693..0.800 rows=37 loops=1)
         Recheck Cond: (data @> '{"properties": {"STREET": "FRANCE"}}'::jsonb)
         Heap Blocks: exact=14
         ->  Bitmap Index Scan on city_jsonb_lines_data_idx  (cost=0.00..53.55 rows=207 width=0) (actual time=0.659..0.659 rows=37 loops=1)
               Index Cond: (data @> '{"properties": {"STREET": "FRANCE"}}'::jsonb)
 Planning time: 0.209 ms
 Execution time: 0.941 ms
(8 rows)

--použití funkcionálního indexu
postgres=# EXPLAIN ANALYZE SELECT count(*) FROM city_jsonb_lines WHERE data->'properties'->>'STREET' = 'FRANCE';
                                                                 QUERY PLAN                                             
---------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=3360.23..3360.24 rows=1 width=0) (actual time=0.203..0.203 rows=1 loops=1)
   ->  Bitmap Heap Scan on city_jsonb_lines  (cost=24.43..3357.65 rows=1033 width=0) (actual time=0.130..0.187 rows=37 loops=1)
         Recheck Cond: (((data -> 'properties'::text) ->> 'STREET'::text) = 'FRANCE'::text)
         Heap Blocks: exact=14
         ->  Bitmap Index Scan on city_jsonb_lines_expr_idx  (cost=0.00..24.17 rows=1033 width=0) (actual time=0.085..0.085 rows=37 l oops=1)
               Index Cond: (((data -> 'properties'::text) ->> 'STREET'::text) = 'FRANCE'::text)
 Planning time: 0.185 ms
 Execution time: 0.298 ms
(8 rows)

Podpora JSONu a jsonb je v začátcích a rozhodně je to jeden ze směrů, který by měl být v dalších verzích rozvíjen a podporován.

Nové funkce pro typ JSON

V 9.4 se rozšiřuje podpora typu JSON o několik funkcí. Funkce json_typeof(json) zobrazí typ honoty typu JSON. Funkce json_build_array vytvoří pole ve formátu JSON. json_build_object vytvoří objekt ve formátu JSON.

postgres=# SELECT json_typeof(json '123'), json_typeof(json '"ahoj"'), json_typeof(json 'true');
 json_typeof | json_typeof | json_typeof
-------------+-------------+-------------
 number      | string      | boolean
(1 row)

postgres=# SELECT json_build_array(1,2,3);
 json_build_array
------------------
 [1, 2, 3]
(1 row)

Time: 0.594 ms
postgres=# SELECT json_build_array('Hi','Hello');
 json_build_array
------------------
 ["Hi", "Hello"]
(1 row)

Time: 0.871 ms
postgres=# SELECT json_build_object('name','Tom','surname', 'Lane');
          json_build_object          
--------------------------------------
 {"name" : "Tom", "surname" : "Lane"}
(1 row)

Time: 1.091 ms
postgres=# SELECT json_build_object('name','Tom','surname', 'Lane', 'Age',48);
                json_build_object                
--------------------------------------------------
 {"name" : "Tom", "surname" : "Lane", "Age" : 48}
(1 row)

Nově máme k dispozici funkce sloužící k parsování JSONu - json_to_record a json_to_recordset:

postgres=# SELECT * FROM json_to_record('{"name" : "t15", "type" : "GE1043"}', true) AS x(name text, type text);
 name |  type 
------+--------
 t15  | GE1043
(1 row)

postgres=# SELECT * FROM json_to_recordset('[{"a":10, "b":10},{"a":30}]', true) AS x(a int, b int);
 a  |   b   
----+--------
 10 |     10
 30 | [null]
(2 rows)

Funkce json_array_elements a json_array_elements_text rozvine JSON pole na tabulku v Postgresu:

postgres=# SELECT * FROM json_array_elements_text('[{"a":10, "b":10},{"a":30}, "hi"]');
      value      
------------------
 {"a":10, "b":10}
 {"a":30}
 hi
(3 rows)

Time: 1.041 ms
postgres=# SELECT * FROM json_array_elements('[{"a":10, "b":10},{"a":30}, "hi"]');
      value      
------------------
 {"a":10, "b":10}
 {"a":30}
 "hi"
(3 rows)

Číselné konstruktory pro typ date, timestamp, interval

Implementací těchto funkcí jsem si splnil jeden svůj starý sen. Hrozně mi vadilo, že k vytvoření datumu z číselných poležek jsem musel použít formátované řetězce (a poměrně složité operace s řetězci), přičemž ale ty potřebné funkce v kódu Postgresu jsou, nejsou ovšem zpropagované do SQL.

-- nesprávně - náchylné být nefunkční při změně konfigurace Postgresu
CREATE OR REPLACE FUNCTION create_date1(y int, m int, d int)
RETURNS date LANGUAGE sql AS
$$ SELECT format('%s-%s-%s', $1, $2, $3)::date $$;

-- správně, ale vyžaduje manipulaci s řetězci a parsování řetězce
CREATE OR REPLACE FUNCTION create_date2(y int, m int, d int)
RETURNS date LANGUAGE sql AS
$$ SELECT to_date(format('%s-%s-%s', $1, $2, $3), 'YYYY-MM-DD') $$;
SELECT create_date2(2014,5,8);

-- řešení v 9.4, bez jakékoliv manipulace s řetězci
postgres=# SELECT make_date(2014,5,8);
 make_date
────────────
 2014-05-08
(1 row)

Ne, že by na soudobých CPU představovala manipulace s řetězci v těchto funkcích významnější úzké hrdlo (při běžném použití databáze), ale jistým malým způsobem mi to vždy trhalo žíly. V každém případě nová sada funkcí je efektivně implementovaná a navíc přináší určitý komfort:

postgres=# SELECT make_timestamp(2014,5,8,9,37,0);
   make_timestamp
─────────────────────
 2014-05-08 09:37:00
(1 row)

postgres=# SELECT make_interval(0,0,2,1);
 make_interval
───────────────
 15 days
(1 row)

postgres=# SELECT make_interval(weeks := 2, days := 1);
 make_interval
───────────────
 15 days
(1 row)

Time: 0.820 ms
postgres=# SELECT make_interval(months := 1);
 make_interval
───────────────
 1 mon
(1 row)

Funkce pro konverzi mezi LO a Bytea

I když si nemyslím, že BLOBy (Large Objects) v databázi mají smysl, někdy se můžou hodit a ušetřit dost práce. V Postgresu existuje docela šikovné API, které umožní jednoduše přesouvat obsah LO mezi klientem a serverem. I starší verze umožňovaly další manipulaci s LO (na straně serveru), např. konverzi do typu Bytea - a následnou konverzi do řetězce v kódování Base64. Muselo se ale trochu hackovat. Tento nedostatek je v 9.4 odstraněn díky funkcím lo_create, lo_get a lo_put:

postgres=# \lo_import ~/avatar.png
lo_import 16395
postgres=# SELECT encode(lo_get(16395), 'base64');
                                    encode                                   
──────────────────────────────────────────────────────────────────────────────
 iVBORw0KGgoAAAANSUhEUgAAACsAAAAyCAIAAABK5rXEAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA↵
 B3RJTUUH1woNFgE2tT89UQAAEu9JREFUWMNVeFmQnNd13jnn3n/rdbqnZ8FsxDILVoEkSJiSKIqk↵
 bFEORcf0VnaYKJW4Uik/ZHlybD/k3alKKlV5SrkqlUSplEzLkexyKZIIElwAEiIJEAABYsBZMIPZ↵
 p5fp/d/uPScPPYDi+9D/7eque879v7N838Hn/vRzRCQieLyQmEVBDGAMKBHtoVYmVJKA0kZAlGJE↵
 ...

Ostatní nové funkce

Funkce pg_sleep dostala dva sourozence - pg_sleep_for (s parametrem interval) a pg_sleep_until. Jsou to v podstatě jen SQL makra, jejichž smyslem je zjednodušit vývojářům život. S těmito funkcemi ale opatrně! Pokud by se použily uvnitř déle trvající transakce, kde se navíc přistupuje k tabulkám, tak se efektivně vyblokuje VACUUM (jinak, volání funkce sleep uvnitř transakce většinou není dobrý nápad). A když už jsem nakousl toto téma. Hlídejte si delší otevřené transakce - v pohledu pg_stat_activity vidíte záznam ve stavu "idle in transaction". Jedná se o aplikační chybu, která vede ke zpomalení postgresu (díky vyblokovanému VACUUMu), druhak k vyčerpání spojení do databáze (bo otevřené transakce blokují znovupoužití spojení).

SELECT pg_sleep(1.5);
SELECT pg_sleep_for('5 minutes');
SELECT pg_sleep_until('tomorrow 03:00');

V předchozích verzích nebylo úplně triviální otestovat, zda je nějaké pole prázdné (funkce array_upper, array_lower, array_dims vrací NULL) . S funkcí cardinality(pole) to už triviální je. Tato funkce vrací počet prvků v poli, a pro prázdné pole vrací nulu.

postgres=# SELECT array_dims('{}'::int[]);
 array_dims
────────────
 [null]
(1 row)

postgres=# SELECT cardinality('{}'::int[]);
 cardinality
─────────────
           0
(1 row)

Prostředí uložených procedur

Nově lze event triggery (DDL) psát i v Perlu:

CREATE OR REPLACE FUNCTION perlsnitch() RETURNS event_trigger AS $$
  elog(NOTICE, "perlsnitch: " . $_TD->{event} . " " . $_TD->{tag} . " ");
$$ LANGUAGE plperl;

CREATE EVENT TRIGGER perl_a_snitch
    ON ddl_command_start
    EXECUTE PROCEDURE perlsnitch();

Pro PL/pgSQL je tu nově možnost aktivovat extra kontroly kódu, přičemž si lze vybrat, jestli tyto kontroly mají vyhazovat varování nebo chyby. Zatím se jedná spíše o koncept - k dispozici je pouze jedna kontrola na zastíněné proměnné.

SET plpgsql.extra_warnings TO 'shadowed_variables';

CREATE FUNCTION foo(f1 int) RETURNS int AS $$
DECLARE
f1 int;
BEGIN
RETURN f1;
END
$$ LANGUAGE plpgsql;
WARNING:  variable "f1" shadows a previously defined variable
LINE 3: f1 int;

SET plpgsql.extra_errors to 'shadowed_variables';
create or replace function shadowtest(f1 int)
   returns boolean as $$
declare f1 int; begin return 1; end $$ language plpgsql;
ERROR:  variable "f1" shadows a previously defined variable
LINE 3: declare f1 int; begin return 1; end $$ language plpgsql;

Krůčkem vpřed je možnost získat obsah zásobníku volání (call stack) příkazem GET DIAGNOSTICS PG_CONTEXT:

CREATE OR REPLACE FUNCTION public.outer_func() RETURNS integer AS $$
BEGIN
  RETURN inner_func();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.inner_func() RETURNS integer AS  $$
DECLARE
  stack text;
BEGIN
  GET DIAGNOSTICS stack = PG_CONTEXT;
  RAISE NOTICE e'--- Call Stack ---\n%', stack;
  RETURN 1;
END;
$$ LANGUAGE plpgsql;

SELECT outer_func();

NOTICE:  --- Call Stack ---
PL/pgSQL function inner_func() line 4 at GET DIAGNOSTICS
PL/pgSQL function outer_func() line 3 at RETURN
 outer_func
------------
          1
(1 row)

S debugováním plpgsql kódu by mohl pomoci přepínač #print_strict_params, který zajistí zobrazení parametrů dotazů označených jako STRICT (chceme aby výsledek byl přesně jeden řádek), pokud vrátí počet řádků různý od jedné. Mimo kód si lze vynutit toto chování nastavením plpgsql.print_strict_params na true v konfigu Postgreu (výchozí hodnota je false):

-- test printing parameters after failure due to STRICT
CREATE TABLE foo AS SELECT 2 f1, 'foo'::text f2;

SET plpgsql.print_strict_params TO true;

CREATE OR REPLACE FUNCTION footest() RETURNS void AS $$
DECLARE
  x record;
  p1 int := 2;
  p3 text := 'foo1';
BEGIN
  -- no rows
  SELECT * FROM foo WHERE f1 = p1 AND f1::text = p3 INTO STRICT x;
END$$ LANGUAGE plpgsql;

postgres=# SELECT footest();
ERROR:  query returned no rows
DETAIL:  parameters: p1 = '2', p3 = 'foo1'
CONTEXT:  PL/pgSQL function footest() line 8 at SQL statement
Time: 43.162 ms

Pokud nemáte kód, který je založen na vyhazování a obsluhování velkého počtu vyjímek, tak tato funkce bude mít zanedbatelnou režii a není důvod, proč ji nezapnout (pokud používáte plpgsql a klauzuli STRICT).

Pro zobrazení delších textů se může hodit možnost zalamování textů v extended režimu (upozornění: tato funkce bude dostupná až v následující verzi)

extended mode 9.1, extended wrapped mode 9.4

Správa a monitoring databáze

Malou, ale zásadní změnou je možnost konfigurace serveru čistě pomocí SQL resp. příkazu ALTER SYSTEM SET. Reload konfigurace se vynutí voláním funkce pg_reload_conf().

S instalací PostgreSQL získáte desítky extenzí - některé z nich je nutné explicitně aktivovat příkazem LOAD. Pro opakované použití můžeme extenzi načíst při startu Postgresu - v konfiguraci nastavíme seznam shared_preload_libraries nebo (nově) session_preload_libraries. Změna první hodnoty vyžaduje restart Postgresu. session_preload_libraries restart nevyžaduje (vztahuje se na nově vytvořené spojení) a může se specificky upravit pro uživatele, pro databázi. Extenze, které pracují se sdílenou pamětí, zatím vyžadují aktivaci pomocí shared_preload_libraries. V budoucnu by ale tento požadavek mohl zmizet.

V 9.4 můžeme omezit rychlost klonování serveru (throttling) nastavením --max-rate (v kB za sec) příkazu pg_basebackup. Nově také můžeme v pg_basebackup realokovat tablespaces (volba -T olddir=newdir).

Ve specifických případech se může hodit volba --analyze-in-stages příkazu vacuumdb. Ta způsobí opakované volání přikazu ANALYZE s parametry statistic target: 1, 10, 100. Při první iteraci můžeme mít k dispozici hrubé statistiky velice rychle, každá následující iterace statistiky zpřesňuje. Při binárním upgrade nedochází k přenosu statistik a možnost rychle získat alespoň hrubé statistiky snižuje dobu nezbytného výpadku.

/usr/local/pgsql/bin/vacuumdb -v  --analyze-in-stages postgres &> ~/log

INFO:  analyzing "public.pgbench_accounts"
INFO:  "pgbench_accounts": scanned 300 of 634921 pages, containing 18900 live rows and 0 dead rows; 300 rows in sample, 40000024 estimated total rows
...
INFO:  analyzing "public.pgbench_accounts"
INFO:  "pgbench_accounts": scanned 3000 of 634921 pages, containing 189000 live rows and 0 dead rows; 3000 rows in sample, 40000024 estimated total rows
...
INFO:  analyzing "public.pgbench_accounts"
INFO:  "pgbench_accounts": scanned 30000 of 634921 pages, containing 1890000 live rows and 0 dead rows; 30000 rows in sample, 40000024 estimated total rows

Zásadní úpravy se dočkala extenze pg_stat_statements. Tato extenze zajišťuje statisku použití SQL příkazů včetně redukce konstant (nahrazení konstant parametry, tj SELECT * FROM t1 WHERE c1='Pavel' a SELECT * FROM t1 WHERE c1='Petr' se po normalizaci stane jedním SQL příkazem SELECT * FROM t1 WHERE c1 = ?). Ke každému SQL příkazu se eviduje počet exekucí, celková doba provádění, přístupy do cache, použití IO a použití dočasných souborů. Dříve tato data byla kompletně uložena v sdílené paměti. V 9.4 je text SQL příkazu uložen v souboru. Tím se výrazně redukuje alokace paměti a díky tomu se výchozí limit maximálního počtu příkazů mohl změnit z 1000 na 5000.

V 9.4 se také zaloguje držitel zámku (číslo procesu) v záznamu logu o čekání na zámek delším než 1sec (ve výchozím nastavení). Později můžeme zkusit dohledat tento proces v zalogovaných pomalých dotazech a potvrdit si tak (nebo vyvrátit) hypotézu, že zámky jsou způsobeny pomalými dotazy.

-- 9.3
LOG:  process 26258 still waiting for ShareLock on transaction 3143 after 1000.180 ms
STATEMENT:  update t set a = 59;
LOG:  process 26258 acquired ShareLock on transaction 3143 after 13065.146 ms
STATEMENT:  update t set a = 59;

-- 9.4
LOG:  process 20863 still waiting for ShareLock on transaction 2029 after 1000.179 ms
DETAIL:  Process holding the lock: 20861. Wait queue: 20863.
CONTEXT:  while updating tuple (0,2) in relation "t"
STATEMENT:  update t set a = 59;
LOG:  process 20863 acquired ShareLock on transaction 2029 after 14124.213 ms
CONTEXT:  while updating tuple (0,2) in relation "t"
STATEMENT:  update t set a = 59;

Replikace

Déle než rok se pracuje na implementaci vestavěné logické replikace. Možná i více než polovina patchů je již integrovaná do jádra. Nicméně vyjma několika experimentálních extenzí (jedna z extenzi implementuje auditing), není tento nový kód zpřístupněn uživatelům. Na produkčně použitelné extenze si budeme muset ještě jeden rok počkat (věřím, že v 9.5 logická replikace bude).

Pracovalo se i na vestavěné binární replikaci - nastavením recovery_min_apply_delay (např na hodnotu 5min) lze pozdržet aplikaci segmentů transakčních logů a vytvořit tak repliku, která je vůči masteru explicitně posunutá v čase (delayed replication).

Snížení úrovně zámků pro vybrané příkazy ALTER TABLE

Ve starších verzích všechny příkazy ALTER TABLE vynucovaly exkluzivní přístup (zámek) k tabulce. Což samozřejmě způsobovalo problémy u větších tabulek a pomalejších operací. Počínaje 9.4 si varianty příkazu ALTER: VALIDATE CONSTRAINT, CLUSTER ON, SET WITHOUT CLUSTER, ALTER COLUMN SET STATISTICS, ALTER COLUMN SET (), ALTER COLUMN RESET () vystačí s méně agresivním zámkem (VALIDATE CONSTRAINT vyžaduje ShareUpdateExclusiveLock - blokuje pouze další ALTER nebo VACUUM).

-- vyžaduje exkluzivní zámek, ale stávající data nekontroluje
postgres=# ALTER TABLE omega ADD CONSTRAINT slow CHECK(slow_constraint(a)) NOT VALID;
ALTER TABLE
-- kontroluje stávající data, ale neblokuje DML a SELECT
postgres=# ALTER TABLE omega VALIDATE CONSTRAINT slow;
ALTER TABLE

Na toto chování se čekalo dlouho - teprve nyní má ALTER TABLE ADD CONSTRAINT NOT VALID smysl.

Příkaz EXPLAIN

U komplikovanějších dotazů s větším počtem JOINů (větší > 16) už i plánování dotazu může mít nezanedbatelnou režii (vteřiny, případně desítky vteřin). Této režie si nemusí být uživatel vědom, a při optimalizaci dotazů se může vydat po falešné stopě. Aby se tak nestalo, tak nyní EXPLAIN zobrazuje čas nezbytny pro naplánování dotazu:

                                                                  QUERY PLAN                                                                 
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Sort  (cost=20.74..20.77 rows=10 width=133) (actual rows=1 loops=1)
   Sort Key: n.nspname, c.relname
   Sort Method: quicksort  Memory: 17kB
   ->  Hash Join  (cost=1.14..20.58 rows=10 width=133) (actual rows=1 loops=1)
         Hash Cond: (c.relnamespace = n.oid)
         ->  Seq Scan on pg_class c  (cost=0.00..19.06 rows=19 width=73) (actual rows=52 loops=1)
               Filter: ((relkind = ANY ('{r,""}'::"char"[])) AND pg_table_is_visible(oid))
               Rows Removed by Filter: 246
         ->  Hash  (cost=1.10..1.10 rows=3 width=68) (actual rows=2 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 1kB
               ->  Seq Scan on pg_namespace n  (cost=0.00..1.10 rows=3 width=68) (actual rows=2 loops=1)
                     Filter: ((nspname <> 'pg_catalog'::name) AND (nspname <> 'information_schema'::name) AND (nspname !~ '^pg_toast'::text))
                     Rows Removed by Filter: 4
 Planning time: 0.741 ms
 Execution time: 0.668 ms
(15 rows)

Time: 2.977 ms

Data mám v cache (a jsou malá), takže u tohoto dotazu plánování zabere o něco více než samotná exekuce dotazu. Dalším vylepšením příkazu EXPLAIN je zobrazení klíčů pro agregaci (což může pomoci se orientovat v komplexnějším plánu):

postgres=# EXPLAIN select count(*), relnamespace from pg_class group by relnamespace;
                           QUERY PLAN                           
─────────────────────────────────────────────────────────────────
 HashAggregate  (cost=12.42..12.46 rows=3 width=4)
   Group Key: relnamespace
   ->  Seq Scan on pg_class  (cost=0.00..10.95 rows=295 width=4)
 Planning time: 0.182 ms
(4 rows)

Posledním vylepšením příkazu EXPLAIN je zobrazení počtu ztrátových nebo úplných stránek k bitmap heap scanu. Bitmap heap scan je řízen bitmapou o velikosti, která by měla být menší než je work_mem. Při dostatku paměti tato bitmapa obsahuje id řádků, při nedostatku id datových stránek - případně část jsou id řádků, část id stránek. Jelikož se pro id řádků nemusí provádět recheck, tak jsou operace s řádkovými id rychlejší (tuple id) za cenu větší spotřeby paměti:

postgres=# CREATE TABLE aa AS SELECT * FROM generate_series(1, 1000000) AS a ORDER BY random();
SELECT 1000000
postgres=# CREATE INDEX aai ON aa(a);
CREATE INDEX
postgres=# SET enable_indexscan=false;
SET
postgres=# SET enable_seqscan=false;
SET
postgres=# SET work_mem = '64kB';
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM aa WHERE a BETWEEN 100000 AND 200000;
                                                        QUERY PLAN                                                       
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Bitmap Heap Scan on aa  (cost=1853.03..7233.48 rows=97230 width=4) (actual time=21.461..345.237 rows=100001 loops=1)
   Recheck Cond: ((a >= 100000) AND (a <= 200000))
   Rows Removed by Index Recheck: 712693
   Heap Blocks: exact=814 lossy=3108
   ->  Bitmap Index Scan on aai  (cost=0.00..1828.72 rows=97230 width=0) (actual time=21.288..21.288 rows=100001 loops=1)
         Index Cond: ((a >= 100000) AND (a <= 200000))
 Planning time: 0.291 ms
 Execution time: 474.558 ms
(8 rows)

postgres=# SET work_mem = '10MB';
SET
postgres=# EXPLAIN ANALYZE SELECT * FROM aa WHERE a BETWEEN 100000 AND 200000;
                                                        QUERY PLAN                                                       
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Bitmap Heap Scan on aa  (cost=1853.03..7233.48 rows=97230 width=4) (actual time=20.858..173.533 rows=100001 loops=1)
   Recheck Cond: ((a >= 100000) AND (a <= 200000))
   Heap Blocks: exact=3922
   ->  Bitmap Index Scan on aai  (cost=0.00..1828.72 rows=97230 width=0) (actual time=20.073..20.073 rows=100001 loops=1)
         Index Cond: ((a >= 100000) AND (a <= 200000))
 Planning time: 0.183 ms
 Execution time: 300.147 ms
(7 rows)

ALTER TABLESPACE MOVE

Příkaz ALTER TABLESPACE MOVE nám umožňuje přesunout určitý typ objektů (vše, tabulky, indexy, materializované pohledy) do jiného tablespace. Objekty ještě můžeme filtrovat podle vlastníka:

ALTER TABLESPACE testspace MOVE ALL TO pg_default;
ALTER TABLESPACE testspace2 MOVE TABLES OWNED BY test_owner TO pg_default;

Ostatní vylepšení a změny

Background worker procesy (zákaznické procesy mající přistup k sdílené paměti) lze dynamicky startovat a zastavovat a uvnitř těchto procesů lze dynamicky alokovat a dealokovat sdílenou paměť (dříve bylo nutné sdílenou paměť alokovat při startu a nebylo možné ji uvolnit). Je hotové API pro zajištění komunikace mezi background worker procesem a procesem obsluhujícím přihlášeného uživatele (shared memory message queue). Polymorfní agregační funkce mohou mít nepolymorfní stavovou proměnnou. Zavedením tzv inverzní přechodové funkce (inversion transition function) se zásadně zrychluje výpočet klouzavých analytických agregačních funkcí. Další optimalizací se dosáhlo cca 20% zrychlení výpočtu agregačních funkcí sum a avg pro typ numeric. Optimalizovala se manipulace s obsahem transakčního logu včetně redukce objemu zapsaných dat při update řádku (pokud data zůstávají na téže stránce, tak se do logu zapisují změněné hodnoty (dříve celé řádky)). work_mem a maintenance_work_mem lze bezpečně nastavit na terabajty (za předpokladu, že máte k dispozici dostatek RAM). Výchozí hodnota pro work_mem je nově 4MB a pro maintenance_work_mem je 64MB. Výchozí hodnota effective_cache_size 4GB. Revitalizoval se datový typ line. Nově i agregační funkce mohou být variadické (s variabilním počtem parametrů) - např. variadický sum přes více sloupců. Defaultní parametry a pojmenované parametry lze použít i v případě analytických window funkcí. Tam, kde to je možné a povolené (a zatím pouze na Linuxu) se pro sdílenou paměť použijí velké datové stránky (nastavení huge_tlb_pages v postgresql.conf) - pozor na THP defrag (doporučuje se vypnout). Došlo k vylepšení komprimace v implementaci GIN indexů - ty by měly být menší a tedy budou vyžadovat méně RAM, také se budou rychlejší načítat z disku. Také by se měly zrychlit dotazy vůči GIN indexu s podmínkou typu "sporadický & častý". Nově lze vytvářet triggery i pro fdw (cizí/externí) tabulky. Extenze autoexplain (s nastavením analyze) zaloguje i režii triggerů. Seznam contrib modulů rozšířil modul pg_prewarm, který umožňuje přednačtení zadané tabulky do bufferů Postgresu. Volba FORCE NULL příkazu COPY vynutí test na NULL i pro řetězce zadené v uvozovkách.

Drobného vylepšení se dočkala i funkce PITR (Point-In-Time Recovery). Při obnově databáze a přehrávání transakčních logů PostgreSQL přehrává všechny dostupné segmenty transakčního logu. Přehrávání bylo možné pozastavit (případně ukončit) v zadaném čase nebo zadané transakci. Nově je možné pozastavit přehrávání při dosažení prvního konzistentního stavu databáze (recovery_target=immediate). Monitoring exportu segmentů transakčního logu umožňuje pohled pg_stat_archiver.

postgres=# select * from pg_stat_archiver ;
-[ RECORD 1 ]------+------------------------------
archived_count     | 0                            
last_archived_wal  |                              
last_archived_time |                              
failed_count       | 0                            
last_failed_wal    |                              
last_failed_time   |                              
stats_reset        | 2014-05-20 22:12:13.888164+02

V Makefile je podpora pro překlad a instalaci klientských aplikací a nezbytných knihoven - což by mělo zjednodušit vytváření instalačních balíčků obsahujcích pouze klienta (pokud vím, tak takový instalační balíček pro MS Windows chybí). Pro některé aplikace dodávané s Postgresem (createuser, dropuser, pg_isready, ..) jsou nově k dispozici TAP testy. Konečně lze jednoduše samostatně startovat vybrané regresní testy (test make check-tests TESTS="json jsonb"). V pgBenchi lze omezit počet transakcí za sekundu (a lze tak sledovat o něco reálnější vliv PostgreSQL na CPU, IO, ..).

RPM balíčky pro Fedoru s 9.2kou jsou v repozitáři http://yum.postgresql.org/repopackages.php#pg94. Samotná instalace je triviální: yum install postgresql94-server postgresql94-contrib.

Ve své únorové přednášce na P2D2 nastínil Simon Rigs svou vizi implementace podpory OLAPu v Postgresu v následujících letech. Simon je hodně velký vizionář a byznysman, takže jeho vize nemusí vypadat úplně realisticky (ačkoliv mnoho z toho, co v roce 2008 sliboval ohledně vestavěné replikace se splnilo). V každém případě lze čekat, že jeho tým bude generovat patche pro zrychlení dotazů typických pro OLAP. Myslím si, že je realistické čekat první produkční extenze postavené nad background worker API (scheduler, killer nečinných spojení, multi CPU sort ..) a samozřejmě základní dopracování logické replikace a extenze spojené s touto funkcionalitou (audit). Existují prototypy implementace autonomních transakcí, UPSERT (INSERT or UPDATE), SKIP LOCKED DATA, implementace jsquery, import FDW schématu. Co z toho bude hotové a co budou moci využívat uživatelé, uvidíme ale až za rok.