Caratteristiche del linguaggio Q e KDB+ utilizzando l'esempio di un servizio in tempo reale

Puoi leggere cosa sono la base KDB+, il linguaggio di programmazione Q, quali sono i loro punti di forza e di debolezza nel mio precedente Articolo e brevemente nell'introduzione. Nell'articolo implementeremo un servizio su Q che elaborerà il flusso di dati in ingresso e calcolerà ogni minuto varie funzioni di aggregazione in modalità “tempo reale” (ovvero avrà il tempo di calcolare tutto prima della successiva porzione di dati). La caratteristica principale di Q è che si tratta di un linguaggio vettoriale che permette di operare non con singoli oggetti, ma con i loro array, array di array e altri oggetti complessi. Lingue come Q e i suoi parenti K, J, APL sono famosi per la loro brevità. Spesso un programma che occupa diverse schermate di codice in un linguaggio familiare come Java può essere scritto su di esse in poche righe. Questo è ciò che voglio dimostrare in questo articolo.

Caratteristiche del linguaggio Q e KDB+ utilizzando l'esempio di un servizio in tempo reale

Introduzione

KDB+ è un database a colonne focalizzato su grandi quantità di dati, ordinati in modo specifico (principalmente in base al tempo). Viene utilizzato principalmente nelle istituzioni finanziarie: banche, fondi di investimento, compagnie di assicurazione. Il linguaggio Q è il linguaggio interno di KDB+ che ti consente di lavorare in modo efficace con questi dati. L’ideologia Q è brevità ed efficienza, mentre la chiarezza viene sacrificata. Ciò è giustificato dal fatto che il linguaggio vettoriale sarà comunque difficile da comprendere, e la brevità e la ricchezza della registrazione consentono di vedere una parte molto più ampia del programma su uno schermo, il che alla fine ne facilita la comprensione.

In questo articolo implementiamo un programma completo in Q e potresti voler provarlo. Per fare ciò, avrai bisogno della Q effettiva. Puoi scaricare la versione gratuita a 32 bit sul sito web dell'azienda kx – www.kx.com. Lì, se sei interessato, troverai informazioni di riferimento su Q, il libro Q Per i mortali e vari articoli su questo argomento.

Formulazione del problema

Esiste una fonte che invia una tabella con dati ogni 25 millisecondi. Poiché KDB+ viene utilizzato principalmente in finanza, supponiamo che si tratti di una tabella di transazioni (operazioni), che ha le seguenti colonne: time (tempo in millisecondi), sym (denominazione della società in borsa - IBM, AAPL,…), prezzo (il prezzo al quale sono state acquistate le azioni), dimensione (dimensione della transazione). L'intervallo di 25 millisecondi è arbitrario, né troppo piccolo né troppo lungo. La sua presenza significa che i dati arrivano al servizio già bufferizzati. Sarebbe semplice implementare il buffering lato servizio, incluso il buffering dinamico in base al carico corrente, ma per semplicità ci concentreremo su un intervallo fisso.

Il servizio deve contare ogni minuto per ogni simbolo in entrata dalla colonna sym una serie di funzioni di aggregazione: prezzo massimo, prezzo medio, dimensione della somma, ecc. informazioni utili. Per semplicità, assumeremo che tutte le funzioni possano essere calcolate in modo incrementale, cioè per ottenere un nuovo valore è sufficiente conoscere due numeri: il valore vecchio e quello in arrivo. Ad esempio, le funzioni max, media e somma hanno questa proprietà, ma la funzione mediana no.

Assumeremo inoltre che il flusso di dati in entrata sia ordinato temporalmente. Questo ci darà l'opportunità di lavorare solo con l'ultimo minuto. In pratica è sufficiente poter lavorare con i minuti attuali e quelli precedenti nel caso in cui alcuni aggiornamenti siano in ritardo. Per semplicità non considereremo questo caso.

Funzioni di aggregazione

Le funzioni di aggregazione richieste sono elencate di seguito. Ne ho presi il maggior numero possibile per aumentare il carico sul servizio:

  • alto – prezzo massimo – prezzo massimo al minuto.
  • basso – prezzo minimo – prezzo minimo al minuto.
  • firstPrice – primo prezzo – primo prezzo al minuto.
  • lastPrice – ultimo prezzo – ultimo prezzo al minuto.
  • firstSize – prima dimensione – prima dimensione commerciale al minuto.
  • lastSize – last size – l'ultima dimensione dell'operazione in un minuto.
  • numTrades – count i – numero di operazioni al minuto.
  • volume – dimensione somma – somma delle dimensioni degli scambi al minuto.
  • pvolume – somma prezzo – somma dei prezzi al minuto, richiesta per avgPrice.
  • – somma fatturato prezzo*dimensione – volume totale di transazioni al minuto.
  • avgPrice – pvolume%numTrades – prezzo medio al minuto.
  • avgSize – volume%numTrades – dimensione media delle operazioni al minuto.
  • vwap – fatturato%volume – prezzo medio al minuto ponderato in base alla dimensione della transazione.
  • cumVolume – volume totale – dimensione accumulata delle transazioni nel corso dell'intero tempo.

Discutiamo immediatamente di un punto non ovvio: come inizializzare queste colonne per la prima volta e per ogni minuto successivo. Alcune colonne del tipo firstPrice devono essere inizializzate su null ogni volta; il loro valore non è definito. Altri tipi di volume devono essere sempre impostati su 0. Ci sono anche colonne che richiedono un approccio combinato, ad esempio cumVolume deve essere copiato dal minuto precedente e per il primo impostato su 0. Impostiamo tutti questi parametri utilizzando i dati del dizionario tipo (analogo a un record):

// 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 sim e ora al dizionario per comodità, ora initWith è una riga già pronta dalla tabella aggregata finale, dove resta da impostare il sim e l'ora corretti. Puoi usarlo per aggiungere nuove righe a una tabella.

Avremo bisogno di aggCols durante la creazione di una funzione di aggregazione. L'elenco deve essere invertito a causa dell'ordine in cui vengono valutate le espressioni in Q (da destra a sinistra). L'obiettivo è garantire che il calcolo vada da alto a cumVolume, poiché alcune colonne dipendono da quelle precedenti.

Colonne che devono essere copiate in un nuovo minuto da quello precedente, la colonna sym viene aggiunta per comodità:

rollColumns:`sym`cumVolume;

Ora dividiamo le colonne in gruppi in base a come devono essere aggiornate. Si possono distinguere tre tipologie:

  1. Accumulatori (volume, fatturato,..) – dobbiamo aggiungere il valore in entrata a quello precedente.
  2. Con un punto speciale (alto, basso, ..) – il primo valore del minuto viene preso dai dati in ingresso, il resto viene calcolato utilizzando la funzione.
  3. Riposo. Calcolato sempre utilizzando una funzione.

Definiamo le variabili per queste classi:

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

Ordine di calcolo

Aggiorneremo la tabella aggregata in due fasi. Per efficienza, riduciamo prima la tabella in arrivo in modo che ci sia solo una riga per ogni carattere e minuto. Il fatto che tutte le nostre funzioni siano incrementali e associative garantisce che il risultato di questo passaggio aggiuntivo non cambierà. Potresti ridurre la tabella usando select:

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

Questo metodo presenta uno svantaggio: l'insieme di colonne calcolate è predefinito. Fortunatamente, in Q, select è implementato anche come funzione in cui è possibile sostituire argomenti creati dinamicamente:

?[table;whereClause;byClause;selectClause]

Non descriverò in dettaglio il formato degli argomenti; nel nostro caso, solo le espressioni by e select non saranno banali e dovrebbero essere dizionari della forma colonne!espressioni. Pertanto la funzione di contrazione può essere definita come segue:

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 это функция map в Q для одного списка
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

Per chiarezza, ho utilizzato la funzione parse, che trasforma una stringa con un'espressione Q in un valore che può essere passato alla funzione eval e che è richiesto nella funzione select. Si noti inoltre che il preprocesso è definito come una proiezione (ovvero una funzione con argomenti parzialmente definiti) della funzione select, manca un argomento (la tabella). Se applichiamo la preelaborazione a una tabella, otterremo una tabella compressa.

La seconda fase è l'aggiornamento della tabella aggregata. Scriviamo prima l'algoritmo in pseudocodice:

for each sym in inputTable
  idx: row index in agg table for sym+currentTime;
  aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
  aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
  …

In Q, è comune utilizzare le funzioni map/reduce invece dei loop. Ma poiché Q è un linguaggio vettoriale e possiamo facilmente applicare tutte le operazioni a tutti i simboli contemporaneamente, in prima approssimazione possiamo fare a meno di un ciclo, eseguendo 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, Q ha un operatore unico ed estremamente potente: l'operatore di assegnazione generalizzato. Ti consente di modificare un insieme di valori in una struttura dati complessa utilizzando un elenco di indici, funzioni e argomenti. Nel nostro caso assomiglia a questo:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], в нашем случае функция – это присваивание
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

Sfortunatamente, per assegnare una tabella è necessario un elenco di righe, non di colonne, e devi trasporre la matrice (elenco di colonne in elenco di righe) utilizzando la funzione flip. Questo è costoso per una tabella di grandi dimensioni, quindi applichiamo invece un'assegnazione generalizzata a ciascuna colonna separatamente, utilizzando la funzione map (che assomiglia a un apostrofo):

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

Utilizziamo nuovamente la proiezione di funzioni. Si noti inoltre che in Q anche la creazione di un elenco è una funzione e possiamo chiamarla utilizzando la funzione Each(map) per ottenere un elenco di elenchi.

Per garantire che l'insieme di colonne calcolate non sia fisso, creeremo dinamicamente l'espressione precedente. Definiamo innanzitutto le funzioni per calcolare ciascuna 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 dalla colonna row[`numTrades]: se contiene 0, il valore è il primo. Q ha una funzione di selezione - ?[Boolean list;list1;list2] - che seleziona un 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 compito generalizzato con la mia funzione (un'espressione tra parentesi graffe). Riceve il valore corrente (il primo argomento) e un argomento aggiuntivo, che passo nel quarto parametro.

Aggiungiamo separatamente gli altoparlanti a batteria, poiché la funzione è la stessa per loro:

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

Si tratta di un'assegnazione normale secondo gli standard Q, ma assegno un elenco di valori contemporaneamente. 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 che ho fornito sopra. Il risultato sarà simile a questo:

{[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 valutazione delle colonne è invertito perché in Q l'ordine di valutazione va da destra a sinistra.

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

Passi finali

Abbiamo funzioni di preelaborazione e updateAgg che fanno tutto il lavoro. Ma è ancora necessario garantire la corretta transizione attraverso i minuti e calcolare gli indici per l'aggregazione. Prima di tutto definiamo la funzione init:

init:{
  tradeAgg:: 0#enlist[initWith]; // создаем пустую типизированную таблицу, enlist превращает словарь в таблицу, а 0# означает взять 0 элементов из нее
  currTime::00:00; // начнем с 0, :: означает, что присваивание в глобальную переменную
  currSyms::`u#`symbol$(); // `u# - превращает список в дерево, для ускорения поиска элементов
  offset::0; // индекс в tradeAgg, где начинается текущая минута 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // кэш для последних значений roll колонок, таблица с ключом sym
 }

Definiremo anche la funzione roll, che modificherà i minuti correnti:

roll:{[tm]
  if[currTime>tm; :init[]]; // если перевалили за полночь, то просто вызовем init
  rollCache,::offset _ rollColumns#tradeAgg; // обновим кэш – взять roll колонки из aggTable, обрезать, вставить в rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

Avremo bisogno di una funzione per aggiungere nuovi caratteri:

addSyms:{[syms]
  currSyms,::syms; // добавим в список известных
  // добавим в таблицу sym, time и rollColumns воспользовавшись обобщенным присваиванием.
  // Функция ^ подставляет значения по умолчанию для roll колонок, если символа нет в кэше. value flip table возвращает список колонок в таблице.
  `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 (il nome tradizionale di questa funzione per i servizi Q), che viene chiamata dal client per aggiungere dati:

upd:{[tblName;data] // tblName нам не нужно, но обычно сервис обрабатывает несколько таблиц 
  tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
  updMinute[data] each tm; // добавим данные для каждой минуты
};
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]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

È tutto. Ecco il codice completo del nostro servizio, come promesso, 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 l'andamento del servizio. Per fare ciò, eseguiamolo in un processo separato (inseriamo il codice nel file service.q) e chiamiamo 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 // если оба на одном хосте

Innanzitutto, creiamo un elenco di simboli: 10000 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 tre simboli reali all'elenco per facilitarne la ricerca nella tabella. La funzione rnd crea una tabella casuale con n righe, dove il tempo varia da t a t+25 millisecondi.

Ora puoi provare a inviare dati al servizio (aggiungi le prime dieci ore):

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

Puoi verificare nel servizio che la tabella sia stata aggiornata:

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

Il 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

Effettuiamo ora un test di carico per scoprire quanti dati il ​​servizio può elaborare al minuto. Lascia che ti ricordi che impostiamo l'intervallo di aggiornamento su 25 millisecondi. Di conseguenza, il servizio deve (in media) contenere almeno 20 millisecondi per aggiornamento per dare agli utenti il ​​tempo 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 sono due minuti. Puoi provare a eseguire prima per 1000 righe ogni 25 millisecondi:

start 1000

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

start 10000

Il risultato:

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

Ancora una volta, niente di speciale, ma si tratta di 24 milioni di linee al minuto, 400mila al secondo. Per più di 25 millisecondi, l'aggiornamento ha rallentato solo 5 volte, apparentemente quando cambiava il minuto. Aumentiamo a 100.000:

start 100000

Il risultato:

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

Come puoi vedere, 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 è normale lanciare diversi cloni (o addirittura dozzine di cloni) del servizio, ognuno dei quali elabora solo una parte dei caratteri. Tuttavia, il risultato è impressionante per un linguaggio interpretato che si concentra principalmente sulla memorizzazione dei dati.

Potrebbe sorgere la domanda sul perché il tempo cresce in modo non lineare con la dimensione di ciascun aggiornamento. Il motivo è che la funzione di restringimento è in realtà una funzione C, che è molto più efficiente di updateAgg. A partire da una certa dimensione di aggiornamento (circa 10.000), updateAgg raggiunge il suo limite massimo e quindi il suo tempo di esecuzione non dipende dalla dimensione di aggiornamento. È grazie al passaggio preliminare Q che il servizio è in grado di digerire tali volumi di dati. Ciò evidenzia quanto sia importante scegliere l’algoritmo giusto quando si lavora con i big data. Un altro punto è la corretta memorizzazione dei dati in memoria. Se i dati non fossero archiviati in colonne o non fossero ordinati in base al tempo, acquisiremmo familiarità con un errore nella cache TLB: l'assenza di un indirizzo di pagina di memoria nella cache degli indirizzi del processore. La ricerca di un indirizzo impiega circa 30 volte di più in caso di esito negativo e, se i dati sono sparsi, il servizio può rallentare più volte.

conclusione

In questo articolo ho mostrato che i database KDB+ e Q sono adatti non solo per archiviare dati di grandi dimensioni e accedervi facilmente tramite select, ma anche per creare servizi di elaborazione dati in grado di digerire centinaia di milioni di righe/gigabyte di dati anche in un unico processo Q. Il linguaggio Q stesso consente un'implementazione estremamente concisa ed efficiente di algoritmi relativi all'elaborazione dei dati grazie alla sua natura vettoriale, all'interprete di dialetto SQL integrato e ad un insieme di funzioni di libreria di grande successo.

Noterò che quanto sopra è solo una parte di ciò che Q può fare, ha anche altre caratteristiche uniche. Ad esempio, un protocollo IPC estremamente semplice che cancella il confine tra i singoli processi Q e consente di combinare centinaia di questi processi in un'unica rete, che può trovarsi su dozzine di server in diverse parti del mondo.

Fonte: habr.com

Aggiungi un commento