Hlavní navigace

Několik poznámek ke sloupcovým databázím

Pavel Stěhule 30. 3. 2015

Se sloupcovými databázemi se v posledních pěti letech setkávám stále častěji. Myslím si, že během těch následujících pěti let se s nimi setká stále více uživatelů. Jedná se o relativně starý koncept, který ale teprve v posledních deseti letech našel svoje uplatnění. Proč mohou být dnes výrazně rychlejší?

Dneska téměř každé CPU použité na serverech má výkon a vlastnosti, které měly dříve pouze superpočítače. Také se analyzují větší data – u těch velkých chceme výsledky v řádu hodin, u těch středně velkých v minutách nebo ještě lépe ve vteřinách. Sloupcové databáze (zejména ty moderní) umí velice dobře využívat výkon CPU a tudíž mohou spočítat analytické úlohy velice rychle.

Když se pomyslně vrátím o 30 – 40 let zpátky do časů, kdy vznikaly klasické relační databáze, výkonu CPU (a zrovna tak paměti) nebylo nazbyt. Na relačních databázích je zřetelně vidět, jak jsou navržené tak, aby dokázaly zpracovat hromadné zpracování dat s minimem zdrojů. Silně je preferováno sekvenční čtení, které bylo výrazně rychlejší než čtení s náhodným přístupem. Data jsou uložena normalizovaně – což také znamená, že na jejich zpracování nebude potřeba příliš strojových instrukcí (často stačí načíst blok do paměti a použít N bajtů). Exekutor traverzuje prováděcí plán po řádcích, a tak si vystačí s minimem paměti.

Interně jsou tyto databáze tvořené kombinací několika různých interpretů – o kompilaci do strojového kódu se prakticky nikdo nesnažil. IO operace byly tehdy takovým úzkým hrdlem, že optimalizace na úrovni CPU nepřinášela výrazný benefit. Nehledě na to, že na případnou kompilaci byste potřebovali další výkon CPU, kterého nebylo nazbyt (paradoxně úplně v nejdřevnějších dobách se dotazy skutečně kompilovaly do strojového kódu, ale jakmile bylo dost výkonu a paměti na ad hoc optimalizaci a exekuci, tato praxe se opustila). 

Všechno se změnilo s nárůstem výkonu CPU a kapacit pamětí v posledních deseti letech. Volná paměť se může využít jako cache pro IO, čímž se přesouvá úzké hrdlo z IO na CPU, a to je zatím konečná klasických relačních databází. Každé zatím nalezené řešení má aspoň jednu zásadní nevýhodu. Paralelizace komplikuje kód aplikace, zvyšuje počet zámků, vynucuje mezi procesovou komunikaci a synchronizaci. Kompilace, případně JIT kompilace komplikuje kód, a výrazně zpomaluje start. Sloupcové databáze dosahují výrazně vyššího výkonu v agregaci (o řád až dva řády) bez paralelizace, bez kompilace. A to díky výrazně efektivnějšímu využití CPU.

Moderní CPU jsou extrémně výkonná – tedy pokud nečekají na data. Maximální rychlost je podmíněna daty v cache CPU. Díky tomu, že v sloupcové databázi máme data uložena po sloupcích, je velice jednoduché a přirozené provádět operace v blocích, které odpovídají velikosti L1, L2 cache CPU. Pokud dokážeme (a u sloupcových databází to dokážeme docela snadno) minimalizovat počet výpadků cache, můžeme dosáhnout překvapivě vysoké rychlosti zpracování analytických úloh. V následující ukázce použiji dvourozměrné pole jako analogii k tabulce. Podmíněná agregace, podmíněný součet bych mohl implementovat následujícím způsobem:

int sum = 0;
for (r = 0; r < MAX_ROWS; r++)
    if (rel[r, 12] = 1)
        sum += rel[r, 10];

Analogicky bych tabulku ve sloupcových databází vyjádřil jako pole polí odpovídajících jednotlivým sloupcům. Podmíněný součet by mohl vypadat třeba takto:

int sum = 0;
int *s1 = rel[12];
int *s2 = rel[10];
for (r = 0; r < MAX_ROWS; r++)
    if (s1[r] = 1)
        sum += s2[r];

Druhý kód by měl být (při dostatečném počtu hodnot – stovky tisíc hodnot) výrazně rychlejší (s možností hardwarové akcelerace – viz SIMD instrukce). To ale není jediný trumf. Při analytických dotazech často čteme data jen několika málo sloupců z tabulky. U klasické databáze čteme vždy celé řádky – tedy i data sloupců, které se v dotazu neuplatní. Při použití jako OLTP databáze toto chování nemusí být až tak nevýhodné – je hodně velká šance, že během doby, kdy datová stránka bude v cache, přijde dotaz, který bude pracovat s ostatními (zatím nepoužitými sloupci). Navíc u OLTP databází se doporučuje navrhovat a používat rozumně spíš úzké tabulky.

U analytických databázi (OLAP) je to jinak. Často se pracuje s daty, které se nevejdou do cache (a tudíž tam nemají šanci zůstat). Navíc analytici rádi pracují s denormalizovaným schématem typu hvězda nebo sněhová vločka, které vede na široké velké tabulky. Zde čtení dat, které nepotřebujeme, zamrzí. Jenom díky uložení dat do samostatných souborů po sloupcích může vést k velkému zrychlení provádění dotazů – ve chvíli, kdy se nám data nevejdou do cache, IO operace (čtení) zřetelně zpomalují provádění dotazů.

Posledním esem v rukávu (sloupcových databází) je komprimace dat. Na rozdíl od klasických databází se v jednom bloku nemíchají data různých typů, různého charakteru. Docela často lze taková data velice efektivně komprimovat – a jelikož máme dostatek výkonu v CPU, a dost paměti, můžeme si komprimaci bezpečně dovolit. Díky komprimaci můžeme výrazně snížit počet „pomalých“ IO operací (stejná data ve Vertice jsou třetinová vůči Postgresu). Vertica je expertem na komprimaci dat. Byť jsou data uložena duplicitně (v tzv. uspořádaných projekcích), jejich celková velikost bývá cca 1/3 odpovídající klasické relační databáze. Moderní sloupcové databáze toho umí víc – pro zájemce doporučuji prostudovat výbornou monografii The Design and Implementation of Modern Column-Oriented Database Systems, jejímiž spoluautory jsou Daniel Abadi (jeden z hlavních vývojářů Vertiky) a Peter Bonz (jeden z vedoucích vývojářů a výzkumníků MonetDB).

V následujících ukázkách vedle sebe postavím MonetDB, PostgreSQL (cstore) a PostgreSQL. PostgreSQL je zástupcem klasických řádkových relačních SQL databází. MonetDB je jednou z nejstarších sloupcových databází, která používá optimalizovaný sloupcový exekutor. Data jsou uložena nekomprimovaně po sloupcích (autoři doporučují použití souborového systému s transparentní komprimací). Nechybí podpora ANSI SQL včetně podpory uložených procedur. cstore můžeme považovat za mezistupeň mezi sloupcovými a řádkovými databázemi – data jsou uložená sloupcově, vlastní zpracování dotazu řeší exekutor Postgresu (tj. data se zpracovávají po řádcích). Velkou výhodou cstore je jeho integrace do Postgresu (je to extenze Postgresu) – s výjimkou nepodporovaných operací UPDATE a DELETE  nemusíte poznat, že data nejsou uložena v Postgresu (pozor – tabulky v cstore jsou append only). Instalace a provoz jsou triviální.

Pro testování použiji databázi letů v USA z roku 2008 (29 sloupců, 7M řádek). Uvedené časy jsou pro minimálně druhé vykonání dotazu (čímž se na této velikosti dat (806 MB v csv formátu) více méně potlačí vliv rychlosti IO operací (na mém notebooku)):

Mám čtyři dotazy (ukázka je z MonetDB):

1. počet letů podle dnů v týdnu

sql>SELECT COUNT(*), dayofweek FROM x GROUP BY dayofweek ORDER BY 2;
+---------+-----------+
| L1      | dayofweek |
+=========+===========+
| 1036201 |         1 |
| 1032049 |         2 |
| 1039665 |         3 |
| 1032224 |         4 |
| 1035166 |         5 |
|  857536 |         6 |
|  976887 |         7 |
+---------+-----------+
7 tuples (56.551ms)

2. počet letů podle měsíců

sql>SELECT COUNT(*), _month FROM x GROUP BY _month ORDER BY 2;
+--------+--------+
| L1     | _month |
+========+========+
| 605765 |      1 |
| 569236 |      2 |
| 616090 |      3 |
| 598126 |      4 |
| 606293 |      5 |
| 608665 |      6 |
| 627931 |      7 |
| 612279 |      8 |
| 540908 |      9 |
| 556205 |     10 |
| 523272 |     11 |
| 544958 |     12 |
+--------+--------+
12 tuples (46.579ms)

3. procento zrušených letů podle měsíců

sql>SELECT truncate(count(CASE cancelled WHEN 1 THEN 1 ELSE null END) / CAST(count(*) AS float)*100.0, 4), _month
    FROM x GROUP BY _month ORDER BY 2 desc;
+------+--------+
| L4   | _month |
+======+========+
| 3.26 |     12 |
| 0.85 |     11 |
| 0.58 |     10 |
| 1.83 |      9 |
| 1.60 |      8 |
| 1.68 |      7 |
| 1.79 |      6 |
| 1.02 |      5 |
| 1.73 |      4 |
| 2.62 |      3 |
| 3.61 |      2 |
| 2.85 |      1 |
+------+--------+
12 tuples (85.598ms)

4. hledání maxim – počet letů a nejdelší vzdálenost letu

sql>SELECT count(*), max(distance) FROM x WHERE origin = 'ATL';
+--------+------+
| L1     | L2   |
+========+======+
| 414513 | 4502 |
+--------+------+
1 tuple (115.353ms)

Všechny časy jsou pod nebo jen těsně nad 100ms. Z toho, že časy dotazů jsou perfektní, je vidět, že takové dotazy a zhruba taková velikost tabulek je přesně parketa MonetDB. MonetDB potřebuje, aby se mu aktivní data rozumně vešla do operační paměti (má příliš jednoduchou správu paměti, vždy celý sloupec natahuje do paměti a server nesmí aktivně swapovat). Na takto jednoduchých dotazech není co optimalizovat – jde pouze o rychlost rozbalení dat (separace vybraných sloupců z řádek) a rychlost volání agregačních funkcí.

Orientační měření na malých datech

dotaz PostgreSQL cstore cstore pglz MonetDB
1. 1660 ms 1504 ms 1550 ms 56ms
2. 1619 ms 1509 ms 1538 ms 46ms
3. 3044 ms 1940 ms 1997 ms 85ms
4. 1627 ms 877 ms 930 ms 115ms

Rád bych se zastavil u třetího dotazu – lze jej částečně optimalizovat použitím podmíněné agregační funkce (v PostgreSQL od 9.4) – (2857 ms):

SELECT count(*) FILTER (WHERE cancelled = 1) / CAST(count(*) AS float)*100.0, _month
   FROM x
  GROUP BY _month
  ORDER BY 2 desc;

Tady bych ještě zmínil významný vliv pořadí sloupců na dobu běhu dotazu u Postgresu (tabulka má 683 MB – tudíž se pohodlně vejde do paměti, a tak veškerá režie s CPU je dobře vidět). Zase abych vás zbytečně nestrašil – na 7 mil. řádků je rozdíl 1 sec. Pokud bych čekal na IO, nehrála by ta jedna sekunda roli.

-- základní iterace nad tabulkou
-- ukázka z PostgreSQL
postgres=# SELECT count(*) FROM x;
  count
---------
 7009728
(1 row)

Time: 657.830 ms

-- agregační funkce nad prvním sloupcem
postgres=# SELECT count(_year) FROM x;
  count
---------
 7009728
(1 row)

Time: 832.020 ms

-- agregační funkce nad 12 sloupcem
postgres=# SELECT count(cancelled) FROM x;
  count
---------
 7009728
(1 row)

Time: 1850.515 ms

cstore má tabulka 448 MB. Data jsou uložena nekomprimovaně – rozdíl vůči Postgresu dělají režijní data multigenerační architektury.

-- inicializace tabulky v cstore
CREATE EXTENSION cstore_fdw;
CREATE SERVER cstore_server FOREIGN DATA WRAPPER cstore_fdw;

CREATE FOREIGN TABLE cs_x (
    _year smallint,
    _month smallint,

    ...

    nasdelay smallint,
    securitydelay smallint,
    lateaircraftdelay smallint
) SERVER cstore_server;

INSERT INTO cs_x SELECT * FROM x;

postgres=# SELECT count(cancelled) FROM cs_x;
  count
---------
 7009728
(1 row)

Time: 750.607 ms
postgres=# SELECT count(_year) FROM cs_x;
  count
---------
 7009728
(1 row)

Time: 768.073 ms

Zde je zřetelně vidět, že nezáleží na pořadí sloupce v tabulce. Pro relativně malá data je cstore rychlejší někde mezi 10 až 30 %. Data do 1GB z dnešního pohledu nejsou velká, ale na druhou stranu – je to statistika všech letů v USA během jednoho roku.

cstore umí komprimovat v něm uložená data. Po komprimaci jsou data v cstore uložena do 160MB souboru. U malých datasetů dekomprimace cstore mírně zpomaluje (představuje pouze režii), což by nikoho nemělo překvapit – používá se interní komprimace z Postgresu, která vůbec není optimalizována pro použití v sloupcových databází.

Orientační měření na větších datech

Data do 1GB jsou relativně malá a na mém notebooku se vejdou do RAM – a tudíž efekt uložení dat ve sloupcích na IO operace je spíše malý. Proto jsem si vygeneroval ještě jednu sadu tabulek – jejich desetinásobek. Jelikož používám šifrovaný disk, měly by sloupcové databáze dát Postgresu dost na frak – bude velký rozdíl v objemu dešifrovaného obsahu.

DO $$ BEGIN FOR i IN 1..10 LOOP INSERT INTO xx SELECT * FROM x; END LOOP; END $$;
..

Tabulka xx má 6,8 GB v Postgresu, 4,3 GB v nekomprimovaném cstore, a 1,5 GB v komprimovaném cstore. Jenom rozdíl velikost 5 GB bude znamenat rozdíl v rychlosti minimálně minutu (na mém notebooku). Následující výsledky jsou pro první spuštění dotazu (kromě trvání dotazu je uvedena i intenzita čtení dat):

dotaz PostgreSQL cstore cstore pglz MonetDB
1. 97sec(60MB/s) 30sec(11MB/s) 20sec(4MB/s) 7sec
2. 96sec(60MB/s) 22sec(11MB/s) 19sec(2MB/s) 7sec
3. 104sec(60MB/s) 42sec(11MB/s) 22sec(2.5MB/s) 7sec
4. 105sec(60MB/s) 30sec(19MB/s) 17sec(11MB/s) 2.5sec

U Postgresu bych si mohl trochu pomoci indexy – s natlačením Postgresu do index only scanu bych se měl dostat na časy blízké cstore. Nicméně si nejsem jistý, jestli by se mi do toho v praxi chtělo. Index nad každým sloupečkem fakticky znamená extrémně pomalé INSERTy, UPDATEy  – v Postgresu navíc efektivně vyblokujete tzv hot update – což je způsob modifikace řádku, který minimalizuje nutnost volání příkazu  VACUUM.

cstorem se dostávám na časy cca 25–50 % Postgresu, přičemž je výrazně méně zatížené IO (které může rychleji obsluhovat ostatní uživatele). Při těchto úlohách je MonetDB 20× rychlejší než Postgres a 5–10× rychlejší než cstore (a pokud nalezne data v paměti – tak ještě může být MonetDB o další řád rychlejší). Tabulka s daty x má pouze 29 sloupců. V OLAPu se pracuje s tabulkami s více než stovky sloupců. Samozřejmě, že potom pouze uložení dat ve sloupcích vede k ohromnému zrychlení. Na této myšlence je založena existence forku PostgreSQL – Greenplum.

Rozdíl až o dva řády je ohromující, byť na testu, pro který je MonetDB navrhován a dlouhodobě optimalizován. Pro analytické úlohy (typicky masivní agregace) je sloupcové uložení dat evidentně výhodné. Opačná situace je u OLTP, ačkoliv je otázkou, jak ještě dlouho (zítřek ještě bude patřit klasickým databázím, ale nepochybuji, že do pěti let bude většina velkých databází podporovat alternativní uložení dat ve sloupcích). INSERT, UPDATE, DELETE jednoho řádku (u řádkových databází) ovlivní typicky pouze jeden blok dat (jednu datovou stránku). U sloupcových databází pak v případě INSERTu a DELETE  řádku musíme zmodifikovat N bloků, kde N je počet sloupců. A jelikož se u sloupcových databází používají větší bloky (32, 64KB), znamená to, že se musí manipulovat s větším objemem dat, a že se musí zapisovat větší objem dat.

Zatímco u klasických relačních databází řádkové DML operace jsou docela jednoduché a nenáročné, jsou u sloupcových databází drahé a netriviální, pokud mají být efektivní. Na druhou stranu u OLAP databází nepotřebujeme rychlé jednořádkové DML operace. Často se používají pouze operace typu masivního importu (bulk load). Také se tyto databáze označují jako databáze optimalizované pro čtení (read optimized).

postgres=# CREATE UNLOGGED TABLE x_c AS SELECT * FROM x;
SELECT 7009728
Time: 19013.222 ms

postgres=# INSERT INTO cs_x SELECT * FROM x;
INSERT 0 7009728
Time: 30954.555 ms

-- komprimovaný cstore
postgres=# INSERT INTO cs_x_pglz SELECT * FROM x;
INSERT 0 7009728
Time: 35206.968 ms

-- MonetDB
sql>INSERT INTO x1 SELECT * FROM x;
7009728 affected rows (21.0s)

Nicméně na první pohled MonetDB pomalými DML operacemi netrpí – jejich rychlost byla srovnatelná s PostgreSQL. Manipulace s daty je sice náročnější, ale rychleji se dohledají data, která se mají modifikovat.

MonetDB je hodně zajímá databáze. A její autoři jsou opravdu chytří lidé. Velmi dobře odhadli budoucí vývoj. MonetDB je v prvé řadě výzkumný projekt (a to jeden z nejcitovanějších (nejdůležitějších) v této oblasti). To, bohužel, také znamená, že vyjma akademické obce se o této databázi prakticky neví. Docela dlouhou dobu bylo SQL pouze jedním z možných API k této databázi. Teprve v posledních třech možná pěti letech se autoři víc soustředili na SQL a dotáhli jej do provozně použitelného stavu. Monetu nepomáhá ani slabší dokumentace – v podstatě chybí jakákoliv ucelenější podrobnější informace. Víc než z dokumentace se dozvíte z prezentací a článků pro konference a sborníky (což je nepraktické, ale logické, protože tuto databázi nepíše komunita vývojářů pro uživatele, ale profesoři a doktorandi pro svůj výzkum). Pokud ale nepotřebujete vědět, jak MonetDB interně funguje, podle dokumentace ho jednoduše nainstalujeterozběhnete. Ukázkou jinakosti MonetDB může být už i pouhý výpis prováděcího jednoduššího plánu (to se opravdu nepodobá žádné jiné databázi):

sql>EXPLAIN SELECT count(*), max(distance) FROM x WHERE origin = 'ATL';
+-------------------------------------------------------------------------------------------------------------+
| mal                                                                                                         |
+=============================================================================================================+
| function user.s3_1{autoCommit=true}(A0:str):void;                                                           |
|     X_3 := sql.mvc();                                                                                       |
|     X_7:bat[:oid,:str]  := sql.bind(X_3,"sys","x","origin",0);                                              |
|     X_4:bat[:oid,:oid]  := sql.tid(X_3,"sys","x");                                                          |
|     X_56 := algebra.subselect(X_7,X_4,A0,A0,true,true,false);                                               |
|     (X_10,r1_10) := sql.bind(X_3,"sys","x","origin",2);                                                     |
|     X_57 := algebra.subselect(r1_10,A0,A0,true,true,false);                                                 |
|     X_13:bat[:oid,:str]  := sql.bind(X_3,"sys","x","origin",1);                                             |
|     X_58 := algebra.subselect(X_13,X_4,A0,A0,true,true,false);                                              |
|     X_15 := sql.subdelta(X_56,X_4,X_10,X_57,X_58);                                                          |
|     X_17 := sql.projectdelta(X_15,X_7,X_10,r1_10,X_13);                                                     |
|     X_18 := aggr.count(X_17);                                                                               |
|     X_19:bat[:oid,:sht]  := sql.bind(X_3,"sys","x","distance",0);                                           |
|     (X_21,r1_24) := sql.bind(X_3,"sys","x","distance",2);                                                   |
|     X_23:bat[:oid,:sht]  := sql.bind(X_3,"sys","x","distance",1);                                           |
|     X_24 := sql.projectdelta(X_15,X_19,X_21,r1_24,X_23);                                                    |
|     X_25 := aggr.max(X_24);                                                                                 |
|     X_26 := sql.resultSet(2,1,X_18);                                                                        |
|     sql.rsColumn(X_26,"sys.L1","L1","wrd",64,0,X_18);                                                       |
|     sql.rsColumn(X_26,"sys.L2","L2","smallint",16,0,X_25);                                                  |
|     X_38 := io.stdout();                                                                                    |
|     sql.exportResult(X_38,X_26);                                                                            |
| end s3_1;                                                                                                   |
| # querylog.define("explain select count(*), max(distance) from x where origin = \\'ATL\\';","default_pipe") |
| # optimizer.mitosis()                                                                                       |
| # optimizer.dataflow()                                                                                      |
+-------------------------------------------------------------------------------------------------------------+
26 tuples (1.249ms)

Vespod MonetDB je univerzální sloupcový databázový kernel „Goblin“. Díky němu tato databáze nemusí být pouze sloupcovou SQL databází. Za posledních 15 let už byla: XQuery databází, RDF SPARQL databází, proudovou databází (DataCell). Vývojáři experimentují s podporou polí (SciQL). Implementace nového interface spočívá „pouze“ v napsání překladače do interního jazyka MAL – MonetDB Assembly Language. Nepřestává mne překvapovat, jak je tato architektura univerzální a při vší té komplexnosti rychlá. MonetDB zjevně vyvrací tvrzení, že SQL databáze jsou pomalé. Rychlost nezáleží na API, ale na tom, jak vhodně jsou uložena data, a jakým způsobem se zpracovávají.

Pro OLTP bych asi MonetDB nepoužil, jelikož pro něj platí omezení, že pouze jeden uživatel může efektivně modifikovat jeden sloupec (aktuálně pouze s optimistickým zamykáním) – to by v multiuživatelské OLTP aplikaci nefungovalo (sami autoři před takovým použitím varují). V případě analytické aplikace bych už asi váhal (tam, kde chci mít rychlé agregace a mám star schema) – Postgres funguje dobře – ale ten výkon Monetu vždy bude stát za úvahu. A pro všechny Rkaře – MonetDB je dobře integrován s Rkem.

Našli jste v článku chybu?

31. 3. 2015 4:29

Pokud máte pocit, že Pavel Stěhule neumí SQL, hledejte chybu na své straně.

30. 3. 2015 21:57

Několikařádkové DML operace byly rychlostně srovnatelné s PostgreSQL - v typicky OLAP režimu (v závěru článku to zmiňuji). Testoval jsem masivní insert, který byl cca o 20% pomalejší než u PostgreSQL - nicméně na PostgreSQL nebyl jediný index. Každý index by tento insert o hodně zpomalil.

Tento článek je o použití OLAP databáze na OLAP zátěž. Jelikož většina čtenářů nemá zkušenosti s žádnou OLAP databází (a dost často PostgreSQL používají i pro OLAP), tak používám srovnání s PostgreSQL - kde si…

Měšec.cz: Zdravotní a sociální pojištění 2017: Připlatíte

Zdravotní a sociální pojištění 2017: Připlatíte

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: Babiše přesvědčila 89letá podnikatelka?!

Babiše přesvědčila 89letá podnikatelka?!

Vitalia.cz: Pečete cukroví a zbyl vám bílek?

Pečete cukroví a zbyl vám bílek?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

DigiZone.cz: Rádio Šlágr má licenci pro digi vysílání

Rádio Šlágr má licenci pro digi vysílání

Podnikatel.cz: Na poslední chvíli šokuje výjimkami v EET

Na poslední chvíli šokuje výjimkami v EET

Vitalia.cz: Proč vás každý zubař posílá na dentální hygienu

Proč vás každý zubař posílá na dentální hygienu

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

120na80.cz: Horní cesty dýchací. Zkuste fytofarmaka

Horní cesty dýchací. Zkuste fytofarmaka

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Vitalia.cz: Dáte si jahody s plísní?

Dáte si jahody s plísní?

Root.cz: Certifikáty zadarmo jsou horší než za peníze?

Certifikáty zadarmo jsou horší než za peníze?

Lupa.cz: Insolvenční řízení kvůli cookies? Vítejte v ČR

Insolvenční řízení kvůli cookies? Vítejte v ČR

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu