Živé bitmapy

Co je bitmapa?
Zde je bitmapa jedním druhem indexu. Pokud jste sem přišli s představou grafiky, stačí se vrátit zpět.
Základní pojmy
Bitmapa je jednou z forem indexovacích technik databází. Hodnoty konkrétního sloupce vyjadřuje jako prvek bitového pole nebo vektoru, přičemž každý bit udává, zda se daná hodnota ve sloupci vyskytuje. Nejzákladnější podoba tedy přiřazuje každému řádku bitový index a existenci hodnoty vyjadřuje pomocí 0 a 1.
Představme si například tabulku se sloupcem pohlaví. Tabulka obsahuje 5 řádků a hodnoty sloupce pohlaví jsou následující:
- muž
- žena
- muž
- žena
- jiné
V tomto případě lze bitmapový index sloupce pohlaví vyjádřit takto:
- muž: 10100
- žena: 01010
- jiné: 00001
Tak kde se to skutečně používá
Většina čtenářů si jistě pomyslí, že se bitmapy nepoužívají pouze k vyjádření těchto hodnot. Abychom ukázali, jak se bitmapový index správně využívá, uvažujme následující případ.
1CREATE TABLE IF NOT EXISTS Users (
2 id INT PRIMARY KEY,
3 name VARCHAR(100),
4 active BOOLEAN,
5 sex BOOLEAN,
6 age INT
7);
Předpokládejme, že tato tabulka obsahuje 1 000 000 řádků. Dále spusťme následující dotaz.
1SELECT * FROM Users WHERE active = TRUE AND SEX = TRUE;
Tento dotaz vyhledá řádky, v nichž jsou sloupce active i sex nastaveny na TRUE. Nejrychlejším způsobem, jak najít řádky splňující podmínku, že oba sloupce jsou TRUE, je pravděpodobně využití bitové operace AND.
- Bitmapový index sloupce active je předem indexován jako
1100110011...(bitové pole o 1 000 000 bitech) a - bitmapový index sloupce sex je předem indexován jako
1010101010...(bitové pole o 1 000 000 bitech).
Provedením operace bitové AND nad oběma bitmapovými indexy lze rychle najít řádky, v nichž jsou oba sloupce TRUE. Jelikož jde o jednoduchou bitovou operaci, výsledek bude pravděpodobně získán výrazně rychleji než při použití jiných způsobů průchodu indexem.
Kompromisy
Bitmapové indexy mohou být velmi efektivní, ale mají určité kompromisy. Existuje mnoho dalších nevýhod, nyní se však zaměřme pouze na jednu z nich.
Bitmapové indexy se hodí spíše pro nízkou kardinalitu (tedy případy, kdy sloupec má malý počet možných hodnot). Použití bitmapového indexu u sloupce s vysokou kardinalitou může vést k velmi velkým bitmapám.
Při použití bitmapového indexu u sloupce s vysokou kardinalitou je nutné vytvořit samostatnou bitmapu pro každou jedinečnou hodnotu, a proto může být vyžadováno značné úložiště. Pokud má například sloupec 1 000 000 jedinečných hodnot, je třeba vytvořit 1 000 000 bitmap, což může být velmi neefektivní.
Co tedy použijeme
Problémy bitmap při vysoké kardinalitě lze nahradit použitím struktury nazvané Roaring Bitmap. Roaring Bitmap má řadu vlastností, avšak jeho nejdůležitější principiální myšlenkou a vlastností je dynamicky vybírat nejvhodnější způsob uložení podle hustoty dat, čímž se maximalizuje jak míra komprese, tak rychlost operací.
Základní pojmy Roaring Bitmap
Dvoustupňové indexování
Roaring Bitmap ukládá celé číslo nejprve prostřednictvím těchto dvou kroků. Ačkoli se jedná o dvoustupňové indexování, lze si ho jednoduše představit jako dvourozměrné pole.
- Extrakce horních 16 bitů: horních 16 bitů celého čísla se extrahuje a použije jako index. Jde o index vnějšího pole z dvourozměrného pole o délce 65536. Toto pole ukládá adresu kontejneru, který je popsán níže.
- Extrakce dolních 16 bitů: dolních 16 bitů celého čísla se extrahuje a použije jako pozice uvnitř kontejneru.
Kontejnery
Tento kontejner odpovídá vnitřnímu poli ve dvourozměrném poli, jak jsme přirovnali. Na rozdíl od pole horních 16 bitů se však jeho vnitřní struktura mění podle množství uložených dat. Roaring Bitmap podporuje tyto 3 typy kontejnerů.
- Array Container: tento kontejner se používá, když je v něm uloženo málo dat. Konkrétně se používá pro uložení 4096 nebo méně prvků. Je implementován jako jednoduché seřazené pole celých čísel. Prvek lze vyhledat binárním vyhledáváním.
- Bitmap Container: tento kontejner se používá, když je v něm uloženo mnoho dat. Konkrétně se používá pro uložení více než 4096 prvků. Je implementován jako bitmapa o 65536 bitech (8192 bajtů). Každý bit udává, zda se celé číslo na dané pozici vyskytuje.
- Run Container: tento kontejner se používá k ukládání souvislých rozsahů celých čísel. Je efektivní například pro souvislé hodnoty jako 1, 2, 3, 4, 5. Je implementován jako seznam dvojic (počáteční hodnota, délka). Pokud jsou přítomna všechna čísla? Znamená to, že jsou přítomna od začátku až do konce, a stačí tedy uložit pouze jednu dvojici (0, 65536).
Tento přístup umožňuje, aby Roaring Bitmap velmi efektivně využívala paměť a poskytovala optimální výkon pro různé hustoty dat.
Funkce Roaring Bitmap
Základní funkce
Roaring Bitmap poskytuje následující základní funkce.
- Vložení: přidá nové celé číslo do bitmapy. Během tohoto procesu lze vybrat vhodný kontejner nebo v případě potřeby kontejner převést.
- Odstranění: odebere existující celé číslo z bitmapy. I zde se podle stavu kontejneru provádí příslušné zpracování.
- Vyhledávání: ověří, zda se dané celé číslo v bitmapě nachází. Tento proces probíhá velmi rychle.
Operace nad množinami
Roaring Bitmap dále podporuje následující operace nad množinami.
- Sjednocení (Union): vytvoří novou bitmapu obsahující všechny prvky obou bitmap.
- Průnik (Intersection): vytvoří novou bitmapu obsahující pouze prvky, které se nacházejí v obou bitmapách.
- Symetrický rozdíl (Symmetric Difference): vytvoří novou bitmapu obsahující prvky, které se nacházejí pouze v jedné z obou bitmap.
Tyto operace jsou optimalizovány podle typu kontejneru a provádějí se velmi rychle.
V jazyce Go
V jazyce Go lze Roaring Bitmap snadno používat pomocí balíčku github.com/RoaringBitmap/roaring. Tento balíček poskytuje všechny funkce Roaring Bitmap a je optimalizován podle specifik jazyka Go.
Instalace
1go get github.com/RoaringBitmap/roaring/v2
Příklad použití
1package main
2
3import (
4 "log"
5
6 "github.com/RoaringBitmap/roaring/v2"
7)
8
9func main() {
10 b1 := roaring.BitmapOf(1, 2, 3, 4) // inicializace hodnotami 1, 2, 3, 4
11 b2 := roaring.BitmapOf(2, 4, 6, 8, 12) // inicializace hodnotami 2, 4, 6, 8, 12
12
13 b1.Add(5) // přidání 5
14 b2.Add(10) // přidání 10
15
16 b2.Remove(12) // odebrání 12
17
18 and := roaring.FastAnd(b1, b2)
19 or := roaring.FastOr(b1, b2)
20
21 // operace nad bitmapou mění originál, proto se použije kopie
22 xor := b1.Clone()
23 xor.Xor(b2)
24
25 log.Printf("and result: %v", and.ToArray())
26 log.Printf("or result: %v", or.ToArray())
27 log.Printf("xor result: %v", xor.ToArray())
28}
12025/09/08 22:16:27 and result: [2 4]
22025/09/08 22:16:27 or result: [1 2 3 4 5 6 8 10]
32025/09/08 22:16:27 xor result: [1 3 5 6 8 10]
Mezi hlavní funkce, které nejsou uvedeny v předchozím příkladu, patří:
Contains(x uint32) bool: ověří, zda se daná hodnota v bitmapě nacházíGetCardinality() uint64: vrátí počet prvků obsažených v bitmapěRank(x uint32) uint64: vrátí počet prvků menších nebo rovných dané hodnotěToBytes() ([]byte, error): serializuje bitmapu do bajtového řezuFromBytes(data []byte) error: obnoví bitmapu z bajtového řezuClone() *Bitmap: vytvoří kopii bitmapyClear(): inicializuje bitmapuNextValue(x uint32) int64: vrátí další prvek větší nebo rovný xPreviousValue(x uint32) int64: vrátí předchozí prvek menší než x neboMaximum() uint32: vrátí největší prvek bitmapyMinimum() uint32: vrátí nejmenší prvek bitmapy
Existují tyto funkce. Podrobnosti naleznete v oficiální dokumentaci.
Pro informaci, balíček roaring obsahuje také balíček podporující nejen uint32, ale i uint64. Poskytuje téměř stejné rozhraní, takže jej lze použít přímou záměnou.
1package main
2
3import (
4 "log"
5
6 "github.com/RoaringBitmap/roaring/v2/roaring64"
7)
8
9func main() {
10 b1 := roaring64.BitmapOf(1, 2, 3, 4) // inicializace hodnotami 1, 2, 3, 4
11 b2 := roaring64.BitmapOf(2, 4, 6, 8, 12) // inicializace hodnotami 2, 4, 6, 8, 12
12
13 b1.Add(5) // přidání 5
14 b2.Add(10) // přidání 10
15
16 b2.Remove(12) // odebrání 12
17
18 and := roaring64.FastAnd(b1, b2)
19 or := roaring64.FastOr(b1, b2)
20
21 // operace nad bitmapou mění originál, proto se použije kopie
22 xor := b1.Clone()
23 xor.Xor(b2)
24
25 log.Printf("and result: %v", and.ToArray())
26 log.Printf("or result: %v", or.ToArray())
27 log.Printf("xor result: %v", xor.ToArray())
28}
Závěr
Roaring Bitmap je silný nástroj pro efektivní ukládání a manipulaci s množinami celých čísel s vysokou kardinalitou. V jazyce Go lze pomocí balíčku github.com/RoaringBitmap/roaring snadno využít všechny funkce Roaring Bitmap. Tento balíček díky různým typům kontejnerů a optimalizovaným operacím maximalizuje využití paměti a výkon. Roaring Bitmap lze využít v mnoha oblastech, například při indexování databází, analýze logů nebo výpočtech statistik, k efektivnímu zpracování dat.
Roaring Bitmap vyniká zejména na velkých datových sadách a lze jej použít v mnoha aplikacích. V kombinaci se stručnou a efektivní syntaxí jazyka Go poskytuje vývojářům silný nástroj. Do budoucna lze očekávat další vývoj Roaring Bitmap, s novými funkcemi a optimalizacemi.
Proč jsem to vlastně dělal? Kvůli indexování jednotlivých tagů a rychlým operacím sjednocení v databázi časových řad.