PostgreSQL v roce 2009 (PostgreSQL 8.4)

Pavel Stěhule 12. 3. 2009

Každoročně přinášíme článek věnovaný novinkám v PostgreSQL a ani letošní rok nebude výjimkou. V březnu finišují práce na dokončení verze 8.4, která by měla být k dispozici začátkem léta. Na nové funkce se uživatelé dlouho těšili - podpora rekurzivních a analytických dotazů. Co nás přesně čeká?

Obsah

PostgreSQL v roce 2009

Po dokončení verze 8.3 jsem byl přesvědčen, že 8.3 je verze, která se hned tak nepřekoná. Mýlil jsem se. 8.3 prošlápla cestu ještě lepší, funkcemi nabité, verzi 8.4. 8.3 se dokončovala poměrně bolestivě. V této verzi došlo k přepracování docela zásadních funkcí jádra databáze. To mělo celou řadu dopadů. Jednak se vytvořil prostor pro nové funkce (např. refaktoringem planneru), jednak se jasně ukázalo, že stávající vývojový proces (resp. jakési zázemí) přestává být funkční, a že musí přijít změna. Změna přišla a to v podobě tzv. commitfestů. Roční cyklus je rozdělen do čtyř tříměsíčních etap. A každá etapa se skládá z jednoho měsíce určeného k opravám chyb, vlastnímu vývoji a dvouměsíčnímu commitfestu, během kterého se mají vývojáři rozhodnout zda a které přihlášené patche integrovat do PostgreSQL. Evidence patchů je, možná trochu překvapivě, ve wiki (např. http://wiki.pos­tgresql.org/wi­ki/CommitFest_2008–11). Projekt zatím zůstává u CVS, částečně, pro složitější patche, se používá Git. Docela to funguje. Opravdu se toho stihlo hodně, a prakticky ustaly diskuze o změně vývoje. To může signalizovat, že jdeme správnou cestou.

Většina té nejhorší a nejnevděčnější práce se odbyla v 8.3 (většiny změn (kromě vyššího výkonu) si uživatel nevšimne). V 8.4 je to přesně naopak. Řada funkcí byla dotažena, další, po kterých uživatelé volali, se dostaly do jádra.

Datové typy

Nově můžeme používat typ citex – case insensitive text. Lepším řešením by byla podpora COLLATIONS. Když nic jiného, tak alespoň toto:

postgres=# \i /usr/local/pgsql/share/contrib/citext.sql
postgres=# create table testci(email citext);
CREATE TABLE
Time: 9,377 ms
postgres=# insert into testci
  values('Pavel.Stehule@Gmail.CZ');
INSERT 0 1
postgres=# select * from testci where email like '%gmail.cz';
         email
------------------------
 Pavel.Stehule@Gmail.CZ
(1 row)
postgres=# select * from testci
              where email = 'pavel.stehule@gmail.cz';
         email
------------------------
 Pavel.Stehule@Gmail.CZ
(1 row)

Pole

Nové funkce

V 8.4 se objeví několik praktických funkcí pro pole. Předně je to agregační funkce array_agg. Ve starších verzích jsme měli k dispozici funkci array_accum, které sice nebyla integrována, nicméně způsob její registrace bylo možné najít v dokumentaci. Tato funkce je ovšem dost pomalá pro větší pole (nad 1000 prvků). Tento nedostatek funkce array_agg řeší.

Další funkcí je funkce unnest. Ta převede jednorozměrné pole na tabulku:

postgres=# select * from unnest(array[1,2,3]);
 unnest
--------
      1
      2
      3

Pro více rozměrná pole podobná funkce neexistuje. Nicméně k dispozici je funkce generate_subscrip­ts, která zjednoduší iteraci nad n-rozměrným polem. Pole se hodí pro řadu úloh – např pro výběr mediánu. Tradiční SQL nijak zvlášť nepomůže. Pomocí vestavěné podpory polí a SQL funkcí můžeme tuto a podobné úlohy vyřešit efektivně a rychle:

postgres=# create function sort(anyarray) returns anyarray as $$
              select array(select * from unnest($1) order by 1)
           $$ language sql immutable strict;
CREATE FUNCTION

postgres=# create or replace function median_sorted(anyarray) returns float as $$
             select ($1[round(array_upper($1,1)/2.0)] + $1[array_upper($1,1)
                    - round(array_upper($1,1)/2.0) + 1]) / 2.0::float;
           $$ language sql immutable strict;
CREATE FUNCTION

postgres=# create or replace function median(anyarray) returns float as $$
              select (a[round(array_upper(a,1)/2.0)] + a[array_upper(a,1)
                     - round(array_upper(a,1)/2.0) + 1]) / 2.0::float
                 from sort($1) a;
           $$ language sql immutable strict;
CREATE FUNCTION

postgres=# create table test_m(a integer);
CREATE TABLE
postgres=# insert into test_m select (random()*1000)::int from generate_series(1,10000);
INSERT 0 10000
postgres=# \timing
Timing is on.
postgres=# select median_sorted(array(select a from test_m order by 1));
 median_sorted
---------------
           502
(1 row)

Time: 26,003 ms
postgres=# select median(array(select a from test_m));
 median
--------
    502
(1 row)

Time: 28,809 ms

Funkce generate_subscripts generuje posloupnost indexů zadaného pole v zadané dimenzi. Definice funkce unnest se pak zjednoduší na následující kód:

create function unnest(anyarray) returns setof anyelement as $$
  select $1[i] from generate_subscripts($1,1) g(i);
$$ language sql immutable strict;

Funkce unnest

K čemu se může hodit funkce unnest? Např. k inverzi agregovaných hodnot (tam, kde to je možné). Představte si, že máte tabulku obsahující vždy seznam čárkou oddělených hodnot a klíč (občas se s podobným formátem setkáte při importu):

postgres=# select * from aggregates ;
     v     | k
-----------+---
 a,b,c,d,e | 1
 f,g,h     | 2
(2 rows)

Seznam hodnot bych chtěl převést opět na řádky. Buďto si můžu napsat uloženou proceduru, nebo využiji jednu specifickou vlastnost PostgreSQL, a to možnost umístit SRF funkci do seznamu zobrazených sloupců – na této pozici může být parametrem SRF funkce proměnná (naopak ve FROM parametry SRF funkce musí být konstanty).

postgres=# select unnest(string_to_array(v,',')), k from aggregates ;
 unnest | k
--------+---
 a      | 1
 b      | 1
 c      | 1
 d      | 1
 e      | 1
 f      | 2
 g      | 2
 h      | 2
(8 rows)

Tuto tabulku můžeme pak klasicky zpracovávat – např. určit rozsah pro jednotlivé klíče:

postgres=# select min(v) || '..' || max(v) as rozsah, k
              from (select unnest(string_to_array(v,',')) v, k
                       from aggregates
                   ) s
             group by k
             order by k;
 rozsah | k
--------+---
 a..e   | 1
 f..h   | 2
(2 rows)

případně vygenerovat zpět původní tabulku:

postgres=# select array_to_string(array_agg(v),','), k
              from (select unnest(string_to_array(v,',')) v, k
                       from aggregates
                   ) s
             group by k
             order by k;
 array_to_string | k
-----------------+---
 a,b,c,d,e       | 1
 f,g,h           | 2
(2 rows)

Perfektně se funkce unnest doplňuje s variadickými funkcemi (viz níže), kde jsou parametry funkci předány jako pole hodnot:

postgres=# CREATE FUNCTION avgc(VARIADIC double precision[])
           RETURNS double precision AS $$
             SELECT avg(v) FROM unnest($1) v
           $$ LANGUAGE sql;
CREATE FUNCTION

postgres=# select avgc(1,2,3,null);
 avgc
------
    2
(1 row)
postgres=# select avgc(1,2,3,4);
 avgc
------
  2.5
(1 row)

Funkce array_fill

Pole v PL/pgSQL musí být před prvním použitím inicializována, což vzhledem k určitým specifikům PL/pgSQL je docela pomalá operace (při větších polích). Nyní mohou programátoři použít funkci array_fill, která vrací pole specifikované inicializační hodnotou a polem dimenzí:

postgres=# select array_fill(0,array[2,2,2]);
          array_fill
-------------------------------
 {{{0,0},{0,0}},{{0,0},{0,0}}}
(1 row)
postgres=# select array_fill(''::varchar,array[2,2]);
    array_fill
-------------------
 {{"",""},{"",""}}
(1 row)

Fulltext

Podpora statistik

V minulé verzi došlo k integraci fulltextu do jádra systému. Integrace ovšem nebyla úplná – chyběla podpora statistik. Bez statistik optimalizátor střílí naslepo, a mohlo se stát (a také se stávalo), že pro složitější dotazy obsahující fulltextové vyhledávání nebyl nalezen optimální prováděcí plán. V 8.4 je toto riziko odstraněno – statistky se generují i pro fulltext:

postgres=# explain analyze
              select * from film where fulltext @@ to_tsquery('dog');
                                QUERY PLAN
---------------------------------------------------------------------------
 Bitmap Heap Scan on film (cost=5.02..62.42 rows=99 width=390)
 (actual time=0.509..1.854 rows=99 loops=1)
   Recheck Cond: (fulltext @@ to_tsquery('dog'::text))
   -> Bitmap Index Scan on film_fulltext_idx
       (cost=0.00..5.00 rows=99 width=0)
       (actual time=0.456..0.456 rows=99 loops=1)
         Index Cond: (fulltext @@ to_tsquery('dog'::text))
 Total runtime: 2.230 ms
(5 rows)

Podpora vyhledání prefixu

Při implementaci nejrůznějších našeptávačů jistě přijde vhod fulltextové vyhledání prefixu:

-- vyžaduje GIN index
postgres=# select *
              from codebooks.psc_obce
             where to_tsvector('simple', cast_obce) @@ to_tsquery('simple', 'Bene:*');
         obec          |         cast_obce         | psc   |      nazev_posty      | lau1
-----------------------+---------------------------+-------+-----------------------+-------
 Benecko               | Benecko (část)            | 51237 | Benecko               | CZ0514
 Benecko               | Benecko (část)            | 51401 | Jilemnice             | CZ0514
 Bušanovice            | Beneda                    | 38422 | Vlachovo Březí        | CZ0315
 Broumov               | Benešov                   | 55001 | Broumov 1             | CZ0523
 Benešov               | Benešov                   | 25601 | Benešov u Prahy       | CZ0201
 Benešov               | Benešov                   | 67953 | Benešov u Boskovic    | CZ0641

Uložené procedury

EXECUTE USING

Doposud bylo používání dynamického SQL limitováno možným rizikem SQL injektáže. Kromě toho, také, docela nečitelným výsledným kódem. Tyto problémy z části odstraňuje klauzule USING v příkazu PL/pgSQL EXECUTE. Díky ní totiž můžeme parametrizovat SQL příkaz, podobně jako v PREPARED STATEMENTS.

nezabezpečené dynamické SQL:

EXECUTE 'SELECT * FROM ' || tabname || ' WHERE value = \'' || value || '\'';

zabezpečené dynamické SQL:

EXECUTE 'SELECT * FROM ' || tabname::regclass || ' WHERE value = $1' USING value;

Díky klauzuli USING bude použití dynamického SQL o dost bezpečnější a bude s ním méně práce. I tak ale platí, že DSQL by se mělo používat, pokud možno, co nejméně a v opravdu nutných případech (např. z důvodu nutnosti generování prováděcího plánu se znalostí parametrů).

Příkaz CASE

Z prostředí SQL/PSM (standardizovaný jazyk pro uložené procedury dle ANSI – setkat se s ním můžete v DB2 nebo MySQL) byla převzata syntaxe příkazu CASE.

/* simple case */
CASE x
    WHEN 1, 2 THEN
         msg := 'one or two';
    ELSE
         msg := 'other value than one or two';
END CASE;

/* search case */
CASE
    WHEN x BETWEEN 0 AND 10 THEN
         msg := 'value is between zero and ten';
    WHEN x BETWEEN 11 AND 20 THEN
         msg := 'value is between eleven and twenty';
END CASE;

Vlastní výjimky

PostgreSQL umožňuje předat ve výjimce relativně dost informací. PL/pgSQL příkaz RAISE byl příliš jednoduchý. Výsledkem výjimky v PL/pgSQL byl pouze jeden řetězec (nebylo možné zadat kód, hint, detail). Toto omezení je minulostí. Syntaxe příkazu RAISE byla rozšířena tak, aby umožnila specifikovat všechny dostupné parametry výjimky:

RAISE division_by_zero;
RAISE SQLSTATE '22012';
RAISE EXCEPTION 'Nonexistent ID --> %', user_id
   USING HINT = 'Please check your user id';
RAISE 'Duplicate user ID: %', user_id
   USING ERRCODE = 'unique_violation';
RAISE 'Duplicate user ID: %', user_id
   USING ERRCODE = '23505';

Vlastní vyjímky lze samozřejmě zachytit – v minulých verzích bylo možné zachytit pouze systémové výjimky:

BEGIN
   ...
EXCEPTION
  WHEN SQLSTATE '22012' THEN ...

Variadické funkce

Funkce, které pracují s variabilním počtem parametrů označujeme jako variadické. Mezi variadické funkce např. patří funkce least, greatest, coalesce. Implementace těchto funkcí není nijak zvlášť komplikovaná, nicméně není triviální a trochu vypadává z kontextu C funkcí (vynucuje si úpravy parseru). Na uživatelské úrovni nezbývalo než přetěžovat funkce, což vedlo k zbytečnému duplikování kódu. Řešením je zavedení tzv. variadických funkcí, resp. modifikátoru VARIADIC v seznamu parametrů. Ten pozměňuje chování parseru. Všechny reálné parametry, které jsou pozičně na pozici a za pozicí variadického parametru, jsou sloučeny do pole, které je pak předáno funkci.

CREATE FUNCTION myleast(VARIADIC a numeric[])
 RETURNS NUMERIC AS $$
   SELECT min($1[i])
      FROM generate_subscripts($1,1) g(i)
 $$ LANGUAGE SQL IMMUTABLE;
CREATE FUNCTION
Time: 5,274 ms
postgres=# SELECT myleast(1,2,3,4);
 myleast
---------
        1
(1 row)

Variadické funkci lze předhodit i pole (tj. obejít parser) pomocí modifikátoru hodnoty VARIADIC:

postgres=# SELECT myleast(VARIADIC ARRAY[1,3,4,-5,6,8]);
 myleast
---------
      -5

Tabulkové funkce

Funkce, které vrací tabulku, jsou podle ANSI SQL tzv. tabulkové funkce. Totéž (s mírně odlišnou syntaxí) se v PostgreSQL označuje jako SRF funkce (Set Returned Functions). Do 8.4 byla zahrnuta i podpora ANSI SQL syntaxe, která je podle mého názoru přehlednější a názornější:

--ANSI SQL
create or replace function tab(i int)
returns table(a int, b int) as $$
  select i, i+1
     from generate_series(1, $1) g(i);
$$ language sql immutable strict;

postgres=# select * from tab(2);
 a | b
---+---
 1 | 2
 2 | 3
(2 rows)

-- PostgreSQL syntaxe
create or replace function tab(in int, out a int, out b int)
returns setof record as $$
  select i, i+1
     from generate_series(1, $1) g(i);
$$ language sql immutable strict;

Defaultní hodnoty parametrů funkce

Možnost specifikovat defaultní hodnoty parametrů funkce patří zvyšuje programátorský komfort a snižuje duplicitu kódu. Implementace zatím odpovídá zhruba implementaci ve Firebirdu, tj. chybějící parametry zprava doplňují defaultní hodnoty.

postgres=# create or replace function instr(varchar, varchar, int = 1)
           returns int as $$
              select position($2 in substring($1 from $3));
           $$ language sql immutable strict;
CREATE FUNCTION
Time: 2,738 ms
postgres=# select instr('abcabc','a'); instr
-------
     1
(1 row)

Time: 1,907 ms
postgres=# select instr('abcabc','a',2);
 instr
-------
     3
(1 row)

Time: 1,381 ms

RETURN QUERY EXECUTE

Že se RETURN QUERY docela ujalo, mne příjemně překvapilo. Jednak jsem vůbec nečekal, že by podobné rozšíření mohlo projít do core, jednak vlastní implementace byla docela jednoduchá – v podstatě pár nových řádek v parseru, jinak se použil stávající kód. To je to, co mne na hackingu nejvíc baví. Přidá se pár řádků, pár řádků kódu se upraví a nová funkce je na světě.

V 8.4 RETURN QUERY podporuje také dynamické SQL – tj. zčásti odpadá vzor:

FOR r IN EXECUTE '....' LOOP
  RETURN NEXT r;
END LOOP

který lze nahradit rychlejším a čitelnějším kódem:

RETURN QUERY EXECUTE '...';

Uživatelský interface

Výpis velikostí v seznamu db. objektů

Zjistit velikost tabulky nebo databáze již nebude problém.

postgres=# \dt+ codebooks.*
                        List of relations
  Schema   |   Name   | Type  | Owner |     Size   | Description
-----------+----------+-------+-------+------------+-------------
 codebooks | lau1     | table | pavel | 8192 bytes |
 codebooks | lau2     | table | pavel | 448 kB     |
 codebooks | nuts0    | table | pavel | 8192 bytes |
 codebooks | nuts1    | table | pavel | 8192 bytes |
 codebooks | nuts2    | table | pavel | 8192 bytes |
 codebooks | nuts3    | table | pavel | 8192 bytes |
 codebooks | psc_obce | table | pavel | 1352 kB    |
(7 rows)

Lámání dlouhých textů ve výpisu

postgres=# \pset format wrapped
Output format is wrapped.
postgres=# select 'Příliš žlutý kůň se napil žluté vody',
                  'Příliš žlutý kůň se napil žluté vody';
        ?column?         |        ?column?
-------------------------+-------------------------
 Příliš žlutý kůň se nap | Příliš žlutý kůň se nap
 il žluté vody           ; il žluté vody
(1 row)

Ještě by to chtělo zalamovat po celých slovech – snad příště.

Spuštění externího editoru

Jistě se mnou souhlasíte, že konzole psql se nehodí pro editaci funkcí. Pokud máte nastavenou sys. proměnnou EDITOR můžete příkazem \ef názevfce editovat kód funkce v externím editoru.

[pavel@localhost ~]$ export EDITOR=nano
[pavel@localhost ~]$ psql postgres
psql (8.4devel)
Type "help" for help.
postgres=# \ef foo
postgres-# ;
CREATE FUNCTION
Time: 3,605 ms
Posgres

Národní prostředí

Lokalizované výstupy funkce to_char

Funkci to_char lze nyní přinutit k generování skutečně lokalizovaných datumů (tj. názvů dnů a měsíců):

postgres=# select to_char(current_date,
                          'tmDay, DD. tmMonth');
      to_char
-------------------
 Středa, 11. Červen
(1 row)

Separátní locales pro každou databázi

Chybějící podpora COLLATIONS značně diskvalifikuje použítí PostgreSQL jako db pro vícejazyčné aplikace. Prvním krokem v implementaci COLLATIONS je možnost nastavit locales pro každou databázi zvlášť. Drobnou nevýhodou je, že při vytváření databáze s jiným nastavením COLLATIONS je však potřeba použít za vzor databázi template0. Pokud použijete jinou šablonu, je třeba provést reindexaci nové databáze.

[pavel@localhost pgsql]$ /usr/local/pgsql/bin/psql -l
                                             List of databases
    Name   | Owner    | Encoding |    Collation    |      Ctype      |          Access Privileges
-----------+----------+----------+-----------------+-----------------+-------------------------------------
 postgres  | postgres | UTF8     | cs_CZ.UTF-8     | cs_CZ.UTF-8     |
 template0 | postgres | UTF8     | cs_CZ.UTF-8     | cs_CZ.UTF-8     | {=c/postgres,postgres=CTc/postgres}
 template1 | postgres | UTF8     | cs_CZ.UTF-8     | cs_CZ.UTF-8     | {=c/postgres,postgres=CTc/postgres}
 testdb    | pavel    | LATIN2   | cs_CZ.iso8859-2 | cs_CZ.iso8859-2 |
(4 rows)

testdb=# set client_encoding to utf8;
SET
Time: 1,133 ms
testdb=# select upper('žššžšž');
 upper
--------
 ŽŠŠŽŠŽ
(1 row)
testdb=# select octet_length('žýř');
 octet_length
--------------
            3
(1 row)

pavel@localhost pgsql]$ psql postgres
psql (8.4devel)
Type "help" for help.
postgres=# select octet_length('žýř');
 octet_length
--------------
            6
(1 row)

Podpora COLLATIONS vzniká u nás v ČR – díky práci Radka Strnada a do core se dostala také díky nezlomnému úsilí Zdeňka Kotaly.

SQL

Analytické funkce

Během vánočních svátků, vlastně na Silvestra, byl commitnut patch Hitoshi Harady obsahující podporu analytických (window nebo OLAP) funkcí. Commit do jádra je takový vrchol snah každého vývojáře. Je to výsledek docela poctivého několika měsíčního kódování a pak také obvykle několika měsíčního lobingu, diskuzí nad kódem, desítek nejrůznějších úprav – prostě hromada práce. Na jaře byl Hitochi mírně rozčarován, že se jeho patch do jádra nedostal – bylo na něm ještě hodně práce, o čem se osobně přesvědčil – ale dotáhl to až do konce, budiž mu věčná sláva :). Když už do toho jednou člověk spadne, tak kódování je docela návyková záležitost. Už kolik let si říkám, že tohle byl můj poslední patch.

Analytické funkce nejsou novinkou – prvně se objevily již v extenzi ANSI SQL 99 (část OLAP). Na normě spolupracoval Oracle s IBM, takže byly velice brzo implementovány, a to v Oracle 8i (2001) a v DB2 7. V ostatních RDBMS se objevily o dost později (např. MS SQL server 2008), a letos je PostgreSQL první vlašťovkou v O.S. světě. Z pohledu rigidních relačních teoretiků jsou analytické funkce dost kontroverzní záležitostí – jsou mimo rámec relační algebry a také překračují prvotní architekturu, kdy se SQL server neměl starat o zobrazení dat. Návrh ještě pamatuje časy, kdy nejapnosti typu ORM byly fantazií pouze několika málo utopistů (kdy se ještě nedalo plýtvat výkonem). Na druhou stranu, díky analytickým funkcím můžeme zapomenout self join alchymii a ušetříme si pěknou řádku kódu. Takže analytické funkce byly vzaty na milost (ještě před rokem se Tom Lane vyjádřil zhruba v tom smyslu, že větší pitomost než SQL/OLAP neviděl (a v něčem měl možná pravdu) a že nečeká, že by se někdy tyto funkce do PostgreSQL dostaly. Člověk míní, a … – dost velký podíl na tomto commitu má právě Tom Lane (ostatně jako na většině kódu v PostreSQL). V 8.4 ještě není podpora analytických funkcí kompletní, nicméně to zásadní tam je:

postgres=# SELECT * FROM foo;
 a  | b
----+---
  1 | 1
  1 | 1
  2 | 1
  4 | 1
  2 | 1
  4 | 1
  5 | 1
 11 | 3
 12 | 3
 22 | 3
 16 | 3
 16 | 3
 16 | 3
(13 rows)

postgres=# SELECT a, b, sum(a) OVER wo,
postgres-#        rank() OVER wo, dense_rank() OVER wo,
postgres-#        (percent_rank() OVER wo)::numeric(3,2), (cume_dist() OVER wo)::numeric(3,2),
postgres-#        count(*) OVER wo, sum(a) OVER (PARTITION BY b ORDER BY a ROWS UNBOUNDED PRECEDING) AS cum_sum,
postgres-#        lag(a,-1) OVER wo, lag(a,1) OVER wo,
postgres-#        avgc(lag(a,-1) OVER wo, a, lag(a,1) OVER wo)::numeric(4,2) AS run_avg,
postgres-#        first_value(a) OVER wo, last_value(a) OVER wo
postgres-#    FROM foo
postgres-#   WINDOW wo AS (PARTITION BY b
postgres(#                 ORDER BY a
postgres(#                 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
 a  | b | sum | rank | dense_rank | percent_rank | cume_dist | count | cum_sum | lag | lag | run_avg | first_value | last_value
----+---+-----+------+------------+--------------+-----------+-------+---------+-----+-----+---------+-------------+------------
  1 | 1 |  19 |    1 |          1 |         0.00 |      0.29 |     7 |       1 |   1 |     |    1.00 |           1 |          5
  1 | 1 |  19 |    1 |          1 |         0.00 |      0.29 |     7 |       2 |   2 |   1 |    1.33 |           1 |          5
  2 | 1 |  19 |    3 |          2 |         0.33 |      0.57 |     7 |       4 |   2 |   1 |    1.67 |           1 |          5
  2 | 1 |  19 |    3 |          2 |         0.33 |      0.57 |     7 |       6 |   4 |   2 |    2.67 |           1 |          5
  4 | 1 |  19 |    5 |          3 |         0.67 |      0.86 |     7 |      10 |   4 |   2 |    3.33 |           1 |          5
  4 | 1 |  19 |    5 |          3 |         0.67 |      0.86 |     7 |      14 |   5 |   4 |    4.33 |           1 |          5
  5 | 1 |  19 |    7 |          4 |         1.00 |      1.00 |     7 |      19 |     |   4 |    4.50 |           1 |          5
 11 | 3 |  93 |    1 |          1 |         0.00 |      0.17 |     6 |      11 |  12 |     |   11.50 |          11 |         22
 12 | 3 |  93 |    2 |          2 |         0.20 |      0.33 |     6 |      23 |  16 |  11 |   13.00 |          11 |         22
 16 | 3 |  93 |    3 |          3 |         0.40 |      0.83 |     6 |      39 |  16 |  12 |   14.67 |          11 |         22
 16 | 3 |  93 |    3 |          3 |         0.40 |      0.83 |     6 |      55 |  16 |  16 |   16.00 |          11 |         22
 16 | 3 |  93 |    3 |          3 |         0.40 |      0.83 |     6 |      71 |  22 |  16 |   18.00 |          11 |         22
 22 | 3 |  93 |    6 |          4 |         1.00 |      1.00 |     6 |      93 |     |  16 |   19.00 |          11 |         22
(13 rows)

Pozor – ORDER BY v analytických funkcích má trochu jiný význam než klauzule ORDER BY v příkazu SELECT.

Parametrické pohledy

Za tímto názvem se skrývají obyčejné SRF funkce, které byly i v předchozích verzích. V 8.4 se kód IMMUTABLE SQL funkce o jednom SQL příkazu zařadí přímo do SQL příkazu. Dochází k subtituci (podobně jako u skalárních SQL funkcí). Díky tomu planner vidí do funkce a dokáže predikovat výsledek funkce. Ve starších verzích byly SQL SRF funkce pro planner black box – a docházelo ke generování neoptimálních prováděcích plánů. Z toho důvodu se nedoporučovalo zapouzdřovat SELECTy do funkcí. Další výhodou může být vykonávání po řádcích (ty typické SRF funkce plní jakousi paměťovou tabulku a jako výsledek se předává odkaz na tuto tabulku – to je výhodné u menších výsledných množin a nevýhodné u těch velkých – kvůli nárokům na operační paměť):

postgres=# create function full_foo()
                   returns setof foo as $$
                  select * from foo
                $$ language sql immutable;
CREATE FUNCTION

postgres=# create function full_foo_pl()
           returns setof foo as $$
           begin
             return query select * from foo;
             return;
           end;
           $$ language plpgsql immutable;
CREATE FUNCTION

postgres=# explain select * from foo;
                    QUERY PLAN
---------------------------------------------------
 Seq Scan on foo  (cost=0.00..1.06 rows=6 width=4)
(1 row)

postgres=# explain select * from full_foo();
                    QUERY PLAN
---------------------------------------------------
 Seq Scan on foo  (cost=0.00..1.06 rows=6 width=4)
(1 row)

postgres=# explain select * from full_foo_pl();
                             QUERY PLAN
---------------------------------------------------------------------
 Function Scan on full_foo_pl  (cost=0.00..260.00 rows=1000 width=4)
(1 row)

Je evidentní, že při zapouzdření SELECTu PL/pgSQL funkcí došlo k rozhození planneru. Nyní mají SQL SRF funkce skutečně blíže pohledům než funkcím – nulová režie, nulové negativní ovlivnění planneru.

Rekurzivní dotazy (Common Table Expression – CTE)

Konečně tu jsou dlouho slibované rekurzivní dotazy. Implementace v PostgreSQL vychází z ANSI SQL (klauzule WITH). Kromě ANSI SQL syntaxe existuje ještě starší, jednoduší a omezenější zápis CONNECT BY (Oracle), jehož podpora by se řadě uživatelů určitě líbila. CONNECT BY se ovšem do core nedostal. Nesešlo se dostatek argumentů pro implementaci duplicitní funkce – budiž nám alespoň útěchou, že WITH je standardní, a dost mocnější syntaxe.

PostreSQL zatím nepodporuje tzv. GROUPING SETS (Prototyp už existuje, ale sešly se tři funkce, které zásadně modifikují executor (CTE, Analytic queries a GROUPING SETS) a na GS už nezbylo dostatečně velké časové okno pro implementaci. Bez GS lze jen poměrně neefektivně kombinovat výpis agregovaných hodnot s celkovým součtem – musíme použít UNION ALL, což znamená minimálně dvojí sekvenční čtení tabulky:

postgres=# select * from prodej;
      nazev      |    kategorie    | cena
-----------------+-----------------+-------
 jogurt          | mléčné produkty | 20.00
 chléb - šumava  | pečivo          | 10.00
 plnotučné mléko | mléčné produkty | 15.00
 rohlíky         | pečivo          | 12.00
(4 rows)

S nerekurzivním CTE můžeme jedno sekvenční čtení eliminovat:

postgres=# with prehled as
              (select kategorie, sum(cena) cena
                  from prodej
                 group by kategorie
              ) select *
                   from prehled
                union all
                select 'Celkem', sum(cena)
                   from prehled;
    kategorie    | cena
-----------------+-------
 pečivo          | 22.00
 mléčné produkty | 35.00
 Celkem          | 57.00
(3 rows)

Uvnitř klauzule WITH plním relaci „prehled“ (je to podobné použití dočasné tabulky – ovšem odpadá režie na aktualizaci hodnot v systémových tabulkách, která je spojená se vznikem a zánikem dočasné tabulky). Z relace prehled pak čtu přímo a prostřednictvím union k výsledku přidávám agregovanou hodnotou nad relací prehled.

Po CTE voláme hlavně kvůli podpoře rekurze. První ukázka obsahuje vizualizovaný výpis rekurzivní organizační struktury:

Vstupní data:

postgres=# select * from org_schema;
 jmeno  | nadrizeny
--------+-----------
 Libor  | NULL
 Vráťa  | Libor
 Petra  | Libor
 Martin | Vráťa
 Pavel  | Martin
 Marek  | Martin
 Robin  | Petra
(7 rows)

Výpis:

postgres=#
01           with recursive rq as
02              (select 0 as level, jmeno as path, *
03                  from org_schema where nadrizeny is null
04               union all
05               select level+1 as lev,
06                       path || e'\\' || o.jmeno as path, o.*
07                  from rq, org_schema o
08                 where rq.jmeno = o.nadrizeny
09              ) select *, repeat(' ', level) || jmeno
10                    from rq order by path;
 lev |           path            | jmeno  | nadrizeny | ?column?
-----+---------------------------+--------+-----------+-------------
   0 | Libor                     | Libor  | NULL      | Libor
   1 | Libor\Petra               | Petra  | Libor     |   Petra
   2 | Libor\Petra\Robin         | Robin  | Petra     |     Robin
   1 | Libor\Vráťa               | Vráťa  | Libor     |   Vráťa
   2 | Libor\Vráťa\Martin        | Martin | Vráťa     |     Martin
   3 | Libor\Vráťa\Martin\Marek  | Marek  | Martin    |       Marek
   3 | Libor\Vráťa\Martin\Pavel  | Pavel  | Martin    |       Pavel
(7 rows)

Vnitřní dotaz v CTE (řádky 2..8) obsahuje dva dotazy – oba plní relaci rq. Separátorem je klauzule union all. První dotaz (řádky 2,3) vyhledá kořen. Druhý dotaz (řádky 5..8) se opakuje dokud vrací alespoň jeden řádek. Výsledek tohoto dotazu se přidá na zásobník a použije se jako obsah relace rq pro další iteraci. Všimněte si, že dochází ke spojení relace rq a tabulky org_schema (řádky 7,8). Kromě hodnot z tabulky org_schema si vypočítávám hloubku rekurze (sl. level) – pro kořen nastavuji 0, pak vždy zvyšuji o jedničku pro všechny potomky (rq.level + 1).

Vnější dotaz CTE (řádky 9,10) transformuje obsah relace rq (která v ten okamžik obsahuje sjednocení obsahu rq všech iterací – resp. vše, co se naakumulovalo na zásobníku) na výsledný dotaz. Na řádku 9 se na základě hodnoty v sloupci level určí odsazení (tj. dochází k vizualizaci hiearchie). Jelikož se používá metoda přibližuje metodě prohledávání do šířky (nikoliv do hloubky) je nutné výstup seřadit podle sloupce path.

Při maximálním rozlišení a velikosti okna konzole spusťe následující příkaz:

WITH RECURSIVE Z(IX, IY, CX, CY, X, Y, I) AS (
                SELECT IX, IY, X::float, Y::float, X::float, Y::float, 0
                FROM (SELECT -2.2 + 0.031 * i, i FROM generate_series(0,101) AS i) AS xgen(x,ix),
                     (SELECT -1.5 + 0.031 * i, i FROM generate_series(0,101) AS i) AS ygen(y,iy)
                UNION ALL
                SELECT IX, IY, CX, CY, X * X - Y * Y + CX AS X, Y * X * 2 + CY, I + 1
                FROM Z
                WHERE X * X + Y * Y < 16::float
                AND I < 100
          )
    SELECT array_to_string(array_agg(SUBSTRING(' .,,,-----++++%%%%@@@@#### ', LEAST(GREATEST(I,1),27), 1)),'')
    FROM (
          SELECT IX, IY, MAX(I) AS I
          FROM Z
          GROUP BY IY, IX
          ORDER BY IY, IX
         ) AS ZT
    GROUP BY IY
    ORDER BY IY;

Schválně jestli poznáte výsledek?

ANSI SQL kompatibilní omezení výpisu

Dost dlouho standard nenabízel možnost, jak zobrazit pouze určitou podmnožinu zobrazení výsledku dotazu. Což vedlo dodavatele databází k vlastním (bohužel nekompatibilním) rozšířením. Takže u Oracle se používá ROWNUM, v MSSQL TOP, PostgreSQL a MySQL Limit (vzájemně nekompatibilně). Konečně si normovači všimly díry a zavedli do standardu limit a offset:

postgres=# select *
              from generate_series(1,100)
             offset 10 rows
             fetch next 5 rows only;
 generate_series
-----------------
              11
              12
              13
              14
              15

Tato syntaxe se dost blíží tomu, co bylo a je v PostgreSQL, ale jako na potvoru, pořadí klauzulí offset a limit je přesně naopak. Pozn. Další standardní způsob omezení výpisu je použít analytické funkce (např. row_number()).

Neviditelné funkce

Visibility maps

Tzv. líné VACUUM dostalo k dispozici informaci o tom, které datové stránky tabulky byly změněny. Tím se zrychlí VACUUM na velkých a málo měněných tabulkách – VACUUM je mnohem cílenější.

Free space maps

V případě vkládaní či aktualizaci záznamů v tabulce, je třeba dohledat volné místo, kam nový záznam vložit. Do verze 8.3 informace byly ukládány ve statickém poli. V případě velkých tabulek, pak nebylo možné držet informaci o volném místě pro celou tabulku, a zbytečně se pak tabulka nafukovala. Ve verzi 8.4 byla celá správa volného místa přepracována a je nyní mnohem efektivnější. Díky tomu zmizely i dvě konfigurační položky z postgresql.conf.

Podpora semijoinu a antijoinu pro EXISTS

Tak jak se postupně planner vylepšuje, mění se i doporučení, které konstrukce preferovat. V čase před osmičkovou řadou se nedoporučovaly poddotazy s IN, pak došlo k jejich zásadnímu zrychlení. Takovému, že se začalo nedoporučovat poddotazy s EXISTS. V 8.4 došlo i na EXISTS (naopak NOT EXISTS je dotaženější než NOT IN, tj. počínaje 8.4 si dávejte pozor na NOT IN):

postgres=# explain select * from film where film_id in (select film_id from
  film_actor );
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Hash Join (cost=117.26..195.78 rows=977 width=390)
   Hash Cond: (film.film_id = film_actor.film_id)
   -> Seq Scan on film (cost=0.00..65.00 rows=1000 width=390)
   -> Hash (cost=105.05..105.05 rows=977 width=2)
         -> HashAggregate (cost=95.28..105.05 rows=977 width=2)
               -> Seq Scan on film_actor (cost=0.00..81.62 rows=5462 width=2)
(6 rows)
postgres=# explain select * from film f where exists (select * from film_actor
  where film_id = f.film_id);
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Hash Join (cost=117.26..195.78 rows=977 width=390)
   Hash Cond: (f.film_id = film_actor.film_id)
   -> Seq Scan on film f (cost=0.00..65.00 rows=1000 width=390)
   -> Hash (cost=105.05..105.05 rows=977 width=2)
         -> HashAggregate (cost=95.28..105.05 rows=977 width=2)
               -> Seq Scan on film_actor (cost=0.00..81.62 rows=5462 width=2)
(6 rows)

postgres=# explain select * from film f where not exists (select * from
  film_actor where film_id = f.film_id);
                                QUERY PLAN
--------------------------------------------------------------------------
 Hash Anti Join (cost=149.90..245.12 rows=23 width=390)
   Hash Cond: (f.film_id = film_actor.film_id)
   -> Seq Scan on film f (cost=0.00..65.00 rows=1000 width=390)
   -> Hash (cost=81.62..81.62 rows=5462 width=2)
         -> Seq Scan on film_actor (cost=0.00..81.62 rows=5462 width=2)
(5 rows)

Zrychlené COPY

Došlo k optimalizaci rutin, které používá příkaz COPY. Na tabulce

create table data(a int, b float, c numeric(10,3));

s jedním miliónem řádků byl import cca o 26% rychlejší než v 8.3. Z měření by se dalo usuzovat, že rychlost importu je mnohem stabilnější – vůči 8.3 je mnohem menší rozptyl 8139 +/- 157ms proti 11093 +/- 1376 ms.

Zrychlený HASH index

Do verze 8.3 HASH index nebyl doporučovaný. Jednak nemá implementovaný WAL, takže při pádu se může poškodit, a jednak neexistoval případ, kdy by byl rychlejší než B-TREE. V rámci GSoC se implementace HASH indexu ve verzi 8.4 změnila a nyní se ukládá do indexu jen spočtená 32bitová hodnota hash funkce. Díky tomu dochází k rychlému vyhledání dlouhých VARCHAR položek. Pro hodnoty typu integer se rychlost příliš nezmění.

Paralení import dumpu

pg_restore (utilita pro import binárního dumpu) dokáže otevřít víc spojení do databáze a dump načítat, zpracovávat v souběžných procesech (příp. vláknech na MS Windows). Kupodivu to funguje. Na syntetickém příkladu importu dvou tabulek v relaci 1:N (obě tabulky byly stejně velké) došlo k dvojnásobnému zrychlení importu (při dvou procesech). Je otázkou, jak se ukáže tato funkce v praxi.

[pavel@localhost bin]$ ./dropdb test
[pavel@localhost bin]$ ./createdb test
[pavel@localhost bin]$ time ./pg_restore -d test  ~/data.dta

real    0m16.649s
user    0m1.171s
sys 0m0.027s
[pavel@localhost bin]$ ./dropdb test
[pavel@localhost bin]$ ./createdb test
[pavel@localhost bin]$ time ./pg_restore -d test -m2 ~/data.dta

real    0m8.782s
user    0m0.811s
sys 0m0.031s

Zabezpečení

Práva na sloupce

Počínaje verzí 8.4 můžeme nastavovat přístupová práva i na jednotlivé sloupce tabulky.

postgres=# create role p;
CREATE ROLE
postgres=# create table foob(a int, b int);
CREATE TABLE
postgres=# insert into foob values(1,2);
INSERT 0 1
postgres=# GRANT all ( b ) on table foob to public;
GRANT
postgres=# set role p;
SET
postgres=> select * from foob;
ERROR:  permission denied for relation foob
postgres=> select b from foob;
 b
---
 2
(1 row)

Dohled

Provozní statistiky – pohled pg_stat_user_fun­ctions

Na otázku jaké tabulky a indexy se používají a jak intenzivně dokážeme odpovědět díky dotazům do pohledů pg_stat_*_tables a pg_stat_*_indexes. Od 8.4 je možné podobně monitorovat i funkce. Fantazii se meze nekladou – zrovna teď by se mi tato funkce hodila – bohužel migrujeme na 8.3 a nikoliv na 8.4. Během migrace z 8.1 na 8.3 se snažíme trochu vyčistit kód a přišel jsem na to, že s vysokou pravděpodobností se nám v databázi nahromadily nepoužívané (mrtvé) funkce. V 8.3 si musím pomoci a skriptem do všech funkcí přidat kód, který zapíše volání funkce do připravené tabulky. V 8.4 je monitorování funkcí defaultně vypnuto.

postgres=# set track_functions to 'pl';
SET
postgres=# create or replace function test(i integer) returns int as $$
           begin perform pg_sleep(1);
             return i;
           end;$$ language plpgsql;
CREATE FUNCTION
postgres=# SELECT test(10);
 test
------
   10
(1 row)

postgres=# select * from pg_stat_user_functions;
 funcid | schemaname | funcname | calls | total_time | self_time
--------+------------+----------+-------+------------+-----------
  24608 | public     | test     |     1 |       1002 |      1002
(1 row)

Pozor – zdrojem dat pro tento pohled je executor. U skalarních SQL funkce (a non volatile SRF single SQL funkcí) se uplatňuje inlining. Z pohledu executoru neexistují a tudíž se ani nemohou objevit v tomto pohledu.

RPM repozitory

Na adrese http://yum.pgsqlrpms­.org/ si můžete vyzkoušet PostgreSQL repository. Vůči distribuci obsahuje navíc balíčky knihoven, které se do distribuce nedostaly (např. orafce, plpgsh, plpgpsm) a můžete si vybrat verzi PostgreSQL, kterou potřebujete, tj. 8.3 je připravena pro RHEL/CentOS 5, RHEL/CentOS 4, Fedora 7–10 a pod. A nechybí ano možnost nainstalovat si z rep. neprodukční vývojovou verzi. Nyní i v RPM je možnost mít nainstalováno víc verzí (starší balíčky se přepisovaly).

epsql

epsql (experimental psql) je čistě privátní záležitost. SQL konzole psql musí za všech okolností a všech prostředích pracovat bez chyb. Je to první aplikace, prostřednictvím ní uživatel získá první dojem o PostgreSQL. Tudíž jsou core vývojáři poměrně hodně nedůtkliví ohledně požadavků na přidávání nových funkcí. Je to hodně konzervativní aplikace – podobně jako Oraclovské sqlplus. Tudíž jsem udělal vlastní fork. Pár dní jsem si hrál a psql upravil tak, že používá UTF rámečky a ANSI escape kódy pro zvýraznění záhlaví tabulek (a kromě toho, to rozumně zalamuje (v mezerách)). epsql jsem je tzv postcardware, takže jestli se vám epsql líbí, pošlete pohlednici na adresu uvedenou v README.

Rozšířená podpora stylů

K dispozici jsou 4 styly rámečků a 7 stylů vykreslení. Je možné nastavit escape sekvenci (barvy a zvýraznění) pro řádek se záhlavím sloupců a pro jednotlivá záhlaví.

Posgres 2

Číslování řádků výpisu zdrojového kódu funkce

Funkce, která mi hodně chyběla je listing funkce s číslováním řádků. Proto je v epsql. Metapříkaz \lf má odlehčenou variantu – se symbolem mínus. V případě použití odlehčené varianty se nečíslují řádky výpisu.

postgres=# select test(10);
ERROR:  division by zero
CONTEXT:  PL/pgSQL function "test" line 3 at assignment

postgres=# \lf test
*** CREATE OR REPLACE FUNCTION public.test(a integer)
***  RETURNS integer
***  LANGUAGE plpgsql
*** AS $function$
  1 declare b int;
  2 begin
  3   b := a/0;
  4   return a;
  5 end;
*** $function$

Integrace kurzorů

V psql se docela nepohodlně přenášejí data z databáze k dalšímu zpracování. Co umí psql dobře, je zobrazení výsledku dotazu. Pro cokoliv ostatního je skoro jednodušší si napsat uloženou proceduru nebo skript v bashi. Pokusem o vyřešení omezenosti psql je integrace podpory kurzorů – metapříkazy \fetch a \fetchall. Metapříkaz fetch přečte záznam z kurzoru a jeho obsah uloží do psql proměnných. S obsahem těchto proměnných pak můžeme dále pracovat:

postgres=# BEGIN;
BEGIN
postgres=# DECLARE c CURSOR FOR SELECT * FROM pg_database;
DECLARE CURSOR
postgres=# \fetch c \echo :datname :datcollate
template1 cs_CZ.UTF-8
postgres=# \fetch c \echo :datname :datcollate
template0 cs_CZ.UTF-8
postgres=# \fetch c \echo :datname :datcollate
postgres cs_CZ.UTF-8
postgres=# \fetch c \echo :datname :datcollate
ERROR:  (not available)
postgres=# COMMIT;
COMMIT

Metapříkaz \fetchall provede totéž co příkaz fetch a navíc pro každý záznam všechny příkazy v psql bloku – tj. na jednom řádku:

postgres=# CREATE TABLE test1(a int); CREATE TABLE test2(a int);
CREATE TABLE
CREATE TABLE
postgres=# BEGIN;
BEGIN
postgres=# DECLARE c CURSOR FOR SELECT tablename FROM pg_tables WHERE tablename LIKE 'test%';
DECLARE CURSOR
postgres=# \fetchall c DROP TABLE :tablename;
DROP TABLE
DROP TABLE
postgres=# COMMIT;
COMMIT
postgres=#

Na co se letos nedostalo

Na podpoře GROUPING SETS mně čeká ještě dost práce. Snad do léta by to mohlo být v CVS HEADu. Naopak, podpora jmenného a kombinovaného předávání parametrů (named and mixed notation) je téměř hotová, bohužel s jinou syntaxí, než známe z ADY nebo z Oraclu. Uvažuje se o lepší podpoře C++ (pro UDF – User Defined Functions). Snad se začne pracovat na podpoře COLLATION dle ANSI SQL a koncepčněji se začalo řešit další směřování partitioningu. Z celoroční práce KaiGaie Koehiho se v 8.4 dostalo pouze na základní integraci SELinuxu. Doufejme, že se během tohoto roku povede dokončit i druhou část, tj. přístupová práva k řádkům (row level security). V 8.5 by se mohly objevit některé funkce, které zjednoduší použití PostgreSQL i pro laiky – např. wizard pro configuraci postgresql.conf nebo index advisor. Začalo se pracovat na SQL/MED, a je dost možné, že se stávající statistiky doplní o statistiku čekání na uvolnění zámků. V 8.5 téměř jistě bude podpora režimu Hot StandBy.

Docela příjemné zjištění je, že do 8.4 nezanedbatelně přispěli i našinci. Jinak práce na Postgresu je stále ještě hodně. Pokud např. hledáte dobré a praktické téma na diplomovou práci a programování máte v malíku, podívejte se na naši nabídku témat diplomových prací. Prací pro velký softwareový projekt je možně se lecčemu naučit a navíc vaše práce nemusí přijít nazmar.

Nerad bych to zakřikl, ale mám určité tušení, že 8.4, tu bude stejně dlouho a bude stejně zásadní jako byla verze 7.4. A dost možná, že další verze už bude první v devítkové řadě – intenzivně se začalo pracovat na integraci replikací a clusteringu do jádra – to se teď rýsuje jako hlavní cíl pro rok 2009. Málokdo dokáže odhadnout co bude v dalších dvou letech, a jak se projeví ekonomická krize. Začíná být patrná snaha ušetřit na licencích Oracle a migrovat na O.S. všechny nebo některé nekritické nebo pro firmy nevýdělečné systémy (tam, kde dříve výhradně používali komerční software). Uvidíme. Čeká nás zajímavý rok.

Našli jste v článku chybu?
Měšec.cz: Ceny PHM v Evropě. Finty na úspory

Ceny PHM v Evropě. Finty na úspory

Vitalia.cz: Galerie: Strouhanka ze starých rohlíků? Kdepak

Galerie: Strouhanka ze starých rohlíků? Kdepak

Vitalia.cz: Očkování je nutné, říká homeopatka

Očkování je nutné, říká homeopatka

Měšec.cz: Do ostravské MHD bez jízdenky. Stačí karta

Do ostravské MHD bez jízdenky. Stačí karta

Měšec.cz: Platíme NFC mobilem. Konečně to funguje!

Platíme NFC mobilem. Konečně to funguje!

Lupa.cz: Samořídicí taxíky jsou tu. Začíná s nimi Uber

Samořídicí taxíky jsou tu. Začíná s nimi Uber

DigiZone.cz: AXN u FreeSatu měsíc zdarma

AXN u FreeSatu měsíc zdarma

DigiZone.cz: Hodlá Markíza skončit v DVB-T?

Hodlá Markíza skončit v DVB-T?

Měšec.cz: Na návštěvě na exekutorském úřadě

Na návštěvě na exekutorském úřadě

Měšec.cz: Co když na dovolené přijdete o kartu?

Co když na dovolené přijdete o kartu?

Root.cz: Xiaomi má vlastní notebook podobný Macu

Xiaomi má vlastní notebook podobný Macu

Měšec.cz: Test: Výběry z bankomatů v cizině a kurzy

Test: Výběry z bankomatů v cizině a kurzy

DigiZone.cz: Jetelín končí. Prima ho vyřadila

Jetelín končí. Prima ho vyřadila

Měšec.cz: Kurzy platebních karet: vyplatí se platit? (TEST)

Kurzy platebních karet: vyplatí se platit? (TEST)

Lupa.cz: Co vzal čas: internetové kavárny a herny

Co vzal čas: internetové kavárny a herny

Podnikatel.cz: OSA zdraží, ale taky přidá nový poplatek

OSA zdraží, ale taky přidá nový poplatek

Lupa.cz: Měřičům síly hesla se nedá věřit. Víte proč?

Měřičům síly hesla se nedá věřit. Víte proč?

Měšec.cz: Se stavebkem k soudu už (většinou) nemusíte

Se stavebkem k soudu už (většinou) nemusíte

Lupa.cz: Kdo vykrádá LinkedIn? Zjistit to má soud

Kdo vykrádá LinkedIn? Zjistit to má soud

Lupa.cz: Elektronika tajemství zbavená. Jak s ní začít?

Elektronika tajemství zbavená. Jak s ní začít?