Capabilitățile limbajului Q și KDB+ prin exemplul serviciului în timp real

Despre ce este baza de date KDB+, limbajul de programare Q, ce puncte forte și slabe are, puteți citi în articolul meu anterior pe care l-ați citit și pe scurt în introducere. În acest articol, vom implementa un serviciu în Q care va procesa fluxul de date în timp real și va calcula funcții agregate minut cu minut (adică va reuși să facă toate calculele înainte de a primi următoarea porție de date). Principala caracteristică a Q este că este un limbaj vectorial, permițând operarea nu pe obiecte individuale, ci pe array-uri, array-uri de array-uri și alte obiecte complexe. Limbaje precum Q și omoloacele sale K, J, APL sunt celebre pentru concizia lor. Adesea, un program care ocupă câteva ecrane de cod într-un limbaj familiar precum Java poate fi scris în câteva linii în aceste limbaje. Exact acest lucru vreau să demonstrez în acest articol.

Capabilitățile limbajului Q și KDB+ prin exemplul serviciului în timp real

Introducere

KDB+ este o bază de date coloană, orientată spre volume foarte mari de date, ordonate într-un anumit mod (în primul rând, pe timpul de procesare). Este folosită în principal în organizațiile financiare – bănci, fonduri de investiții, companii de asigurări. Limbajul Q este limbajul intern al KDB+, permițând lucrul eficient cu aceste date. Ideologia Q se bazează pe concizie și eficiență, iar claritatea este sacrificată în acest caz. Aceasta se justifică prin faptul că un limbaj vectorial va fi oricum greu de înțeles, iar concizia și densitatea codului permit vizualizarea unei părți mult mai mari a programului pe un singur ecran, ceea ce, în cele din urmă, facilitează înțelegerea acestuia.

În acest articol, vom realiza un program complet în Q, iar dumneavoastră, poate, veți dori să îl încercați. Pentru aceasta, aveți nevoie de limbajul Q. Puteți descărca versiunea gratuită pe 32 de biți de pe site-ul companiei kx – www.kx.com. Acolo, dacă sunteți interesat, veți găsi informații de referință despre Q, cartea Q For Mortals și diverse articole pe această temă.

Formularea problemei

Există o sursă care trimite un tabel de date la fiecare 25 de milisecunde. Deoarece KDB+ este folosit în primul rând în domeniul financiar, să presupunem că este un tabel de tranzacții (trades), care are următoarele coloane: time (timp în milisecunde), sym (simbolul companiei la bursă – IBM, AAPL,…), price (prețul, la care au fost cumpărate acțiunile), size (dimensiunea tranzacției). Intervalul de 25 de milisecunde a fost ales arbitrar, nu este prea mic și nu prea mare. Prezența acestuia indică faptul că datele sosesc în serviciu deja în buffer. S-ar putea implementa cu ușurință bufferizarea de partea serviciului, inclusiv dinamic, în funcție de încărcătura actuală, dar pentru simplitate ne vom opri la un interval fix.

Serviciul trebuie să calculeze pe minut pentru fiecare simbol din coloana sym un set de funcții agregate – max price, avg price, sum size etc. informații utile. Pentru simplitate vom presupune că toate funcțiile pot fi calculate incremental, adică pentru a obține o nouă valoare, este suficient să se știe două numere – valoarea veche și valoarea de intrare. De exemplu, funcțiile max, average, sum au această proprietate, în timp ce funcția mediană nu.

De asemenea, vom presupune că fluxul de date de intrare este ordonat în timp. Acest lucru ne va permite să lucrăm doar cu ultima minută. În practică, este suficient să lucrăm cu minutul curent și cel anterior, pentru cazul în care unele actualizări au întârziat. Pentru simplitate, nu vom lua în considerare acest caz.

Funcții agregate

Mai jos sunt enumerate funcțiile agregate necesare. Am luat cât mai multe pentru a crește încărcătura pe serviciu:

  • high – max price – prețul maxim pe minut.
  • low – min price – prețul minim pe minut.
  • firstPrice – first price – primul preț pe minut.
  • lastPrice – last price – ultimul preț pe minut.
  • firstSize – first size – prima dimensiune a tranzacției pe minut.
  • lastSize – last size — ultima dimensiune a tranzacției pe minut.
  • numTrades – count i – numărul de tranzacții pe minut.
  • volume – sum size – suma dimensiunilor tranzacțiilor pe minut.
  • pvolume – sum price – suma prețurilor pe minut, necesară pentru avgPrice.
  • turnover – sum price*size – volumul total de tranzacții pe minut.
  • avgPrice – pvolume%numTrades – prețul mediu pe minut.
  • avgSize – volume%numTrades – dimensiunea medie a tranzacției pe minut.
  • vwap – turnover%volume – prețul mediu ponderat după dimensiunea tranzacției pe minut.
  • cumVolume – sum volume – dimensiunea cumulată a tranzacțiilor de-a lungul timpului.

Să discutăm imediat un aspect mai puțin evident – cum să inițializăm aceste coloane pentru prima dată și pentru fiecare minut ulterior. Unele coloane, cum ar fi firstPrice, trebuie inițializate de fiecare dată cu valoarea null, pentru că valoarea lor nu este definită. Alte coloane, cum ar fi volume, trebuie să fie setate întotdeauna la 0. Există, de asemenea, coloane care necesită o abordare combinată – de exemplu, cumVolume trebuie copiat din minutul anterior, iar pentru primul minut trebuie setat la 0. Vom defini toate aceste parametrii folosind tipul de date dicționar (analog înregistrării):

// 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 объяснен ниже

Am adăugat sym și time în dicționar pentru comoditate, acum initWith este un rând complet din tabelul agregat final, unde trebuie să definim corect sym și time. O putem folosi pentru a adăuga rânduri noi în tabel.

aggCols va fi necesar atunci când creăm funcția de agregare. Lista trebuie inversată din cauza particularităților ordinii de calcul al expresiilor în Q (de la dreapta la stânga). Scopul este de a asigura calcularea în direcția de la high la cumVolume, deoarece unele coloane depind de cele anterioare.

Coloanele care trebuie copiate în noul minut din precedentul, coloana sym a fost adăugată pentru comoditate:

rollColumns:`sym`cumVolume;

Acum vom împărți coloanele în grupuri conform modului în care trebuie actualizate. Pot fi identificate trei tipuri:

  1. Acmulatoare (volume, turnover,..) – trebuie să adunăm valoarea de intrare la cea anterioară.
  2. Cu un punct special (high, low, ..) – prima valoare din minut este preluată din datele de intrare, celelalte sunt calculate folosind o funcție.
  3. Celelalte. Sunt mereu calculate folosind o funcție.

Vom defini variabile pentru aceste clase:

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

Ordinea calculului

Vom actualiza tabelul agregat în două etape. Pentru eficiență, vom comprima mai întâi tabela de intrare astfel încât să rămână un singur rând pentru fiecare simbol și minut. Faptul că toate funcțiile noastre sunt incremental și asociative ne garantează că rezultatul acestui pas suplimentar nu se va schimba. Tabela ar putea fi comprimată folosind selectul:

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

Această metodă are un dezavantaj – setul de coloane calculabile este definit dinainte. Din fericire, în Q, selectul este implementat și ca o funcție, unde pot fi introduse argumente create dinamic:

?[table;whereClause;byClause;selectClause]

Nu voi detalia formatul argumentelor; în cazul nostru, expresiile by și select sunt singurele non-triviale și trebuie să fie dicționare de tipul columns!expressions. Astfel, funcția de compresie poate fi definită astfel:

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 este funcția map în Q pentru o singură listă
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

Pentru claritate, am folosit funcția parse, care transformă un șir cu expresii Q într-o valoare care poate fi transmisă funcției eval și care este necesară în selecția funcțională. De asemenea, observăm că preprocess este definit ca o proiecție (adică o funcție cu argumente parțial definite) a funcției select, un argument (tabelul) lipsind. Dacă aplicăm preprocess tabelului, vom obține un tabel comprimat.

A doua etapă constă în actualizarea tabelului agregat. Să scriem mai întâi algoritmul în pseudocod:

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];
  …

În Q, este obișnuit să folosim funcții map/reduce în loc de bucle. Dar deoarece Q este un limbaj vectorial și toate operațiile pot fi aplicate simultan tuturor simbolurilor, în prima aproximare putem face fără bucle, efectând operații cu toate simbolurile simultan:

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

Dar putem merge și mai departe; în Q există un operator unic și extrem de puternic – operatorul de atribuire generalizată. Acesta permite modificarea unui set de valori într-o structură de date complexă folosind o listă de indici, funcții și argumente. În cazul nostru, acesta arată astfel:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], în cazul nostru funcția este atribuire
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

Din păcate, pentru a atribui într-un tabel este necesară o listă de rânduri, și nu de coloane, așa că trebuie să transpunem matricea (lista de coloane în lista de rânduri) folosind funcția flip. Pentru un tabel mare, acest lucru este costisitor, așa că în loc de asta, vom aplica atribuirea generalizată fiecărei coloane în parte, folosind funcția map (care arată ca un apostrof):

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

Folosim din nou proiecția funcției. De asemenea, observați că în Q crearea unei liste este și ea o funcție și o putem apela folosind funcția each(map) pentru a obține o listă de liste.

Pentru a face setul de coloane calculate să nu fie fix, vom crea expresia de mai sus dinamic. Mai întâi, vom defini funcțiile pentru a calcula fiecare coloană, folosind variabilele row și inp pentru a face referire la datele agregate și cele de intrare:

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");

Unele coloane sunt speciale, iar prima lor valoare nu ar trebui să fie calculată printr-o funcție. Putem să stabilim că este prima pe coloana row[`numTrades] – dacă aceasta are 0, atunci valoarea este prima. În Q există o funcție de selecție — ?[Boolean list;list1;list2] – care alege o valoare din lista 1 sau 2 în funcție de condiția din primul argument:

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

Aici am apelat la atribuirea generalizată cu funcția mea (expresia în acolade). Aceasta primește valoarea curentă (primul argument) și un argument suplimentar care îl transmit în al patrulea parametru.

Separat, adăugăm coloanele de acumulator, deoarece pentru acestea funcția este aceeași:

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

Aceasta este o atribuție obișnuită în Q, doar că atribui imediat o listă de valori. În final, vom crea funcția principală:

// ":",/: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),")]}";

Prin această expresie creez dinamic o funcție dintr-un șir care conține expresia pe care am menționat-o mai sus. Rezultatul va arăta astfel:

{[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])]}

Ordinea de calculare a coloanelor este inversată, deoarece în Q ordinea de calcul este de la dreapta la stânga.

Acum avem două funcții de bază necesare pentru calcule, rămâne să adăugăm puțin infrastructură și serviciul este gata.

Pașii finali

Avem funcțiile preprocess și updateAgg, care fac toată treaba. Dar este necesar să asigurăm o trecere corectă prin minute și să calculăm indicii pentru agregare. În primul rând, vom defini funcția init:

init:{
  tradeAgg:: 0#enlist[initWith]; // creăm un tabel tipizat gol, enlist transformă dicționarul în tabel, iar 0# înseamnă a lua 0 elemente din el
  currTime::00:00; // să înceapă cu 0, :: înseamnă că atribuirea se face într-o variabilă globală
  currSyms::`u#`symbol$(); // `u# - transformă lista într-un arbore, pentru a accelera căutarea elementelor
  offset::0; // indicele în tradeAgg, unde începe minuta curentă 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // cache pentru ultimele valori ale coloanelor de rulare, tabel cu cheia sym
 }

De asemenea, vom defini funcția roll, care va schimba minuta curentă:

roll:{[tm]
  if[currTime>tm; :init[]]; // dacă am depășit miezul nopții, vom apela doar init
  rollCache,::offset _ rollColumns#tradeAgg; // actualizăm cache-ul – luăm coloanele de rulare din aggTable, le tăiem, le inserăm în rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

Ne va trebui o funcție pentru a adăuga noi simboluri:

addSyms:{[syms]
  currSyms,::syms; // adăugăm în lista cunoscută
  // adăugăm în tabelul sym, time și rollColumns folosind o atribuire generalizată.
  // Funcția ^ completează valorile implicite pentru coloanele de rulare, dacă simbolul nu este în cache. value flip table returnează lista coloanelor din tabel.
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 }

Și, în cele din urmă, funcția upd (numele tradițional al acestei funcții pentru serviciile Q), care este apelată de client pentru a adăuga date:

upd:{[tblName;data] // tblName nu ne este necesar, dar de obicei serviciul procesează mai multe tabele
  tm:exec distinct time from data:() xkey preprocess data; // preprocess & calculează timpul
  updMinute[data] each tm; // adăugăm date pentru fiecare minut
};
updMinute:{[data;tm]
  if[tmcurrTime; roll tm; currTime::tm]; // schimbăm minutul, dacă este necesar
  data:select from data where time=tm; // filtrare
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // simboluri noi
  updateAgg[`tradeAgg;offset+currSyms?syms;data]; // actualizăm tabela agregată. Funcția ? caută indicele elementelor listei din dreapta în lista din stânga.
 };

Și asta e tot. Iată codul complet al serviciului nostru, așa cum am promis, doar câteva linii:

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];
 };

Testare

Vom verifica performanța serviciului. Pentru aceasta, îl vom rula într-un proces separat (plasați codul în fișierul service.q) și vom apela funcția init:

q service.q –p 5566

q)init[]

Într-o altă consolă, rulați al doilea proces Q și conectați-vă la primul:

h:hopen `:host:5566
h:hopen 5566 \/\/ dacă ambele sunt pe același host

Mai întâi, să creăm o listă de simboluri – 10000 de bucăți și să adăugăm o funcție pentru a crea un tabel aleatoriu. În a doua consolă:

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

Am adăugat în lista simbolurilor trei reale, pentru a facilita căutarea lor în tabel. Funcția rnd creează un tabel aleatoriu cu n rânduri, unde timpul variază de la t la t+25 milisecunde.

Acum putem încerca să trimitem date către serviciu (să adăugăm primele zece ore):

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

Se poate verifica în serviciu că tabelul s-a actualizat:

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

Rezultatul:

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

Acum vom efectua un test de stres pentru a afla câte date poate gestiona serviciul pe minut. Vă reamintesc că am stabilit un interval pentru actualizări de 25 de milisecunde. Prin urmare, serviciul trebuie (în medie) să se încadreze în cel puțin 20 de milisecunde pe actualizare pentru a oferi timp utilizatorilor să solicite date. Introduceți următoarele în al doilea proces:

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 – asta este două minute. Putem încerca să pornim mai întâi pentru 1000 de rânduri la fiecare 25 de milisecunde:

start 1000

În cazul meu, rezultatul este de aproximativ câteva milisecunde pe actualizare. Așa că, imediat, voi crește numărul de rânduri la 10.000:

start 10000

Rezultatul:

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

Din nou, nimic special, și totuși acesta este 24 de milioane de rânduri pe minut, 400 de mii pe secundă. Actualizarea a fost întârziată timp de mai mult de 25 de milisecunde doar de 5 ori, probabil la schimbarea minutei. Vom crește la 100.000:

start 100000

Rezultatul:

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

Înțelegem că serviciul se descurcă cu greu, dar reușește să rămână pe linia de flotare. Un astfel de volum de date (240 de milioane de rânduri pe minut) este extrem de mare, în astfel de cazuri este obișnuit să pornim mai multe clone (sau chiar zeci de clone) ale serviciului, fiecare dintre ele gestionând doar o parte din simboluri. Cu toate acestea, rezultatul este impresionant pentru un limbaj interpretat, care este orientat în primul rând pe stocarea datelor.

Ar putea apărea întrebarea de ce timpul crește neliniar odată cu dimensiunea fiecărei actualizări. Motivul este că funcția de compresie este de fapt o funcție C, care funcționează mult mai eficient decât updateAgg. Începând de la o anumită dimensiune a actualizării (în jur de 10.000), updateAgg își atinge plafonul, iar timpul său de execuție nu mai depinde de dimensiunea actualizării. Tocmai datorită pasului preliminar Q, serviciul este capabil să digere astfel de volume de date. Acest lucru subliniază cât de important este, lucrând cu date mari, să alegi algoritmul corect. Un alt aspect important este stocarea corectă a datelor în memorie. Dacă datele nu ar fi stocate pe coloane sau nu ar fi ordonate în timp, am întâlni un fenomen cunoscut sub numele de TLB cache miss – absența adresei paginii de memorie în cache-ul adreselor procesorului. Căutarea adresei durează de aproximativ 30 de ori mai mult în caz de eșec, iar în cazul datelor dispersate, poate încetini serviciul de mai multe ori.

Concluzie

În acest articol, am arătat că baza KDB+ și Q sunt potrivite nu doar pentru stocarea datelor mari și accesul simplu la acestea prin selecție, ci și pentru crearea de servicii de procesare a datelor capabile să digere sute de milioane de rânduri/ gigaocte de date chiar și într-un singur proces Q. Limbajul Q permite implementarea extrem de concisă și eficientă a algoritmilor de procesare a datelor datorită naturii sale vectoriale, interpretatorului încorporat al dialectului SQL și setului foarte reușit de funcții de bibliotecă.

Voi menționa că cele de mai sus reprezintă doar o parte din capacitățile Q; acesta are și alte caracteristici unice. De exemplu, un protocol IPC extrem de simplu, care șterge granița dintre procesele Q individuale și permite combinarea a sute de aceste procese într-o rețea unică, care poate fi dispersată pe zeci de servere din colțuri diferite ale lumii.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster