Żywy Bitmap
Czym jest Bitmapa?
Bitmapa w tym kontekście jest rodzajem indeksu. Jeżeli przyszli Państwo z myślą o grafice, proszę nacisnąć przycisk "Wstecz".
Podstawowa Koncepcja
Bitmapa jest formą techniki indeksowania w bazach danych. Wartość określonej kolumny jest reprezentowana jako element lub forma tablicy bitów lub wektora, gdzie każdy bit wskazuje na istnienie lub brak określonej wartości w tej kolumnie. Zatem, najbardziej podstawowa forma polega na przypisaniu indeksu bitowego do każdego wiersza (row) i reprezentowaniu istnienia za pomocą 0 i 1.
Na przykład, załóżmy, że mamy tabelę z kolumną "Płeć". Załóżmy, że w tej tabeli jest 5 wierszy, a wartości w kolumnie "Płeć" są następujące:
- Mężczyzna
- Kobieta
- Mężczyzna
- Kobieta
- Inne
W tym przypadku, indeks bitmapowy dla kolumny "Płeć" może być reprezentowany w następujący sposób:
- Mężczyzna: 10100
- Kobieta: 01010
- Inne: 00001
Zatem, Gdzie To Jest Faktycznie Używane?
Wszyscy zapewne myślą, że nie służy to jedynie do reprezentowania tych prostych danych. Aby właściwie wykorzystać indeks bitmapowy, rozważmy następujący przypadek.
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);
Załóżmy, że ta tabela zawiera 1 000 000 wierszy. I załóżmy, że wykonujemy następujące zapytanie (query).
1SELECT * FROM Users WHERE active = TRUE AND SEX = TRUE;
To zapytanie szuka wierszy, w których kolumny active i sex są jednocześnie TRUE. Najszybszą metodą na znalezienie wierszy spełniających warunek, że obie kolumny są TRUE, będzie wykorzystanie operacji bitowej AND.
- Załóżmy, że indeks bitmapowy dla kolumny active jest wstępnie zindeksowany jako
1100110011...
(tablica 1 000 000 bitów), oraz - Załóżmy, że indeks bitmapowy dla kolumny sex jest wstępnie zindeksowany jako
1010101010...
(tablica 1 000 000 bitów).
Wówczas, wykonując operację bitową AND na tych dwóch indeksach bitmapowych, można szybko znaleźć wiersze, w których obie kolumny są TRUE. Ponieważ jest to prosta operacja bitowa, wynik można uzyskać znacznie szybciej niż w przypadku skanowania innych indeksów.
Kompromis (Trade-Off)
Indeksy bitmapowe mogą być bardzo wydajne, ale wiążą się z kilkoma kompromisami. Istnieje wiele innych wad, ale teraz skupimy się tylko na jednej.
Mianowicie, indeksy bitmapowe są bardziej odpowiednie dla niskiej kardynalności (tj. gdy liczba możliwych wartości w kolumnie jest mała). Użycie indeksu bitmapowego dla kolumny o wysokiej kardynalności może spowodować, że bitmapa stanie się bardzo duża.
Jeśli użyjemy indeksu bitmapowego dla kolumny o wysokiej kardynalności, konieczne jest utworzenie oddzielnej bitmapy dla każdej unikalnej wartości, co może wymagać dużej przestrzeni dyskowej. Na przykład, jeśli kolumna ma 1 000 000 unikalnych wartości, trzeba by utworzyć 1 000 000 bitmap, co może być bardzo nieefektywne.
Zatem My
Aby zastąpić bitmapę wynikającą z wysokiej kardynalności, możemy użyć struktury zwanej Roaring Bitmap. Roaring Bitmap ma różne cechy, ale jego największą podstawową filozofią i cechą jest dynamiczne wybieranie najbardziej odpowiedniego sposobu przechowywania w zależności od gęstości danych, maksymalizując zarówno współczynnik kompresji, jak i szybkość operacji.
Podstawowa Koncepcja Roaring Bitmap
Indeksowanie Dwustopniowe
Aby przechować pojedynczą liczbę całkowitą, Roaring Bitmap najpierw przechodzi przez następujące 2 etapy. Chociaż nazywa się to indeksowaniem dwustopniowym, można to traktować jako tablicę dwuwymiarową.
- Ekstrakcja górnych 16 bitów: Górne 16 bitów liczby całkowitej jest ekstrahowane i używane jako indeks. Staje się to indeksem zewnętrznej tablicy w tablicy dwuwymiarowej o długości 65536. Ta tablica przechowuje adresy kontenerów, o których mowa będzie później.
- Ekstrakcja dolnych 16 bitów: Dolne 16 bitów liczby całkowitej jest ekstrahowane i używane jako pozycja wewnątrz kontenera.
Kontener
Ten kontener odpowiada wewnętrznej tablicy w analogii do tablicy dwuwymiarowej. Jednakże, w przeciwieństwie do górnych 16 bitów, wewnętrzna struktura tego kontenera zmienia się w zależności od ilości zawartych w nim danych. Roaring Bitmap obsługuje następujące 3 typy kontenerów.
- Array Container: Ten kontener jest używany, gdy wewnątrz przechowywanych jest niewiele danych. Konkretnie, jest używany do przechowywania do 4096 elementów. Ten kontener jest zaimplementowany jako po prostu posortowana tablica liczb całkowitych. Elementy można znaleźć za pomocą wyszukiwania binarnego.
- Bitmap Container: Ten kontener jest używany, gdy wewnątrz przechowywanych jest dużo danych. Konkretnie, jest używany do przechowywania ponad 4096 elementów. Ten kontener jest zaimplementowany jako 65536-bitowa (8192 bajty) bitmapa. Każdy bit wskazuje na istnienie lub brak liczby całkowitej w danej pozycji.
- Run Container: Ten kontener jest używany do przechowywania ciągłych zakresów liczb całkowitych. Jest wydajny do przechowywania ciągłych liczb całkowitych, takich jak 1, 2, 3, 4, 5. Ten kontener jest zaimplementowany jako lista par (wartość początkowa, długość). Jeśli wszystkie są obecne? Oznacza to, że są obecne od początku do końca, więc wystarczy zapisać tylko jedną parę (0, 65536).
Takie podejście pozwala Roaring Bitmap na bardzo efektywne wykorzystanie pamięci i zapewnienie optymalnej wydajności dla różnych gęstości danych.
Funkcjonalność Roaring Bitmap
Podstawowe Funkcje
Roaring Bitmap zapewnia następujące podstawowe funkcje.
- Wstawianie: Dodaje nową liczbę całkowitą do bitmapy. W tym procesie można wybrać odpowiedni kontener lub, w razie potrzeby, przekształcić kontener.
- Usuwanie: Usuwa istniejącą liczbę całkowitą z bitmapy. Również w tym procesie następuje odpowiednie przetwarzanie w zależności od stanu kontenera.
- Wyszukiwanie: Sprawdza, czy określona liczba całkowita istnieje w bitmapie. Ten proces jest wykonywany bardzo szybko.
Operacje na Zbiorach
Roaring Bitmap obsługuje również następujące operacje na zbiorach.
- Suma (Union): Tworzy nową bitmapę zawierającą wszystkie elementy z obu bitmap.
- Przecięcie (Intersection): Tworzy nową bitmapę zawierającą tylko elementy obecne w obu bitmapach.
- Różnica Symetryczna (Symmetric Difference): Tworzy nową bitmapę zawierającą elementy obecne tylko w jednej z dwóch bitmap.
Operacje te są zoptymalizowane pod kątem typu kontenera bitmapy i wykonywane są bardzo szybko.
W Języku Go
W języku Go można łatwo używać Roaring Bitmap, korzystając z pakietu github.com/RoaringBitmap/roaring
. Ten pakiet udostępnia wszystkie funkcje Roaring Bitmap i jest zoptymalizowany pod kątem specyfiki języka Go.
Instalacja
1go get github.com/RoaringBitmap/roaring/v2
Przykład Użycia
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) // Inicjalizacja wartościami 1, 2, 3, 4
11 b2 := roaring.BitmapOf(2, 4, 6, 8, 12) // Inicjalizacja wartościami 2, 4, 6, 8, 12
12
13 b1.Add(5) // Dodanie 5
14 b2.Add(10) // Dodanie 10
15
16 b2.Remove(12) // Usunięcie 12
17
18 and := roaring.FastAnd(b1, b2)
19 or := roaring.FastOr(b1, b2)
20
21 // Operacje na bitmapach modyfikują oryginał, dlatego używamy klonu
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]
Główne funkcje, które nie zostały pokazane w powyższym przykładzie kodu, to:
Contains(x uint32) bool
: Sprawdza, czy określona wartość istnieje w bitmapieGetCardinality() uint64
: Zwraca liczbę elementów zawartych w bitmapieRank(x uint32) uint64
: Zwraca liczbę elementów mniejszych lub równych określonej wartościToBytes() ([]byte, error)
: Serializuje bitmapę do slice'a bajtówFromBytes(data []byte) error
: Przywraca bitmapę ze slice'a bajtówClone() *Bitmap
: Tworzy klon bitmapyClear()
: Inicjalizuje bitmapęNextValue(x uint32) int64
: Zwraca następny element większy lub równy xPreviousValue(x uint32) int64
: Zwraca następny element mniejszy lubMaximum() uint32
: Zwraca największy element w bitmapieMinimum() uint32
: Zwraca najmniejszy element w bitmapie
Takie funkcje są dostępne. Szczegółowe informacje można znaleźć w oficjalnej dokumentacji.
Należy zauważyć, że pakiet roaring zawiera pakiety obsługujące uint64, a nie tylko uint32. Zapewniają one niemal identyczny interfejs, co pozwala na bezpośrednią zamianę.
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) // Inicjalizacja wartościami 1, 2, 3, 4
11 b2 := roaring64.BitmapOf(2, 4, 6, 8, 12) // Inicjalizacja wartościami 2, 4, 6, 8, 12
12
13 b1.Add(5) // Dodanie 5
14 b2.Add(10) // Dodanie 10
15
16 b2.Remove(12) // Usunięcie 12
17
18 and := roaring64.FastAnd(b1, b2)
19 or := roaring64.FastOr(b1, b2)
20
21 // Operacje na bitmapach modyfikują oryginał, dlatego używamy klonu
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}
Podsumowanie
Roaring Bitmap jest potężnym narzędziem umożliwiającym efektywne przechowywanie i manipulowanie zbiorami liczb całkowitych o wysokiej kardynalności. W języku Go, korzystając z pakietu github.com/RoaringBitmap/roaring
, można łatwo wykorzystać wszystkie funkcje Roaring Bitmap. Pakiet ten maksymalizuje zarówno zużycie pamięci, jak i wydajność poprzez różne typy kontenerów i zoptymalizowane operacje. Roaring Bitmap może być wykorzystany do efektywnego przetwarzania danych w różnych dziedzinach, takich jak indeksowanie baz danych, analiza logów i obliczenia statystyczne.
Roaring Bitmap osiąga wysoką wydajność, zwłaszcza w przypadku dużych zbiorów danych, i może być użyteczny w różnych aplikacjach. W połączeniu ze zwięzłą i wydajną składnią języka Go, Roaring Bitmap zapewnia programistom potężne narzędzie. Oczekuje się, że wraz z rozwojem Roaring Bitmap, pojawi się więcej funkcji i optymalizacji.
Dlaczego ja to zrobiłem? Dla indeksowania poszczególnych tagów i szybkiej operacji sumy w czasowych bazach danych (Time Series Database).