Caracteristici ale limbajului Q și KDB+ folosind exemplul unui serviciu în timp real

Puteți citi despre baza KDB+, limbajul de programare Q, care sunt punctele lor forte și punctele slabe în documentul meu anterior articol și pe scurt în introducere. În articol, vom implementa un serviciu pe Q care va procesa fluxul de date primit și va calcula diferite funcții de agregare în fiecare minut în modul „în timp real” (adică, va avea timp să calculeze totul înainte de următoarea porțiune de date). Caracteristica principală a lui Q este că este un limbaj vectorial care vă permite să operați nu cu obiecte individuale, ci cu matricele lor, matricele de matrice și alte obiecte complexe. Limbi precum Q și rudele sale K, J, APL sunt renumite pentru concizia lor. Adesea, un program care ocupă mai multe ecrane de cod într-un limbaj familiar precum Java poate fi scris pe ele în câteva rânduri. Asta vreau să demonstrez în acest articol.

Caracteristici ale limbajului Q și KDB+ folosind exemplul unui serviciu în timp real

Introducere

KDB+ este o bază de date în coloană concentrată pe cantități foarte mari de date, ordonate într-un mod specific (în primul rând în funcție de timp). Este folosit în primul rând în instituțiile financiare - bănci, fonduri de investiții, companii de asigurări. Limba Q este limba internă a KDB+ care vă permite să lucrați eficient cu aceste date. Ideologia Q este concizia și eficiența, în timp ce claritatea este sacrificată. Acest lucru este justificat de faptul că limbajul vectorial va fi greu de înțeles în orice caz, iar concizia și bogăția înregistrării vă permit să vedeți o parte mult mai mare a programului pe un singur ecran, ceea ce în cele din urmă îl face mai ușor de înțeles.

În acest articol implementăm un program cu drepturi depline în Q și poate doriți să îl încercați. Pentru a face acest lucru, veți avea nevoie de 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 Pentru muritori și diverse articole pe această temă.

Declarație de problemă

Există o sursă care trimite un tabel cu date la fiecare 25 de milisecunde. Deoarece KDB+ este utilizat în principal în finanțe, vom presupune că acesta este un tabel de tranzacții (tranzacții), care are următoarele coloane: time (timp în milisecunde), sym (desemnarea companiei pe bursă - IBM, AAPL,…), prețul (prețul la care au fost cumpărate acțiunile), mărimea (mărimea tranzacției). Intervalul de 25 de milisecunde este arbitrar, nu prea mic și nici prea lung. Prezența acestuia înseamnă că datele vin în serviciul deja în buffer. Ar fi ușor de implementat tamponarea pe partea de service, inclusiv tamponarea dinamică în funcție de sarcina curentă, dar pentru simplitate, ne vom concentra pe un interval fix.

Serviciul trebuie să conteze fiecare minut pentru fiecare simbol de intrare din coloana sym un set de funcții de agregare - preț maxim, preț mediu, mărime sumă etc. Informatii utile. Pentru simplitate, vom presupune că toate funcțiile pot fi calculate incremental, adică. pentru a obține o nouă valoare, este suficient să cunoașteți două numere - valorile vechi și cele primite. De exemplu, funcțiile max, average, sum au această proprietate, dar funcția mediană nu.

De asemenea, vom presupune că fluxul de date primit este ordonat în timp. Acest lucru ne va oferi posibilitatea de a lucra doar cu ultimul minut. În practică, este suficient să poți lucra cu minutele curente și anterioare în cazul în care unele actualizări întârzie. Pentru simplitate, nu vom lua în considerare acest caz.

Funcții de agregare

Funcțiile de agregare necesare sunt enumerate mai jos. Am luat cât mai multe dintre ele posibil pentru a crește sarcina serviciului:

  • ridicat – preț maxim – preț maxim pe minut.
  • scăzut – preț minim – preț minim pe minut.
  • firstPrice – primul preț – primul preț pe minut.
  • lastPrice – ultimul preț – ultimul preț pe minut.
  • firstSize – prima dimensiune – prima dimensiune comercială pe minut.
  • lastSize – ultima dimensiune – ultima dimensiune a tranzacției într-un minut.
  • numTrades – count i – numărul de tranzacții pe minut.
  • volum – mărimea sumei – suma mărimii tranzacțiilor pe minut.
  • pvolume – sum price – suma prețurilor pe minut, necesară pentru avgPrice.
  • – prețul cifrei de afaceri suma *mărimea – volumul total de tranzacții pe minut.
  • avgPrice – pvolume%numTrades – preț mediu pe minut.
  • avgSize – volum%numTrades – dimensiunea medie a tranzacțiilor pe minut.
  • vwap – cifra de afaceri%volum – preț mediu pe minut ponderat în funcție de dimensiunea tranzacției.
  • cumVolume – volumul sumului – dimensiunea acumulată a tranzacțiilor de-a lungul întregului timp.

Să discutăm imediat un punct care nu este evident - cum să inițializați aceste coloane pentru prima dată și pentru fiecare minut următor. Unele coloane de tipul firstPrice trebuie inițializate la null de fiecare dată când valoarea lor este nedefinită. Alte tipuri de volum trebuie întotdeauna setate la 0. Există, de asemenea, coloane care necesită o abordare combinată - de exemplu, cumVolume trebuie copiat din minutul anterior, iar pentru primul setat la 0. Să setăm toți acești parametri folosind datele din dicționar tip (analog cu o înregistrare):

// 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 la dicționar pentru comoditate, acum initWith este o linie gata făcută din tabelul agregat final, unde rămâne să setați sym și ora corecte. Îl puteți folosi pentru a adăuga rânduri noi la un tabel.

Vom avea nevoie de aggCols când creăm o funcție de agregare. Lista trebuie inversată datorită ordinii în care sunt evaluate expresiile din Q (de la dreapta la stânga). Scopul este de a asigura că calculul merge de la mare la cumVolume, deoarece unele coloane depind de cele anterioare.

Coloanele care trebuie copiate într-un nou minut din cel precedent, coloana sym este adăugată pentru comoditate:

rollColumns:`sym`cumVolume;

Acum să împărțim coloanele în grupuri în funcție de modul în care ar trebui să fie actualizate. Se pot distinge trei tipuri:

  1. Acumulatoare (volum, cifra de afaceri,..) – trebuie să adăugăm valoarea de intrare la cea anterioară.
  2. Cu un punct special (mare, mic, ..) – prima valoare din minut este luată din datele primite, restul sunt calculate folosind funcția.
  3. Odihnă. Întotdeauna calculat folosind o funcție.

Să definim variabile pentru aceste clase:

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

Ordinea de calcul

Vom actualiza tabelul agregat în două etape. Pentru eficiență, micșorăm mai întâi tabelul de intrare, astfel încât să existe doar un rând pentru fiecare caracter și minut. Faptul că toate funcțiile noastre sunt incrementale și asociative garantează că rezultatul acestui pas suplimentar nu se va schimba. Puteți micșora tabelul folosind select:

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

Această metodă are un dezavantaj - setul de coloane calculate este predefinit. Din fericire, în Q, select este implementat și ca o funcție în care puteți înlocui argumentele create dinamic:

?[table;whereClause;byClause;selectClause]

Nu voi descrie în detaliu formatul argumentelor în cazul nostru, doar expresiile prin și select vor fi netriviale și ar trebui să fie dicționare de forme coloane!expresii. Astfel, funcția de micșorare poate fi definită după cum urmează:

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

Pentru claritate, am folosit funcția de analiză, care transformă un șir cu o expresie Q într-o valoare care poate fi transmisă funcției eval și care este necesară în funcția select. De asemenea, rețineți că preprocesul este definit ca o proiecție (adică, o funcție cu argumente parțial definite) a funcției de selectare, lipsește un argument (tabelul). Dacă aplicăm preprocesul unui tabel, vom obține un tabel comprimat.

A doua etapă este 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ă folosiți funcții map/reduce în loc de bucle. Dar, deoarece Q este un limbaj vectorial și putem aplica cu ușurință toate operațiile la toate simbolurile simultan, atunci la o primă aproximare ne putem descurca deloc fără o buclă, efectuând operații pe 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 mai departe, Q are un operator unic și extrem de puternic - operatorul de atribuire generalizată. Vă permite să schimbați un set de valori într-o structură de date complexă folosind o listă de indici, funcții și argumente. In cazul nostru arata asa:

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

Din păcate, pentru a atribui unui tabel aveți nevoie de o listă de rânduri, nu de coloane, și trebuie să transpuneți matricea (listă de coloane în listă de rânduri) folosind funcția flip. Acest lucru este costisitor pentru un tabel mare, așa că în schimb aplicăm o atribuire generalizată fiecărei coloane separat, folosind funcția hartă (care arată ca un apostrof):

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

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

Pentru a ne asigura că setul de coloane calculate nu este fix, vom crea expresia de mai sus în mod dinamic. Să definim mai întâi funcții pentru a calcula fiecare coloană, folosind variabilele rând și inp pentru a ne referi la datele agregate și 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, prima lor valoare nu trebuie calculată de funcție. Putem determina că este primul după coloana row[`numTrades] - dacă conține 0, atunci valoarea este prima. Q are o funcție select - ?[Boolean list;list1;list2] - care selectează 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 numit o atribuire generalizată cu funcția mea (o expresie între acolade). Primește valoarea curentă (primul argument) și un argument suplimentar, pe care îl transmit în al 4-lea parametru.

Să adăugăm separat difuzoarele bateriei, deoarece funcția este aceeași pentru ele:

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

Aceasta este o atribuire normală conform standardelor Q, dar atribui o listă de valori simultan. În sfârșit, să creăm 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),")]}";

Cu această expresie, creez dinamic o funcție dintr-un șir care conține expresia pe care am dat-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 evaluare a coloanei este inversată deoarece în Q ordinea de evaluare este de la dreapta la stânga.

Acum avem două funcții principale necesare pentru calcule, trebuie doar să adăugăm puțină infrastructură și serviciul este gata.

Pasi finali

Avem funcții de preprocesare și actualizare Agg care fac toată munca. Dar este totuși necesar să se asigure tranziția corectă prin minute și să se calculeze indici pentru agregare. Mai întâi de toate, să definim funcția 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
 }

Vom defini, de asemenea, funcția de rulare, care va schimba minutul curent:

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

Vom avea nevoie de o funcție pentru a adăuga caractere noi:

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

Și, în sfârșit, funcția upd (numele tradițional pentru această funcție pentru serviciile Q), care este apelată de client pentru a adăuga date:

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]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

Asta e tot. Iată codul complet al serviciului nostru, așa cum am promis, doar câteva rânduri:

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

Testarea

Să verificăm performanța serviciului. Pentru a face acest lucru, să-l rulăm într-un proces separat (puneți codul în fișierul service.q) și să apelăm funcția init:

q service.q –p 5566

q)init[]

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

h:hopen `:host:5566
h:hopen 5566 // если оба на одном хосте

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 trei simboluri reale la listă pentru a fi mai ușor de căutat în tabel. Funcția rnd creează un tabel aleator cu n rânduri, în care timpul variază de la t la t+25 milisecunde.

Acum puteți încerca să trimiteți date către serviciu (adăugați primele zece ore):

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

Puteți verifica în serviciu dacă tabelul a fost actualizat:

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

Rezultat:

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

Să efectuăm acum teste de încărcare pentru a afla câte date poate procesa serviciul pe minut. Permiteți-mi să vă reamintesc că am setat intervalul de actualizare la 25 de milisecunde. În consecință, serviciul trebuie să se încadreze (în medie) în cel puțin 20 de milisecunde per actualizare pentru a oferi utilizatorilor timp 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 înseamnă două minute. Puteți încerca să rulați 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. Deci voi crește imediat numărul de rânduri la 10.000:

start 10000

Rezultat:

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

Din nou, nimic special, dar este de 24 de milioane de linii pe minut, 400 de mii pe secundă. Pentru mai mult de 25 de milisecunde, actualizarea a încetinit doar de 5 ori, aparent când s-a schimbat minutul. Să creștem la 100.000:

start 100000

Rezultat:

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

După cum puteți vedea, serviciul abia face față, dar cu toate acestea reușește să rămână pe linia de plutire. 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ă se lanseze mai multe clone (sau chiar zeci de clone) ale serviciului, fiecare procesând doar o parte din personaje. Totuși, rezultatul este impresionant pentru un limbaj interpretat care se concentrează în primul rând pe stocarea datelor.

Poate apărea întrebarea de ce timpul crește neliniar odată cu dimensiunea fiecărei actualizări. Motivul este că funcția de micșorare este de fapt o funcție C, care este mult mai eficientă decât updateAgg. Pornind de la o anumită dimensiune a actualizării (în jur de 10.000), updateAgg își atinge plafonul și apoi timpul de execuție nu depinde de dimensiunea actualizării. Datorită pasului preliminar Q, serviciul este capabil să digere astfel de volume de date. Acest lucru evidențiază cât de important este să alegeți algoritmul potrivit atunci când lucrați cu date mari. Un alt punct este stocarea corectă a datelor în memorie. Dacă datele nu ar fi stocate în coloană sau nu au fost ordonate în funcție de timp, atunci ne-am familiariza cu un astfel de lucru precum pierderea cache-ului TLB - absența unei adrese de pagină de memorie în memoria cache a adreselor procesorului. Căutarea unei adrese durează de aproximativ 30 de ori mai mult dacă nu reușește, iar dacă datele sunt împrăștiate, poate încetini serviciul de mai multe ori.

Concluzie

În acest articol, am arătat că baza de date KDB+ și Q sunt potrivite nu numai pentru stocarea datelor mari și accesarea cu ușurință a acestora prin select, ci și pentru crearea de servicii de procesare a datelor care sunt capabile să digere sute de milioane de rânduri/gigaocteți de date chiar și în un singur proces Q. Limbajul Q în sine permite implementarea extrem de concisă și eficientă a algoritmilor legate de procesarea datelor datorită naturii sale vectoriale, interpretului de dialect SQL încorporat și un set de funcții de bibliotecă foarte reușit.

Voi observa că cele de mai sus este doar o parte din ceea ce poate face Q, are și alte caracteristici unice. De exemplu, un protocol IPC extrem de simplu care șterge granița dintre procesele Q individuale și vă permite să combinați sute de aceste procese într-o singură rețea, care poate fi localizată pe zeci de servere din diferite părți ale lumii.

Sursa: www.habr.com

Cumpărați găzduire de încredere pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără găzduire web fiabilă cu protecție DDoS, servere VPS VDS | ProHoster