
Parola di apertura
Ho presentato questa relazione in inglese alla conferenza GopherCon Russia 2019 a Mosca e in russo durante il meetup a Nizhny Novgorod. Si parla di bitmap index, meno comune del B-tree, ma altrettanto interessante. Condivido della mia presentazione alla conferenza in inglese e la trascrizione in russo.
Analizzeremo come è strutturato il bitmap index, quando è migliore, quando è peggiore rispetto ad altri indici e in quali casi è significativamente più veloce; vedremo in quali popolari DBMS sono già presenti bitmap index; cercheremo di scrivere il nostro in Go. E come "dessert", utilizzeremo librerie già pronte per creare un nostro database altamente specializzato e ultraveloce.
Spero davvero che i miei sforzi risultino utili e interessanti per voi. Cominciamo!
Introduzione

Ciao a tutti! Sono le sei di sera, siamo tutti super stanchi. È un momento meraviglioso per parlare di noiosa teoria degli indici nei database, vero? Non preoccupatevi, avrò un paio di righe di codice sorgente qui e là. 🙂
Senza scherzi, la presentazione è piena di informazioni e il nostro tempo è limitato. Iniziamo.

Oggi parlerò di:
- cosa sono gli indici;
- cosa è un bitmap index;
- dove viene utilizzato e dove NON viene utilizzato e perché;
- una semplice implementazione in Go e un po' di lotta con il compilatore;
- un'implementazione leggermente meno semplice, ma molto più performante in Go-assembly;
- "problemi" dei bitmap index;
- implementazioni esistenti.
Allora, cosa sono gli indici?

Un indice è una struttura dati separata che manteniamo e aggiorniamo in aggiunta ai dati principali. Viene utilizzato per accelerare le ricerche. Senza indici, la ricerca richiederebbe una scansione completa dei dati (un processo noto come full scan), e questo processo ha una complessità algoritmica lineare. Ma i database di solito contengono enormi quantità di dati e la complessità lineare è troppo lenta. Idealmente, vorremmo ottenere una complessità logaritmica o costante.
È un argomento vasto e complesso, ricco di sfumature e compromessi, ma, dopo aver osservato decenni di sviluppo e ricerca su vari database, sono pronto ad affermare che ci sono solo alcuni approcci ampiamente utilizzati per la creazione di indici nei database.

Il primo approccio consiste nella riduzione gerarchica dell'area di ricerca, dividendo l'area di ricerca in parti più piccole.
Di solito lo facciamo utilizzando vari tipi di alberi. Un esempio può essere una grande scatola con materiali nel tuo armadio, che contiene scatole più piccole con materiali divisi per vari argomenti. Se hai bisogno di materiali, sicuramente li cercherai nella scatola con la scritta "Materiali", e non in quella con scritto "Biscotti", giusto?

Il secondo approccio consiste nell'individuare immediatamente l'elemento necessario o il gruppo di elementi. Lo facciamo con le hash map o con gli indici inversi. L'uso delle hash map è molto simile all'esempio precedente, solo che invece di avere una scatola con scatole, nel tuo armadio hai un mucchio di piccole scatole con oggetti finali.

Il terzo approccio è quello di eliminare la necessità di cercare. Questo lo facciamo utilizzando i filtri di Bloom o i filtri cuckoo. I primi forniscono una risposta istantanea, liberandoti dalla necessità di effettuare una ricerca.

L'ultimo approccio consiste nell'utilizzare appieno tutte le potenzialità offerte dall'hardware moderno. Questo è esattamente ciò che facciamo negli indici bitmap. Sì, quando li usiamo a volte dobbiamo scorrere l'intero indice, ma lo facciamo in modo super efficiente.
Come ho già detto, il tema degli indici del database è vasto e pieno di compromessi. Questo significa che a volte possiamo usare più approcci contemporaneamente: se abbiamo bisogno di accelerare ulteriormente la ricerca o se è necessario coprire tutti i possibili tipi di ricerca.
Oggi parlerò dell'approccio meno conosciuto tra quelli menzionati: gli indici bitmap.
Chi sono io per parlare di questo argomento?

Lavoro come team leader in Badoo (forse conosci meglio un nostro altro prodotto: Bumble). Abbiamo già oltre 400 milioni di utenti in tutto il mondo e molte funzionalità che si occupano di trovare la migliore corrispondenza per loro. Lo facciamo attraverso servizi personalizzati, che utilizzano anche indici bitmap.
Allora, che cos'è un indice bitmap?

Gli indici bitmap, come suggerisce il nome, utilizzano bitmap o bitset per implementare un indice di ricerca. Da un'altezza di volo d'uccello, questo indice consiste in una o più di queste bitmap che rappresentano alcune entità (come le persone) e le loro proprietà o parametri (età, colore degli occhi, ecc.), e in un algoritmo che utilizza operazioni bit a bit (AND, OR, NOT) per rispondere a una query di ricerca.

Ci viene detto che gli indici bitmap sono i più adatti e molto efficienti nei casi in cui ci sia una ricerca che unisce richieste su più colonne con bassa cardinalità (immaginate "colore degli occhi" o "stato civile" rispetto a qualcosa come "distanza dal centro città"). Ma più tardi mostrerò che funzionano egregiamente anche con colonne ad alta cardinalità.
Consideriamo un esempio semplice di indice bitmap.

Immaginate di avere un elenco di ristoranti a Mosca con proprietà binarie come queste:
- vicino alla metropolitana (near metro);
- ha parcheggio privato (has private parking);
- ha veranda (has terrace);
- accetta prenotazioni (accepts reservations);
- vegan friendly (adatto ai vegetariani);
- costoso (expensive).

Assegniamo a ciascun ristorante un numero progressivo a partire da 0 e riserviamo spazio per 6 bitmap (una per ogni caratteristica). Successivamente, riempiremo queste bitmap a seconda che il ristorante abbia o meno la caratteristica in questione. Se il ristorante 4 ha una veranda, il bit n°4 nella bitmap "ha veranda" sarà impostato su 1 (se non c'è veranda, sarà 0).

Ora abbiamo l'indice bitmap più semplice possibile e possiamo utilizzarlo per rispondere a query come:
- "Mostrami i ristoranti adatti ai vegetariani";
- "Mostrami i ristoranti economici con veranda, dove posso prenotare un tavolo."


Come? Vediamo. La prima query è molto semplice. Tutto ciò di cui abbiamo bisogno è prendere la bitmap "adatto ai vegetariani" e trasformarla in un elenco di ristoranti i cui bit sono attivi.


La seconda query è un po' più complessa. Dobbiamo utilizzare l'operazione bit a bit NOT sulla bitmap "costosa" per ottenere un elenco di ristoranti economici, poi fare un AND con la bitmap "prenotabile" e fare un altro AND con la bitmap "con veranda". La bitmap risultante conterrà un elenco di locali che soddisfano tutti i nostri criteri. In questo esempio, si tratta solo del ristorante "Gioventù".


C'è molta teoria qui, ma non preoccupatevi, vedremo il codice molto presto.
Dove vengono utilizzati gli indici bitmap?

Se cercate su Google gli indici bitmap, il 90% delle risposte saranno in qualche modo collegate a Oracle DB. Ma anche gli altri DBMS sicuramente supportano questa caratteristica interessante, vero? Non proprio.
Facciamo un elenco dei principali sospettati.

MySQL non supporta ancora gli indici bitmap, ma c'è una proposta per aggiungere questa opzione ().
PostgreSQL non supporta gli indici bitmap, ma utilizza bitmap semplici e operazioni bit a bit per unire i risultati della ricerca su altri indici.
Tarantool ha indici bitset, supporta ricerche semplici su di essi.
Redis ha semplici campi bit) senza possibilità di ricerca su di essi.
MongoDB non supporta ancora gli indici bitmap, ma c'è anche una proposta per aggiungere questa opzione.
Elasticsearch utilizza bitmap all'interno).

- Ma nella nostra casa è arrivato un nuovo vicino: Pilosa. È un nuovo database non relazionale, scritto in Go. Contiene solo indici bitmap e basa tutto su di essi. Ne parleremo più tardi.
Implementazione in Go
Ma perché gli indici bitmap sono così raramente utilizzati? Prima di rispondere a questa domanda, vorrei mostrarvi l'implementazione di un semplice indice bitmap in Go.

Le bitmap, in sostanza, sono rappresentate semplicemente come pezzi di dati. In Go usiamo un array di byte per questo.
Abbiamo una bitmap per una caratteristica del ristorante, e ogni bit nella bitmap indica se un determinato ristorante ha o meno quella caratteristica.

Avremo bisogno di due funzioni ausiliarie. Una verrà utilizzata per riempire le nostre bitmap con dati casuali. Casuali, ma con una certa probabilità che il ristorante possieda ciascuna caratteristica. Ad esempio, ritengo che a Mosca ci siano pochissimi ristoranti in cui non sia possibile prenotare un tavolo, e mi sembra che circa il 20% dei locali sia adatto ai vegetariani.
La seconda funzione convertirà la bitmap in un elenco di ristoranti.


Per rispondere alla richiesta "Mostrami ristoranti economici con veranda in cui sia possibile prenotare un tavolo", avremo bisogno di due operazioni bitwise: NOT e AND.
Possiamo semplificare un po' il nostro codice, utilizzando un'operazione AND NOT più complessa.
Abbiamo funzioni per ciascuna di queste operazioni. Entrambe percorrono le slice, prendono gli elementi corrispondenti da ciascuna, li combinano con un'operazione bitwise e mettono il risultato in una slice di output.

Ed è così che possiamo utilizzare le nostre bitmap e le funzioni per rispondere alla richiesta di ricerca.

Le prestazioni non sono così elevate, anche se le funzioni sono molto semplici e abbiamo risparmiato parecchio non restituendo una nuova slice di output ogni volta che la funzione viene chiamata.
Profilando un po' con pprof, ho notato che il compilatore Go ha trascurato una ottimizzazione molto semplice ma fondamentale: l'inlining delle funzioni.

Il fatto è che il compilatore Go teme terribilmente i cicli che attraversano le slice e si rifiuta categoricamente di inlinare le funzioni che contengono tali cicli.

Ma io non ho paura e posso ingannare il compilatore, utilizzando goto invece di un ciclo, come ai vecchi tempi.


E, come puoi vedere, ora il compilatore inlinando la nostra funzione con piacere! Alla fine riusciamo a risparmiare circa 2 microsecondi. Non male!

Il secondo collo di bottiglia è facile da vedere se si guarda attentamente all'output assembly. Il compilatore ha aggiunto un controllo dei limiti della slice direttamente all'interno del nostro ciclo più caldo. Il punto è che Go è un linguaggio sicuro, il compilatore teme che i miei tre argomenti (tre slice) abbiano dimensioni diverse. Infatti, ci sarebbe una possibilità teorica di overflow del buffer.
Calmiamo il compilatore, mostrandogli che tutti i nostri slice hanno la stessa dimensione. Possiamo fare ciò aggiungendo un semplice controllo all'inizio della nostra funzione.

Vedendo questo, il compilatore felice salta il controllo, e noi risparmiamo ben 500 nanosecondi.
Batch grandi
Okay, siamo riusciti a estrarre una certa performance dalla nostra semplice implementazione, ma questo risultato è, di fatto, molto peggiore di quanto potrebbe essere con l'hardware attuale.
Tutto ciò che facciamo sono operazioni bitwise di base, e i nostri processori le eseguono in modo molto efficiente. Purtroppo, però, «nutriamo» il nostro processore con piccoli pezzi di lavoro. Le nostre funzioni eseguono operazioni byte per byte. Possiamo facilmente ottimizzare il nostro codice affinché lavori con pezzi di 8 byte, usando slice di UInt64.

Come potete vedere, questo piccolo cambiamento ha accelerato il nostro programma di otto volte grazie all'aumento del batch di otto volte. Il guadagno, si può dire, è lineare.

Implementazione in assembly

Ma non è finita qui. I nostri processori possono lavorare con pezzi di 16, 32 e persino 64 byte. Queste «operazioni ampie» sono chiamate single instruction multiple data (SIMD; una istruzione, molti dati), e il processo di trasformazione del codice in modo che utilizzi tali operazioni è denominato vettorizzazione.
Sfortunatamente, il compilatore Go non è un campione di vettorizzazione. Attualmente, l'unico modo per vettorizzare il codice in Go è di prendere e scrivere manualmente le operazioni dei dati utilizzando l'assembly Go.

L'assembly Go è una bestia strana. Sapete sicuramente che l'assembly è qualcosa di fortemente legato all'architettura del computer per cui scrivete, ma in Go non è così. L'assembly Go è più simile a un IRL (intermediate representation language) o linguaggio di rappresentazione intermedia: è praticamente indipendente dalla piattaforma. Rob Pike ha tenuto un eccellente su questo argomento qualche anno fa al GopherCon a Denver.
In aggiunta a ciò, Go utilizza un formato insolito chiamato Plan 9, che si differenzia dai formati standardizzati AT&T e Intel.

Si può affermare con certezza che scrivere manualmente in assembly Go non è un compito molto divertente.
Ma, fortunatamente, ci sono già due strumenti di alto livello che ci aiutano a scrivere assembly Go: PeachPy e avo. Entruti gli strumenti generano assembly Go da un codice di livello superiore, scritto rispettivamente in Python e Go.

Questi strumenti semplificano cose come l'allocazione dei registri (selezione di un registro del processore), la scrittura di cicli e, in generale, semplificano il processo di ingresso nel mondo della programmazione in assembly in Go.
Utilizzeremo avo, quindi i nostri programmi saranno quasi normali programmi Go.

Ecco come appare l'esempio più semplice di un programma avo. Abbiamo una funzione main() che definisce al suo interno la funzione Add(), il cui scopo è sommare due numeri. Qui sono presenti funzioni ausiliarie per ottenere i parametri per nome e per ottenere uno dei registri del processore liberi e adatti. Ogni operazione del processore ha una corrispondente funzione in avo, come si può vedere in ADDQ. Infine, vediamo una funzione ausiliaria per memorizzare il valore risultante.

Eseguendo go generate, eseguiremo il programma in avo e alla fine verranno generati due file:
- add.s con il codice risultante in assembly Go;
- stub.go con le intestazioni delle funzioni per collegare i due mondi: Go e assembly.

Ora che abbiamo visto cosa e come fa avo, diamo un'occhiata alle nostre funzioni. Ho implementato sia le versioni scalari che quelle vettoriali (SIMD) delle funzioni.
Iniziamo a guardare le versioni scalari.

Come nell'esempio precedente, chiediamo di fornirci un registro di uso generale libero e corretto, non dobbiamo calcolare offset e dimensioni per gli argomenti. Tutto questo è gestito da avo per noi.

In precedenza, utilizzavamo etichette e goto (o salti) per migliorare le prestazioni e per ingannare il compilatore Go, ma adesso lo facciamo fin dall'inizio. Il fatto è che i cicli sono un concetto di livello superiore. In assembly, abbiamo solo etichette e salti.

Il codice rimanente dovrebbe essere già familiare e chiaro. Emuliamo un ciclo con etichette e salti, prendiamo una piccola parte di dati da entrambe le nostre fette, li uniamo tramite un'operazione bitwise (AND NOT in questo caso) e poi mettiamo il risultato nella fetta risultante. Tutto qui.

Ecco come appare il codice finale in assembly. Non abbiamo dovuto calcolare offset e dimensioni (evidenziati in verde) o tenere traccia dei registri utilizzati (evidenziati in rosso).

Se confrontiamo le prestazioni dell'implementazione in assembly con quelle della migliore implementazione in Go, vediamo che sono equivalenti. E questo è prevedibile. Non abbiamo fatto nulla di speciale: abbiamo semplicemente riprodotto ciò che farebbe il compilatore Go.
Sfortunatamente, non possiamo costringere il compilatore ad applicare inline le nostre funzioni scritte in assembly. Al momento, il compilatore Go non ha questa possibilità, anche se la richiesta di aggiungerla esiste già da tempo.
Proprio per questo motivo è impossibile ottenere vantaggi da piccole funzioni in assembly. Dobbiamo scrivere funzioni più grandi, oppure utilizzare il nuovo pacchetto math/bits, oppure evitare totalmente l'assembly.
Ora diamo un'occhiata alle versioni vettoriali delle nostre funzioni.

Per questo esempio ho deciso di utilizzare AVX2, quindi useremo operazioni che lavorano con blocchi da 32 byte. La struttura del codice è molto simile a quella della versione scalare: caricamento dei parametri, richiesta di un registro generale libero e così via.

Una delle novità riguarda il fatto che le operazioni vettoriali più ampie utilizzano registri speciali e più ampi. Nel caso di blocchi da 32 byte, si tratta di registri con il prefisso Y. Ecco perché vedi la funzione YMM() nel codice. Se avessi usato AVX-512 con blocchi da 64 bit, il prefisso sarebbe stato Z.
La seconda novità riguarda l'ottimizzazione che ho deciso di utilizzare, chiamata espansione del ciclo (loop unrolling), ovvero eseguire otto operazioni del ciclo manualmente prima di tornare all'inizio del ciclo. Questa ottimizzazione riduce il numero di branch nel codice e è limitata dal numero di registri liberi disponibili.

E riguardo alle prestazioni? Sono fantastiche! Abbiamo ottenuto un'accelerazione di circa sette volte rispetto alla migliore soluzione in Go. Impressionante, vero?

Ma anche questa implementazione potrebbe essere potenzialmente accelerata utilizzando AVX-512, prefetching o JIT (just-in-time compiler) per il pianificatore delle query. Ma questa è sicuramente materia per una presentazione a parte.
Problemi degli indici bitmap
Ora che abbiamo esaminato una semplice implementazione dell'indice bitmap in Go e una molto più performante in assembly, parliamo finalmente del motivo per cui gli indici bitmap sono così raramente utilizzati.

Nei vecchi studi scientifici si menzionano tre problemi degli indici bitmap, ma studi più recenti e io stesso affermiamo che non sono più attuali. Non approfondiamo ciascuno di questi problemi, ma li consideriamo superficialmente.
Il problema dell'alta cardinalità
Dunque, ci viene detto che gli indici bitmap sono adatti solo per campi con bassa cardinalità, cioè quelli che hanno pochi valori (ad esempio, sesso o colore degli occhi), e la ragione è che la rappresentazione comune di tali campi (un bit per valore) nel caso di alta cardinalità occuperebbe troppo spazio e, inoltre, questi indici bitmap sarebbero scarsamente (raramente) popolati.


A volte possiamo utilizzare un'altra rappresentazione, ad esempio quella standard che usiamo per rappresentare i numeri. Ma è stata l'emergere di algoritmi di compressione a cambiare tutto. Negli ultimi decenni, scienziati e ricercatori hanno ideato un gran numero di algoritmi di compressione per bitmap. Il loro principale vantaggio è che non è necessario decomprimere i bitmap per eseguire operazioni bit a bit: possiamo effettuare operazioni bit a bit direttamente sui bitmap compressi.

Ultimamente sono emersi anche approcci ibridi, come i bitmap roaring. Essi utilizzano contemporaneamente tre rappresentazioni diverse per i bitmap: i bitmap stessi, gli array e le cosiddette sequenze di bit — bilanciando tra di esse per massimizzare le prestazioni e minimizzare il consumo di memoria.
Puoi incontrare bitmap roaring nelle applicazioni più popolari. Già esistono un'enorme quantità di implementazioni per i più svariati linguaggi di programmazione, incluse oltre tre implementazioni per Go.

Un altro approccio che può aiutarci a gestire un'alta cardinalità è chiamato raggruppamento (binning). Immagina di avere un campo che rappresenta l'altezza di una persona. L'altezza è un numero con virgola mobile, ma noi umani non la consideriamo in questo modo. Non c'è differenza per noi tra un'altezza di 185,2 cm e una di 185,3 cm.
In effetti, possiamo raggruppare valori simili in gruppi all'interno di 1 cm.
E se sappiamo anche che ci sono molto poche persone che hanno un'altezza inferiore a 50 cm e superiore a 250 cm, allora possiamo, in sostanza, trasformare un campo con una cardinalità infinita in un campo con una cardinalità di circa 200 valori.
Certo, se necessario, possiamo effettuare un'ulteriore filtrazione anche in seguito.
Problema di grande capacità di elaborazione
Il problema degli indici bitmap è che il loro aggiornamento può essere molto costoso.
I database devono consentire l'aggiornamento dei dati nel momento in cui centinaia di altre query stanno cercando in questi dati. Abbiamo bisogno di lock per evitare problemi di accesso concorrente ai dati o altri problemi di accesso condiviso. Dove c'è un grande lock, c'è un problema: la contention da lock, quando quel lock diventa un collo di bottiglia.

Questo problema può essere risolto o aggirato tramite lo sharding o l'uso di indici versionati.
Lo sharding è una cosa semplice e ben nota. Puoi shardare un indice bitmap proprio come shardaresti qualsiasi altro dato. Invece di un grande lock avrai molti piccoli lock e così facendo eliminerai la contention da lock.
Un secondo modo per risolvere il problema è utilizzare indici versionati. Puoi avere una copia dell'indice che utilizzi per cercare o leggere, e una per scrivere o aggiornare. E ogni tot tempo (ad esempio, ogni 100 ms o 500 ms) le duplicati e scambi le loro posizioni. Naturalmente, questo approccio è applicabile solo nei casi in cui la tua applicazione può lavorare con un indice di ricerca leggermente in ritardo.
Questi due approcci possono essere utilizzati simultaneamente: puoi avere un indice versionato shardato.
Query più complesse
L'ultimo problema degli indici bitmap è che, come ci dicono, non si adattano bene a tipi di query più complessi, ad esempio le query "per intervallo".
E in effetti, se ci pensi, le operazioni bitwise come AND, OR, ecc. non si adattano molto bene a richieste come "Mostrami gli hotel con prezzi delle stanze da 200 a 300 dollari a notte".

Una soluzione ingenua e molto poco saggia sarebbe prendere i risultati per ogni valore in dollari e combinarli usando l'operazione OR.

Una soluzione leggermente più corretta sarebbe utilizzare il raggruppamento. Ad esempio, in gruppi da 50 dollari. Questo accelererebbe il nostro processo di 50 volte.
Ma il problema si risolve facilmente utilizzando una rappresentazione creata appositamente per questo tipo di richieste. Nei lavori scientifici viene chiamata bitmap codificata per intervallo.

In tale rappresentazione non impostiamo semplicemente un bit per un valore specifico (ad esempio, 200), ma impostiamo quel valore e tutto ciò che è superiore. 200 e oltre. Lo stesso vale per 300: 300 e oltre. E così via.
Utilizzando questa rappresentazione, possiamo rispondere a questo tipo di ricerca passando attraverso l'indice solo due volte. Prima otteniamo un elenco di hotel in cui il costo della camera è inferiore a 300 dollari, e poi eliminiamo quelli in cui il costo della camera è inferiore a 199 dollari. Fatto.

Rimarrete sorpresi, ma anche le richieste geografiche sono possibili con l'uso di indici bitmap. Il trucco consiste nell'utilizzare una rappresentazione geografica che circonda le vostre coordinate con una figura geometrica. Ad esempio, S2 di Google. La figura deve poter essere rappresentata da tre o più linee intersecanti, che possono essere numerate. In questo modo possiamo trasformare la nostra richiesta geospaziale in più richieste "per intervallo" (su queste linee numerate).
Soluzioni pronte
Spero di aver suscitato un po' il vostro interesse e che ora abbiate un altro strumento utile nel vostro arsenale. Se mai avrete bisogno di fare qualcosa di simile, saprete in quale direzione guardare.
Tuttavia, non tutti hanno il tempo, la pazienza e le risorse per creare indici bitmap da zero. Soprattutto quelli più avanzati, con l'uso di SIMD, ad esempio.
Fortunatamente, ci sono alcune soluzioni pronte che possono aiutarvi.

Bitmap Roaring
Per prima cosa, c'è la biblioteca di bitmap roaring di cui ho già parlato. Contiene tutti i contenitori e le operazioni bit necessarie per creare un indice bitmap completo.

Sfortunatamente, al momento nessuna delle implementazioni in Go utilizza SIMD, il che significa che le implementazioni in Go sono meno performanti rispetto a quelle in C, ad esempio.
Pilosa
Un altro prodotto che può aiutarvi è il database Pilosa, il quale è essenzialmente basato esclusivamente su indici bitmap. È una soluzione relativamente nuova, ma sta conquistando consensi a una velocità incredibile.

Pilosa utilizza bitmap roaring al suo interno e ti consente di usarli, semplificando e spiegando tutte quelle cose di cui ho parlato sopra: raggruppamento, bitmap codificati per intervallo, concetto di campo, ecc.
Diamo un'occhiata veloce a un esempio di utilizzo di Pilosa per rispondere a una domanda che già conosci.

L'esempio è molto simile a quello che hai visto prima. Creiamo un client per il server di Pilosa, creiamo un indice e i campi necessari, poi riempiamo i nostri campi con dati casuali con probabilità e, infine, eseguiamo la richiesta familiare.
Dopo di che utilizziamo NOT sul campo 'costoso', quindi intersechiamo il risultato (o AND) con il campo 'terrazza' e con il campo 'prenotazioni'. E infine, otteniamo il risultato finale.

Spero davvero che nel prossimo futuro anche nei database come MySQL e PostgreSQL ci sarà questo nuovo tipo di indici — gli indici bitmap.

Conclusione

Se non ti sei ancora addormentato, grazie. Ho dovuto toccare molte tematiche a volo a causa del tempo limitato, ma spero che la presentazione sia stata utile e, forse, anche motivante.
È bene sapere degli indici bitmap, anche se al momento non ne hai bisogno. Siano un altro strumento nella tua cassetta degli attrezzi.
Abbiamo esaminato vari trucchi per aumentare le prestazioni di Go e quelle cose con cui il compilatore Go non gestisce molto bene. Questo è assolutamente utile da sapere per ogni programmatore Go.
Questo è tutto ciò che volevo raccontarti. Grazie!
Fonte: habr.com
