Le funzionalità del linguaggio Q e di KDB+ attraverso un servizio in tempo reale

Per scoprire cosa sia KDB+, il linguaggio di programmazione Q, quali siano i loro punti di forza e di debolezza, puoi leggere nel mio precedente articolo e brevemente nell'introduzione. In questo articolo implementeremo un servizio in Q che elaborerà un flusso di dati in ingresso e calcolerà al minuto varie funzioni aggregative in tempo reale (ovvero, calcolerà tutto prima della prossima porzione di dati). La principale peculiarità di Q è che è un linguaggio vettoriale, che consente di operare non su singoli oggetti, ma su array, array di array e altri oggetti complessi. Linguaggi come Q e i suoi affini K, J, APL sono famosi per la loro brevità. Spesso un programma che occupa diversi schermi di codice in un linguaggio convenzionale come Java può essere scritto in poche righe su di essi. Questo è esattamente ciò che desidero dimostrare in questo articolo.

Le funzionalità del linguaggio Q e di KDB+ attraverso un servizio in tempo reale

Introduzione

KDB+ è un database a colonne progettato per gestire volumi molto grandi di dati, organizzati in un certo modo (principalmente in base al tempo). È utilizzato principalmente nelle istituzioni finanziarie: banche, fondi di investimento, compagnie di assicurazione. Il linguaggio Q è il linguaggio interno di KDB+, che permette di lavorare in modo efficiente con questi dati. L'ideologia di Q è brevità ed efficienza, sacrificando la chiarezza. Questo è giustificato dal fatto che un linguaggio vettoriale sarà comunque difficile da comprendere e la concisione e la ricchezza della sintassi consentono di visualizzare su un unico schermo una porzione molto più ampia del programma, rendendo così più facile la sua comprensione.

In questo articolo, realizzeremo un programma completo in Q e potreste volerlo provare di persona. Per farlo, avrete bisogno di Q. Potete scaricare la versione gratuita a 32 bit dal sito dell'azienda kx – www.kx.com. Qui troverete anche, se vi interessa, informazioni di riferimento su Q, il libro Q For Mortals e vari articoli su questo argomento.

Definizione del compito

Esiste una fonte che invia ogni 25 millisecondi una tabella con dati. Poiché KDB+ è utilizzato principalmente in ambito finanziario, consideriamo che si tratti di una tabella di scambi (trades), contenente le seguenti colonne: time (tempo in millisecondi), sym (simbolo dell'azienda in borsa – IBM, AAPL,…), price (prezzo al quale sono state acquistate le azioni), size (dimensione dell'operazione). L'intervallo di 25 millisecondi è scelto arbitrariamente, non è troppo piccolo né troppo grande. La sua presenza indica che i dati arrivano al servizio già bufferizzati. Si potrebbe facilmente implementare un buffering a livello di servizio, compreso quello dinamico, a seconda del carico attuale, ma per semplicità ci fermiamo a un intervallo fisso.

Il servizio deve calcolare ogni minuto per ogni simbolo in ingresso dalla colonna sym un insieme di funzioni aggregate: max price, avg price, sum size e altre informazioni utili. Per semplicità, supponiamo che tutte le funzioni possano essere calcolate in modo incrementale, ovvero per ottenere un nuovo valore è sufficiente conoscere due numeri: il valore precedente e il valore in ingresso. Ad esempio, le funzioni max, average e sum possiedono questa proprietà, mentre la funzione mediana no.

Supponiamo anche che il flusso di dati in entrata sia ordinato nel tempo. Questo ci permetterà di lavorare solo con l'ultima minuto. In pratica, è sufficiente saper gestire il minuto attuale e quello precedente nel caso in cui alcuni aggiornamenti siano in ritardo. Per semplificare, non prenderemo in considerazione questo caso.

Funzioni aggregative

Di seguito sono elencate le funzioni aggregative necessarie. Ne ho elencate quante più possibile per aumentare il carico sul servizio:

  • high – max price – prezzo massimo al minuto.
  • low – min price – prezzo minimo al minuto.
  • firstPrice – first price – primo prezzo al minuto.
  • lastPrice – last price – ultimo prezzo al minuto.
  • firstSize – first size – prima dimensione dell'operazione al minuto.
  • lastSize – last size – ultima dimensione dell'operazione al minuto.
  • numTrades – count i – numero di operazioni al minuto.
  • volume – sum size – somma delle dimensioni delle operazioni al minuto.
  • pvolume – sum price – somma dei prezzi al minuto, necessaria per avgPrice.
  • turnover – sum price*size – volume totale delle operazioni al minuto.
  • avgPrice – pvolume%numTrades – prezzo medio al minuto.
  • avgSize – volume%numTrades – dimensione media dell'operazione al minuto.
  • vwap – turnover%volume – prezzo medio ponderato per dimensione dell'operazione al minuto.
  • cumVolume – volume totale – somma del volume delle transazioni nel tempo.

Prima di tutto, discutiamo un aspetto non ovvio: come inizializzare queste colonne per la prima volta e per ogni minuto successivo. Alcune colonne, come firstPrice, devono essere inizializzate ogni volta con un valore nullo, poiché il loro valore non è definito. Altre colonne, come volume, devono sempre essere impostate a 0. Ci sono anche colonne che richiedono un approccio combinato: ad esempio, cumVolume deve essere copiato dal minuto precedente, mentre per il primo minuto deve essere impostato a 0. Definiamo tutti questi parametri utilizzando il tipo di dati dictionary (simile a una registrazione):

// list ! list – создать словарь, 0n – float null, 0N – long null, `sym – тип символ, `sym1`sym2 – список символов
initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time; // список всех вычисляемых колонок, reverse объяснен ниже

Ho aggiunto sym e time al dizionario per comodità, ora initWith è una riga pronta dalla tabella aggregata finale, dove restano da impostare gli appropriati sym e time. Può essere utilizzata per aggiungere nuove righe alla tabella.

aggCols ci sarà utile quando creeremo la funzione di aggregazione. La lista deve essere invertita a causa delle peculiarità dell'ordine di valutazione delle espressioni in Q (da destra a sinistra). L'obiettivo è garantire il calcolo dalla colonna high a cumVolume, poiché alcune colonne dipendono da quelle precedenti.

Colonne da copiare nella nuova minuto dalla precedente, la colonna sym è stata aggiunta per comodità:

rollColumns:`sym`cumVolume;

Ora divideremo le colonne in gruppi in base a come devono essere aggiornate. Possiamo identificare tre tipi:

  1. Accumulatori (volume, turnover, ...) – dobbiamo aggiungere il valore in ingresso a quello precedente.
  2. Con un punto speciale (high, low, ...) – il primo valore nel minuto proviene dai dati in ingresso, gli altri vengono calcolati tramite funzione.
  3. Altri. Vengono sempre calcolati tramite funzione.

Definiamo le variabili per queste classi:

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

Ordine dei calcoli

Aggiorneremo la tabella aggregata in due fasi. Per efficienza, ridurremo prima la tabella in ingresso in modo da avere una sola riga per ogni simbolo e minuto. Il fatto che tutte le nostre funzioni siano incrementali e associative ci garantisce che il risultato di questo passaggio aggiuntivo non cambierà. Si potrebbe ridurre la tabella utilizzando una selezione:

select high:max price, low:min price … by sym,time.minute from table

Questa modalità presenta un inconveniente: il set di colonne calcolate è definito in anticipo. Fortunatamente, in Q il selettore è implementato anche come funzione, a cui possono essere forniti argomenti creati dinamicamente:

?[table;whereClause;byClause;selectClause]

Non entrerò nei dettagli sul formato degli argomenti, in questo caso gli unici espressioni non banali saranno quelle di by e select, e devono essere dizionari della forma columns!expressions. Così, possiamo definire la funzione compressore in questo modo:

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size"); // each è la funzione map in Q per una lista
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

Per chiarezza, ho usato la funzione parse, che trasforma la stringa con l'espressione Q in un valore che può essere passato alla funzione eval e che è richiesto nel selettore funzionale. Notiamo anche che preprocess è definita come una proiezione (cioè una funzione con argomenti parzialmente definiti) della funzione select, un argomento (tabella) è mancante. Se applichiamo preprocess alla tabella, otteniamo una tabella compressa.

Il secondo passo consiste nell'aggiornamento della tabella aggregata. Scriviamo prima l'algoritmo in pseudocodice:

per ogni sym in inputTable
  idx: indice della riga nella tabella aggregata per sym+currentTime;
  aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
  aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
  …

In Q, invece dei cicli si preferisce usare le funzioni map/reduce. Ma dal momento che Q è un linguaggio vettoriale e possiamo applicare tutte le operazioni a tutti i simboli contemporaneamente, inizialmente possiamo fare a meno del ciclo, effettuando operazioni su tutti i simboli contemporaneamente:

idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…

Ma possiamo andare oltre, in Q esiste un operatore unico e incredibilmente potente: l'operatore di assegnazione generalizzata. Questo consente di modificare un insieme di valori all'interno di una struttura dati complessa utilizzando un elenco di indici, funzioni e argomenti. Nel nostro caso si presenta così:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], in questo caso la funzione è l'assegnazione
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

Sfortunatamente, per l'assegnazione in tabella è necessario un elenco di righe, non di colonne, e bisogna trasporre la matrice (trasformando l'elenco di colonne in un elenco di righe) utilizzando la funzione flip. Per una tabella grande, questo è costoso in termini di risorse, quindi applicheremo invece un'assegnazione generalizzata a ciascuna colonna separatamente, utilizzando la funzione map (che appare come un apostrofo):

[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high; row[`volume] + inputTable`volume;…)];

Usiamo nuovamente la proiezione della funzione. Notate anche che in Q la creazione di un elenco è anch'essa una funzione e possiamo chiamarla usando la funzione each(map) per ottenere un elenco di elenchi.

Per evitare che l'insieme di colonne calcolate sia fisso, creeremo l'espressione sopra dinamicamente. Prima definiamo le funzioni per calcolare ogni colonna, utilizzando le variabili row e inp per fare riferimento ai dati aggregati e ai dati di input:

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!
 ("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");

Alcune colonne sono speciali, il loro primo valore non deve essere calcolato dalla funzione. Possiamo determinare che è il primo in base alla colonna row[`numTrades] – se contiene 0, allora il valore è il primo. In Q esiste una funzione di selezione — ?[Boolean list;list1;list2] – che sceglie un valore dalla lista 1 o 2 a seconda della condizione del primo argomento:

// high -> ?[isFirst;inp`high;row[`high]|inp`high]
// @ - тоже обобщенное присваивание для случая когда индекс неглубокий
@[`aggExpression;specialCols;{[x;y]"?[isFirst;inp`",y,";",x,"]"};string specialCols];

Qui ho chiamato un'assegnazione generica con la mia funzione (un'espressione tra parentesi graffe). A essa viene passato il valore corrente (primo argomento) e un argomento aggiuntivo che fornisco nel quarto parametro.

Aggiungiamo separatamente le colonne accumulative, poiché per esse la funzione è la stessa:

// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;

Questa è un'assegnazione normale secondo i criteri di Q, solo che assegno subito una lista di valori. Infine, creiamo la funzione principale:

// ":",/:aggExprs ~ map[{":",x};aggExpr] => ":row[`high]|inp`high" присвоим вычисленное значение переменной, потому что некоторые колонки зависят от уже вычисленных значений
// string[cols],'exprs ~ map[,;string[cols];exprs] => "high:row[`high]|inp`high" завершим создание присваивания. ,’ расшифровывается как map[concat]
// ";" sv exprs – String from Vector (sv), соединяет список строк вставляя “;” посредине
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}";

Con questa espressione creo dinamicamente una funzione da una stringa che contiene l'espressione precedentemente menzionata. Il risultato apparirà così:

{[aggTable;idx;inp] rows:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols ;(cumVolume:row[`cumVolume]+inp`cumVolume;… ; high:?[isFirst;inp`high;row[`high]|inp`high])]}

L'ordine di calcolo delle colonne è invertito, poiché in Q l'ordine di calcolo va da destra a sinistra.

Ora abbiamo due funzioni principali necessarie per i calcoli, resta solo da aggiungere un po' di infrastruttura e il servizio è pronto.

Passaggi finali

Abbiamo le funzioni preprocess e updateAgg che svolgono tutto il lavoro. Ma è necessario garantire il corretto passaggio attraverso i minuti e calcolare gli indici per l'aggregazione. In primo luogo, definiamo la funzione init:

init:{
  tradeAgg:: 0#enlist[initWith]; // creiamo una tabella tipizzata vuota, enlist trasforma un dizionario in una tabella, e 0# significa prendere 0 elementi da essa
  currTime::00:00; // iniziamo con 0, :: indica che l'assegnazione è a una variabile globale
  currSyms::`u#`symbol$(); // `u# - trasforma l'elenco in un albero, per velocizzare la ricerca degli elementi
  offset::0; // indice in tradeAgg, dove inizia il minuto attuale
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // cache per i valori più recenti delle colonne roll, tabella con chiave sym
 }

Definiamo anche la funzione roll, che cambierà il minuto attuale:

roll:{[tm]
  if[currTime>tm; :init[]]; // se siamo oltre la mezzanotte, chiamiamo semplicemente init
  rollCache,::offset _ rollColumns#tradeAgg; // aggiorniamo la cache – prendiamo le colonne roll da aggTable, tagliamo, inseriamo in rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

Avremo bisogno di una funzione per aggiungere nuovi simboli:

addSyms:{[syms]
 currSyms,::syms; // aggiungiamo alla lista conosciuta
 // aggiungiamo alla tabella sym, time e rollColumns sfruttando l'assegnamento generico.
 // La funzione ^ inserisce valori predefiniti per le colonne roll, se il simbolo non è nella cache. value flip table restituisce la lista delle colonne nella tabella.
 `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 }

E, infine, la funzione upd (nome tradizionale di questa funzione per i servizi Q), che viene chiamata dal cliente per aggiungere dati:

upd:{[tblName;data] // tblName non ci serve, ma solitamente il servizio gestisce più tabelle 
 tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
 updMinute[data] each tm; // aggiungiamo dati per ogni minuto
};
updMinute:{[data;tm]
 if[tmcurrTime; roll tm; currTime::tm]; // cambiamo minuto se necessario
 data:select from data where time=tm; // filtraggio
 if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // nuovi simboli
 updateAgg[`tradeAgg;offset+currSyms?syms;data]; // aggiorniamo la tabella aggregata. La funzione ? cerca l'indice degli elementi della lista a destra nella lista a sinistra.
 };

Ecco fatto. Ecco il codice completo del nostro servizio, come promesso, solo poche righe:

initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time;
rollColumns:`sym`cumVolume;

accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size");
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");
@[`aggExpression;specialCols;{"?[isFirst;inp`",y,";",x,"]"};string specialCols];
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}"; / '

init:{
  tradeAgg::0#enlist[initWith];
  currTime::00:00;
  currSyms::`u#`symbol$();
  offset::0;
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg;
 };
roll:{[tm]
  if[currTime>tm; :init[]];
  rollCache,::offset _ rollColumns#tradeAgg;
  offset::count tradeAgg;
  currSyms::`u#`$();
 };
addSyms:{[syms]
  currSyms,::syms;
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime),(initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 };

upd:{[tblName;data] updMinute[data] each exec distinct time from data:() xkey preprocess data};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm];
  data:select from data where time=tm;
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms];
  updateAgg[`tradeAgg;offset+currSyms?syms;data];
 };

Test

Verifichiamo le prestazioni del servizio. Per farlo, eseguiamolo in un processo separato (inserisci il codice nel file service.q) e chiama la funzione init:

q service.q –p 5566

q)init[]

In un'altra console avvia il secondo processo Q e connettiti al primo:

h:hopen `:host:5566
h:hopen 5566 // se entrambi si trovano sullo stesso host

Iniziamo creando una lista di simboli – 10.000 pezzi e aggiungiamo una funzione per creare una tabella casuale. Nella seconda console:

syms:`IBM`AAPL`GOOG,-9997?`8
rnd:{[n;t] ([] sym:n?syms; time:t+asc n#til 25; price:n?10f; size:n?10)}

Ho aggiunto alla lista di simboli tre reali, per facilitare la loro ricerca nella tabella. La funzione rnd crea una tabella casuale con n righe, dove il tempo varia da t a t+25 millisecondi.

Ora possiamo provare a inviare dati al servizio (aggiungiamo le prime dieci ore):

{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10

Puoi controllare nel servizio che la tabella è stata aggiornata:

c 25 200
select from tradeAgg where sym=`AAPL
-20#select from tradeAgg where sym=`AAPL

Risultato:

sym|time|high|low|firstPrice|lastPrice|firstSize|lastSize|numTrades|volume|pvolume|turnover|avgPrice|avgSize|vwap|cumVolume
--|--|--|--|--|--------------------------------
AAPL|09:27|9.258904|9.258904|9.258904|9.258904|8|8|1|8|9.258904|74.07123|9.258904|8|9.258904|2888
AAPL|09:28|9.068162|9.068162|9.068162|9.068162|7|7|1|7|9.068162|63.47713|9.068162|7|9.068162|2895
AAPL|09:31|4.680449|0.2011121|1.620827|0.2011121|1|5|4|14|9.569556|36.84342|2.392389|3.5|2.631673|2909
AAPL|09:33|2.812535|2.812535|2.812535|2.812535|6|6|1|6|2.812535|16.87521|2.812535|6|2.812535|2915
AAPL|09:34|5.099025|5.099025|5.099025|5.099025|4|4|1|4|5.099025|20.3961|5.099025|4|5.099025|2919

Procediamo ora con il test di carico per determinare quanti dati il servizio può elaborare in un minuto. Ricordo che abbiamo impostato un intervallo per gli aggiornamenti di 25 millisecondi. Pertanto, il servizio dovrebbe (in media) rimanere almeno sotto i 20 millisecondi per gli aggiornamenti, in modo da dare tempo agli utenti di richiedere dati. Inserisci quanto segue nel secondo processo:

tm:10:00:00.000
stressTest:{[n] 1 string[tm]," "; times,::h ({st:.z.T; upd[`trade;x]; .z.T-st};rnd[n;tm]); tm+:25}
start:{[n] times::(); do[4800;stressTest[n]]; -1 " "; `min`avg`med`max!(min times;avg times;med times;max times)}

4800 – sono due minuti. Posso provare a far partire prima 1000 righe ogni 25 millisecondi:

start 1000

Nel mio caso, il risultato è di circa un paio di millisecondi per aggiornamento. Quindi, aumento subito il numero di righe a 10.000:

start 10000

Risultato:

min| 00:00:00.004
avg| 9.191458
med| 9f
max| 00:00:00.030

Niente di speciale, eppure si tratta di 24 milioni di righe al minuto, 400 mila al secondo. Solo 5 ritardi oltre 25 millisecondi, evidentemente durante il cambio di minuto. Aumentiamo a 100.000:

start 100000

Risultato:

min| 00:00:00.013
avg| 25.11083
med| 24f
max| 00:00:00.108
q)sum times
00:02:00.532

Come vediamo, il servizio riesce a malapena a farcela, ma riesce comunque a rimanere a galla. Un tale volume di dati (240 milioni di righe al minuto) è estremamente elevato, in questi casi è consuetudine lanciare più cloni (o addirittura decine di cloni) del servizio, ognuno dei quali gestisce solo una parte dei caratteri. Tuttavia, il risultato è impressionante per un linguaggio interpretato, che è principalmente orientato allo stoccaggio dei dati.

Potrebbe sorgere la domanda sul perché il tempo cresca in modo non lineare con l'aumento delle dimensioni di ogni aggiornamento. La ragione è che la funzione di compressione è di fatto una funzione C, che opera molto più efficientemente di updateAgg. A partire da una certa dimensione dell'aggiornamento (intorno ai 10.000), updateAgg raggiunge il suo limite e il suo tempo di esecuzione non dipende più dalla dimensione dell'aggiornamento. È proprio grazie a un passaggio preliminare Q che il servizio è in grado di elaborare tali volumi di dati. Ciò sottolinea quanto sia importante, quando si lavora con grandi dati, scegliere l'algoritmo giusto. Un altro aspetto è la corretta memorizzazione dei dati in memoria. Se i dati non fossero memorizzati in modo colonnare o non fossero ordinati nel tempo, ci troveremmo di fronte a un fenomeno noto come TLB cache miss—ossia l'assenza di un indirizzo di pagina in memoria nella cache degli indirizzi del processore. Trovare l'indirizzo richiede circa 30 volte più tempo in caso di fallimento e, nel caso di dati dispersivi, potrebbe rallentare il servizio di diversi ordini di grandezza.

Conclusione

In questo articolo ho dimostrato che KDB+ e Q non sono solo adatti per l'archiviazione di grandi dati e l'accesso semplice tramite selezioni, ma anche per la creazione di servizi di elaborazione dati in grado di gestire centinaia di milioni di righe/gigabyte di dati anche in un singolo processo Q. Il linguaggio Q consente di implementare algoritmi legati all'elaborazione dei dati in modo straordinariamente conciso ed efficiente grazie alla sua natura vettoriale, all'interprete integrato del dialetto SQL e a un set di funzioni di libreria molto ben realizzato.

Vorrei sottolineare che quanto sopra è solo una parte delle possibilità di Q, che presenta anche altre caratteristiche uniche. Ad esempio, un protocollo IPC estremamente semplice che elimina il confine tra i singoli processi Q e consente di unire centinaia di questi processi in una rete unica, che può trovarsi su decine di server situati in diverse parti del mondo.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster