Jsem velkým fanouškem databází. Když jsem byl na univerzitě, chtěl jsem si dokonce vytvořit vlastní DBMS. Nyní pracuji jak s RDBMS, tak s řešeními NoSQL, a jsem tím velmi nadšený. Víte, žádné zlaté kladivo neexistuje, každý problém má vlastní řešení. Případně podmnožinu řešení.
V sérii blogových příspěvků The SQL I Love <3 vás provedu některými problémy řešenými pomocí SQL, které mi přišly obzvlášť zajímavé. Řešení jsou testována na tabulce s více než 100 miliony záznamů. Všechny příklady používají MySQL, ale nápady se dají aplikovat i na jiná relační datová úložiště, jako je PostgreSQL, Oracle a SQL Server.
Tato kapitola je zaměřena na efektivní skenování velké tabulky pomocí stránkování s offset
na primárním klíči. Tato metoda je také známá jako stránkování pomocí sady klíčů.
Pozadí
V kapitole použijeme pro příklad následující strukturu databáze. Kanonický příklad o uživatelích by měl vyhovovat jakékoli doméně.
CREATE TABLE `users` ( `user_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `external_id` varchar(32) NOT NULL, `name` varchar(100) COLLATE utf8_unicode_ci NOT NULL, `metadata` text COLLATE utf8_unicode_ci, `date_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`), UNIQUE KEY `uf_uniq_external_id` (`external_id`), UNIQUE KEY `uf_uniq_name` (`name`), KEY `date_created` (`date_created`)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Několik poznámek ke struktuře:
-
external_id
sloupec ukládá odkaz na stejného uživatele v jiném systému ve formátu UUID -
name
představujeFirstname Lastname
-
metadata
sloupec obsahuje blob JSON s nejrůznějšími nestrukturovanými daty
Tabulka je poměrně velká a obsahuje přibližně 100 000 000 záznamů. Pojďme se pustit do učení.
Skenování velké tabulky
Problém: Je třeba projít tabulku, extrahovat jednotlivé záznamy, transformovat je uvnitř kódu aplikace a vložit na jiné místo. V příspěvku se zaměříme na první fázi – skenování tabulky.
Očividné a špatné řešení
SELECT user_id, external_id, name, metadata, date_createdFROM users;
V mém případě se 100 000 000 záznamy není dotaz nikdy dokončen. DBMS ho prostě zabije. Proč? Pravděpodobně proto, že vedl k pokusu o načtení celé tabulky do paměti RAM. Před vrácením dat klientovi. Další domněnka – před načtením dat před odesláním to trvalo příliš dlouho a dotaz byl časován. Každopádně náš pokus získat všechny záznamy včas selhal. Musíme najít nějaké jiné řešení.
Řešení č. 2
Můžeme se pokusit získat data ve stránkách. Protože není zaručeno, že záznamy budou v tabulce seřazeny na fyzické nebo logické úrovni – musíme je seřadit na straně DBMS pomocí klauzule ORDER BY
.
SELECT user_id, external_id, name, metadata, date_createdFROM usersORDER BY user_id ASCLIMIT 0, 10 000;10 000 rows in set (0.03 sec)
Sladké. Funguje to. Zeptali jsme se na první stránku s 10 000 záznamy a její vrácení trvalo jen 0.03
sec. Jak by to však fungovalo pro 5000. stránku?“
SELECT user_id, external_id, name, metadata, date_createdFROM usersORDER BY user_id ASCLIMIT 50 000 000, 10 000; --- 5 000th page * 10 000 page size10 000 rows in set (40.81 sec)
Jistě, je to velmi pomalé. Podívejme se, kolik času je potřeba na získání dat pro poslední stránku.
SELECT user_id, external_id, name, metadata, date_createdFROM usersORDER BY user_id ASCLIMIT 99 990 000, 10 000; --- 9999th page * 10 000 page size10 000 rows in set (1 min 20.61 sec)
To je šílené. Může však být v pořádku pro řešení, která běží na pozadí. Ještě jeden skrytý problém tohoto přístupu lze odhalit, pokud se pokusíte odstranit záznam z tabulky uprostřed jejího skenování. Řekněme, že jste dokončili desátou stránku (100 000 záznamů je již navštíveno) a chystáte se skenovat záznamy mezi 100 001 a 110 000 záznamy. Záznamy 99 998 a 99 999 jsou však odstraněny před dalším SELECT
provedením. V takovém případě následující dotaz vrátí neočekávaný výsledek:
SELECT user_id, external_id, name, metadata, date_created FROM users ORDER BY user_id ASC LIMIT 100 000, 10 000; N, id, ... 1, 100 003, ... 2, 100 004, ...
Jak vidíte, dotaz přeskočil záznamy s id 100 001 a 100 002. Záznamy s id 100 001 a 100 002 byly přeskočeny. Ty kód aplikace s tímto přístupem nezpracuje, protože se po dvou operacích smazání objeví v prvních 100 000 záznamech. Proto je metoda nespolehlivá, pokud je datová sada proměnlivá.
Řešení č. 3 – pro dnešek poslední
Přístup je velmi podobný předchozímu, protože stále používá stránkování, ale nyní místo spoléhání na počet skenovaných záznamů používáme jako offset
user_id
posledního navštíveného záznamu.
Zjednodušený algoritmus:
- Z tabulky získáme
PAGE_SIZE
počet záznamů. Počáteční hodnota offsetu je 0. - Použijeme maximální vrácenou hodnotu pro
user_id
v dávce jako offset pro další stránku. - Získáme další dávku ze záznamů, které mají hodnotu
user_id
vyšší než aktuálníoffset
.
Dotaz v akci pro 5 000. stránku, každá stránka obsahuje údaje o 10 000 uživatelích:
SELECT user_id, external_id, name, metadata, date_createdFROM usersWHERE user_id > 51 234 123 --- value of user_id for 50 000 000th recordORDER BY user_id ASCLIMIT 10 000;10 000 rows in set (0.03 sec)
Všimněte si, že hodnoty user_id
nejsou po sobě jdoucí a mohou mít mezery, například 25 348 je hned za 25 345. Řešení funguje i v případě, že jsou smazány některé záznamy z budoucích stránek – ani v tomto případě dotaz nepřeskočí záznamy. Sladké, že?“
Vysvětlení výkonu
Pro další učení doporučuji prozkoumat výsledky EXPLAIN EXTENDED
pro každou verzi dotazu, abyste získali dalších 10 000 záznamů po 50 000 000.
Řešení | Čas | Typ | Klíče | Řádky | Filtrováno | Extra |
---|---|---|---|---|---|---|
1. Obvious | Never | ALL | NULL | 100M | 100.00 | NULL |
2. Stránkování pomocí počtu záznamů jako offset | 40,81 s | index | NULL / PRIMARY | 50M | 200,00 | NULL |
3. Stránkování klíčů pomocí user_id jako offset | 0,03 s | rozsah | PRIMARY / PRIMARY | 50M | 100.00 | Použití where |
Soustřeďme se na klíčový rozdíl mezi prováděcími plány pro 2. a 3. řešení, protože 1. řešení není pro velké tabulky prakticky použitelné.
- Typ Join:
index
vsrange
. První z nich znamená, že se při hledání záznamů prohledává celý strom indexů. Typrange
nám říká, že index se používá pouze k nalezení odpovídajících řádků v zadaném rozsahu. Typrange
je tedy rychlejší nežindex
. - Možné klíče:
NULL
vsPRIMARY
. Ve sloupci jsou uvedeny klíče, které může MySQL použít. BTW, při pohledu do sloupce klíčů vidíme, že nakonec je pro oba dotazy použit klíčPRIMARY
. - Řádky:
50 010 000
vs50 000 000
. Hodnota zobrazuje počet analyzovaných záznamů před vrácením výsledku. U 2. dotazu hodnota závisí na tom, jak hluboké je naše rolování. Pokud se například snažíme získat další10 000
záznamů po 9999. stránce, pak se zkoumá99 990 000
záznamů. Naopak 3. dotaz má konstantní hodnotu; nezáleží na tom, zda načítáme data pro 1. stránku úplně poslední. Vždy se jedná o polovinu velikosti tabulky. - Filtrováno:
200.00
vs100.00
. Sloupec udává odhadované procento tabulky, které má být před zpracováním filtrováno. Mít vyšší hodnotu je lepší. Hodnota100.00
znamená, že dotaz prochází celou tabulku. U 2. dotazu není hodnota konstantní a závisí na čísle stránky: pokud se ptáme na 1. stránku, hodnota filtrovaného sloupce by byla1000000.00
. Pro úplně poslední stránku by byla100.00
. - Extra:
NULL
vsUsing where
. Poskytuje další informace o tom, jak MySQL řeší dotaz. PoužitíWHERE
na klíčiPRIMARY
zrychluje provádění dotazu.
Předpokládám, že typ spojení je parametr dotazu, který nejvíce přispěl k výkonu, aby byl 3. dotaz rychlejší. Další důležitou věcí je, že 2. dotaz je extrémně závislý na počtu stránek k procházení. Hlubší stránkování je v tomto případě pomalejší.
Další návod, jak pochopit výstup příkazu EXPLAIN
, najdete v oficiální dokumentaci k vašemu RDBMS.
Shrnutí
Hlavní téma příspěvku na blogu se týkalo skenování velké tabulky se 100 000 000 záznamy pomocí offset
s primárním klíčem (stránkování podle klíče). Celkem byly přezkoumány 3 různé přístupy, které byly testovány na odpovídajícím souboru dat. Pokud potřebujete skenovat proměnlivou velkou tabulku, doporučuji pouze jeden z nich.
Také jsme revidovali použití příkazu EXPLAIN EXTENDED
pro analýzu plánu provádění dotazů MySQL. Jsem si jist, že i jiné RDBMS mají pro tuto funkci obdobu.
V příští kapitole budeme věnovat pozornost agregaci dat a optimalizaci ukládání. Zůstaňte naladěni!
Jaká je vaše metoda skenování velkých tabulek?
Pamatujete si na nějaký další účel použití stránkování sad klíčů jako v řešení č. 3?
.