Cosa sia il database KDB+, il linguaggio di programmazione Q, quali sono i loro punti di forza e di debolezza, puoi leggerlo nel mio precedente e brevemente nell'introduzione. In questo articolo realizzeremo in Q un servizio che elaborerà un flusso di dati in ingresso e calcolerà in tempo reale varie funzioni aggregative al minuto (cioè riuscirà a calcolare tutto prima della successiva porzione di dati). La principale caratteristica di Q è che è un linguaggio vettoriale, che consente di operare non su singoli oggetti, ma su array, array di array e altre strutture complesse. Linguaggi come Q e parenti come K, J, APL sono celebri 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 in questi linguaggi. Questo è esattamente ciò che desidero dimostrare in questo articolo.

Introduzione
KDB+ è un database colonne, orientato a volumi di dati molto grandi, organizzati in un certo modo (principalmente per tempo). Viene utilizzato principalmente nelle organizzazioni finanziarie: banche, fondi di investimento, compagnie assicurative. Il linguaggio Q è il linguaggio interno di KDB+, che consente di lavorare in modo efficiente con questi dati. L'ideologia di Q è la brevità e l'efficienza, mentre la comprensibilità viene sacrificata. Questo è giustificato dal fatto che un linguaggio vettoriale sarà comunque difficile da percepire, e la brevità e la densità della registrazione permettono di vedere su un unico schermo una parte molto maggiore del programma, facilitando infine la sua comprensione.
In questo articolo realizzeremo un programma completo in Q e potresti voler provarlo tu stesso. Per farlo, avrai bisogno di Q. Puoi scaricare la versione gratuita a 32 bit dal sito dell'azienda kx – . Lì, se ti interessa, troverai informazioni di riferimento su Q, il libro e vari articoli su questo argomento.
Definizione del compito
C'è una fonte che invia ogni 25 millisecondi una tabella di dati. Poiché KDB+ è utilizzato principalmente in finanza, consideriamo che si tratti di una tabella di transazioni (trades), che presenta le seguenti colonne: time (tempo in millisecondi), sym (simbolo della società in borsa – IBM, AAPL,…), prezzo (il prezzo a cui sono state acquistate le azioni), dimensione (dimensione dell'operazione). L'intervallo di 25 millisecondi è stato scelto arbitrariamente, non è né troppo piccolo né troppo grande. La sua presenza significa che i dati arrivano nel servizio già bufferizzati. Sarebbe stato facile implementare il buffering lato servizio, inclusa la gestione dinamica in base ai carichi attuali, ma per semplicità ci limitiamo a un intervallo fisso.
Il servizio deve calcolare al minuto per ogni simbolo in arrivo dalla colonna sym un insieme di funzioni aggreganti – max price, avg price, sum size, ecc. informazioni utili. Per semplicità supponiamo che tutte le funzioni possano essere calcolate in modo incrementale, cioè per ottenere un nuovo valore è sufficiente conoscere due numeri – il valore precedente e il valore in arrivo. Ad esempio, le funzioni max, average, sum hanno questa proprietà, mentre la funzione mediana no.
Supponiamo inoltre che il flusso di dati in arrivo sia ordinato temporalmente. Questo ci permetterà di lavorare solo con l'ultima minuto. Nella pratica, è sufficiente saper lavorare con il minuto attuale e quello precedente nel caso in cui alcuni aggiornamenti arrivino in ritardo. Per semplicità non prenderemo in considerazione questo caso.
Funzioni aggreganti
Di seguito sono elencate le necessarie funzioni aggreganti. Ne ho incluse il maggior numero possibile per aumentare il carico sul servizio:
- high – max price – prezzo massimo per minuto.
- low – min price – prezzo minimo per minuto.
- firstPrice – first price – primo prezzo per minuto.
- lastPrice – last price – ultimo prezzo per minuto.
- firstSize – first size – primo dimensione dell'operazione per minuto.
- lastSize – last size — ultima dimensione dell'operazione per minuto.
- numTrades – count i – numero di operazioni per minuto.
- volume – sum size – somma delle dimensioni delle operazioni per minuto.
- pvolume – sum price – somma dei prezzi per minuto, necessaria per avgPrice.
- turnover – sum price*size – volume totale delle operazioni per minuto.
- avgPrice – pvolume%numTrades – prezzo medio per minuto.
- avgSize – volume%numTrades – dimensione media dell'operazione per minuto.
- vwap – turnover%volume – prezzo medio ponderato per dimensione dell'operazione per minuto.
- cumVolume – sum volume – dimensione totale delle operazioni nel tempo.
Discutiamo subito un punto 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 il valore null, il loro valore non è definito. Altre come volume devono sempre essere impostate a 0. Ci sono anche colonne che richiedono un approccio combinato: per esempio, cumVolume deve essere copiato dal minuto precedente, mentre per il primo deve essere impostato a 0. Definiamo tutti questi parametri usando il tipo di dato dizionario (analogo alla 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 rimane solo da impostare i corretti sym e time. Può essere utilizzata per aggiungere nuove righe nella tabella.
aggCols sarà necessario durante la creazione della funzione aggregante. La lista deve essere invertita a causa delle peculiarità dell'ordine di calcolo delle espressioni in Q (da destra a sinistra). L'obiettivo è garantire il calcolo in direzione da high a cumVolume, poiché alcune colonne dipendono da quelle precedenti.
Colonne che devono essere copiate nel nuovo 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 distinguere tre tipi:
- Accumulatori (volume, turnover,...) – dobbiamo aggiungere il valore in ingresso al precedente.
- Con punto specifico (high, low,...) – il primo valore nel minuto proviene dai dati in ingresso, gli altri vengono calcolati tramite funzione.
- Altri. Vengono sempre calcolati tramite funzione.
Definiamo le variabili per queste classi:
accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;
Ordine di calcoli
Aggiorneremo la tabella aggregata in due fasi. Per efficienza, prima comprimeremo la tabella in ingresso in modo che rimanga una riga per ciascun simbolo e minuto. Il fatto che tutte le nostre funzioni siano incrementali e associative ci garantisce che il risultato di questo passo aggiuntivo non cambierà. La tabella potrebbe essere compressa utilizzando select:
select high:max price, low:min price … by sym,time.minute from table
Questo metodo ha uno svantaggio: il set di colonne calcolate è definito in anticipo. Fortunatamente, in Q select è implementato anche come funzione, dove è possibile passare argomenti creati dinamicamente:
?[table;whereClause;byClause;selectClause]
Non descriverò in dettaglio il formato degli argomenti; nel nostro caso, soltanto le espressioni by e select saranno non banali e dovranno essere dizionari del tipo columns!expressions. Così, la funzione di compressione può essere definita 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 è una funzione map in Q per un elenco singolo
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];
Per chiarezza, ho utilizzato la funzione parse, che trasforma una stringa contenente un'espressione Q in un valore che può essere passato alla funzione eval e che è necessario nella selezione funzionale. Notiamo anche che preprocess è definita come proiezione (ossia, una funzione con argomenti parzialmente specificati) della funzione select, un argomento (tabella) è assente. Se applichiamo preprocess alla tabella, otteniamo una tabella compressa.
La seconda fase consiste nell'aggiornamento della tabella aggregata. Scriviamo prima l'algoritmo in pseudocodice:
for each sym in inputTable
idx: indice di riga nella tabella agg per sym+currentTime;
aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
…
In Q è consuetudine utilizzare funzioni map/reduce al posto dei cicli. Ma poiché Q è un linguaggio vettoriale e tutte le operazioni possono essere applicate a tutti i simboli contemporaneamente, in prima approssimazione possiamo evitare del tutto i cicli, eseguendo operazioni su tutti i simboli insieme:
idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…
Tuttavia, possiamo andare oltre; in Q esiste un operatore unico e estremamente potente - l'operatore di assegnazione generalizzata. Questo operatore consente di modificare un insieme di valori in 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;…)];
Purtroppo, per assegnare valori a una tabella è necessario un elenco di righe, e non di colonne, e occorre quindi trasporre la matrice (da un elenco di colonne a un elenco di righe) usando la funzione flip. Per una tabella grande questo è oneroso, quindi applicheremo l'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;…)];
Stiamo nuovamente utilizzando la proiezione di funzioni. Si noti inoltre che in Q la creazione di una lista è anch'essa una funzione e possiamo richiamarla usando la funzione each(map) per ottenere una lista di liste.
Per fare in modo che il set di colonne calcolate non sia fisso, creeremo l'espressione precedentemente in modo dinamico. Prima definiamo le funzioni per calcolare ogni colonna, utilizzando le variabili row e inp per fare riferimento ai dati aggregati e 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 della colonna row[`numTrades] - se presenta 0, allora il valore è il primo. In Q esiste una funzione di selezione — ?[Boolean list;list1;list2] — che seleziona il valore dalla lista 1 o 2 a seconda della condizione nel 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 generalizzata con la mia funzione (l'espressione tra parentesi graffe). Ad essa viene passato il valore corrente (primo argomento) e un argomento aggiuntivo che fornisco nel quarto parametro.
Aggiungeremo separatamente le colonne di accumulo, poiché per esse la funzione è la medesima:
// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
Questa è un'assegnazione normale per gli standard di Q, solo che assegno immediatamente un elenco 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 di cui sopra. 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 è da destra a sinistra.
Ora abbiamo due funzioni principali necessarie per i calcoli, manca solo da aggiungere un po' di infrastruttura e il servizio è pronto.
Passi finali
Abbiamo le funzioni preprocess e updateAgg, che svolgono tutto il lavoro. Ma è necessario garantire anche la corretta transizione attraverso i minuti e calcolare gli indici per l'aggregazione. Per prima cosa, 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 corrente
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 corrente:
roll:{[tm]
if[currTime>tm; :init[]]; // se abbiamo superato 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 all'elenco conosciuto
// aggiungiamo alla tabella sym, time e rollColumns usando l'assegnazione generica.
// La funzione ^ inserisce valori predefiniti per le colonne roll, se il simbolo non è nella cache. value flip table restituisce un elenco di 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 client 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
};
uppMinute:{[data;tm]
if[tmcurrTime; roll tm; currTime::tm]; // cambiamo minuto, se necessario
data:select from data where time=tm; // filtrazione
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. Questo è 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. A tale scopo, avviamolo in un processo separato (inserisci il codice in un file service.q) e chiamiamo la funzione init:
q service.q –p 5566
q)init[]
In un'altra console, avvia un secondo processo Q e collegati al primo:
h:hopen `:host:5566
h:hopen 5566 \/\/ se entrambi sono sulla stessa host
Per prima cosa, creiamo un elenco 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 dei simboli tre veri, 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 se 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|2919Ora procederemo a test di carico per scoprire quante informazioni il servizio può elaborare al minuto. Ricordo che abbiamo impostato l'intervallo per gli aggiornamenti a 25 millisecondi. Pertanto, il servizio deve (in media) riuscire a completare l'aggiornamento in almeno 20 millisecondi per dare il tempo agli utenti di richiedere i 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 è pari a due minuti. Possiamo provare a eseguire prima per 1000 righe ogni 25 millisecondi:
start 1000
Nel mio caso, il risultato si attesta attorno a un paio di millisecondi per aggiornamento. Quindi aumenterò immediatamente 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
Ancora niente di particolare, eppure si tratta di 24 milioni di righe al minuto, 400 mila al secondo. Più di 25 millisecondi l'aggiornamento ha rallentato solo 5 volte, evidentemente al 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 tenere il passo, ma riesce comunque a rimanere a galla. Un tale volume di dati (240 milioni di righe al minuto) è estremamente grande; in questi casi è consuetudine avviare più cloni (o anche decine di cloni) del servizio, ognuno dei quali elabora solo una parte dei simboli. Tuttavia, il risultato è impressionante per un linguaggio interpretato, che è principalmente orientato all'archiviazione dei dati.
Potrebbe sorgere la domanda sul perché il tempo aumenti in modo non lineare insieme alla dimensione di ciascun aggiornamento. La ragione è che la funzione di compressione è in realtà una funzione C che funziona in modo molto più efficiente di updateAgg. A partire da una certa dimensione dell'aggiornamento (intorno a 10.000), updateAgg raggiunge il suo limite e, oltre a questo, il suo tempo di esecuzione non dipende più dalla dimensione dell'aggiornamento. È proprio grazie al passo preliminare Q che il servizio è in grado di gestire tali volumi di dati. Questo sottolinea quanto sia importante, lavorando con grandi dati, scegliere l'algoritmo giusto. Un altro aspetto è la corretta conservazione dei dati in memoria. Se i dati non fossero memorizzati in modo colonnare o non fossero ordinati temporalmente, ci troveremmo di fronte a un fenomeno noto come TLB cache miss: l'assenza dell'indirizzo della pagina di memoria nella cache degli indirizzi del processore. La ricerca dell'indirizzo richiede circa 30 volte più tempo in caso di fallimento e, in caso di dati dispersi, può rallentare il servizio di diversi ordini di grandezza.
Conclusione
In questo articolo ho dimostrato che il database KDB+ e Q sono adatti non solo per l'archiviazione di grandi dati e l'accesso semplice attraverso selezione, ma anche per la creazione di servizi di elaborazione dati capaci di gestire centinaia di milioni di righe/ gigabyte di dati anche in un singolo processo Q. Il linguaggio Q consente di implementare in modo estremamente conciso ed efficiente algoritmi relativi all'elaborazione dei dati grazie alla sua natura vettoriale, all'interprete SQL integrato e a un eccellente insieme di funzioni di libreria.
Vorrei notare che quanto sopra rappresenta solo una parte delle possibilità di Q, che ha anche altre caratteristiche uniche. Ad esempio, un protocollo IPC estremamente semplice che cancella 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 in diverse parti del mondo.
Fonte: habr.com
