Karakteristikat e gjuhës Q dhe KDB+ duke përdorur shembullin e një shërbimi në kohë reale

Ju mund të lexoni se cilat janë baza e KDB+, gjuha e programimit Q, cilat janë pikat e forta dhe të dobëta të tyre në të mëparshmen time artikull dhe shkurtimisht në hyrje. Në artikull, ne do të zbatojmë një shërbim në Q që do të përpunojë rrjedhën e të dhënave hyrëse dhe do të llogarisë funksione të ndryshme grumbullimi çdo minutë në modalitetin "kohë reale" (d.m.th., do të ketë kohë për të llogaritur gjithçka përpara pjesës tjetër të të dhënave). Karakteristika kryesore e Q është se ajo është një gjuhë vektoriale që ju lejon të operoni jo me objekte të vetme, por me vargjet e tyre, grupe vargjesh dhe objekte të tjera komplekse. Gjuhë të tilla si Q dhe të afërmit e saj K, J, APL janë të famshme për shkurtësinë e tyre. Shpesh, një program që merr disa ekrane kodi në një gjuhë të njohur si Java, mund të shkruhet në to në disa rreshta. Kjo është ajo që dua të tregoj në këtë artikull.

Karakteristikat e gjuhës Q dhe KDB+ duke përdorur shembullin e një shërbimi në kohë reale

Paraqitje

KDB+ është një bazë të dhënash kolone e fokusuar në sasi shumë të mëdha të dhënash, të renditura në një mënyrë specifike (kryesisht sipas kohës). Përdoret kryesisht në institucionet financiare - banka, fondet e investimeve, kompanitë e sigurimeve. Gjuha Q është gjuha e brendshme e KDB+ që ju lejon të punoni në mënyrë efektive me këto të dhëna. Ideologjia Q është shkurtësia dhe efikasiteti, ndërsa qartësia sakrifikohet. Kjo justifikohet me faktin se gjuha e vektorit do të jetë e vështirë për t'u kuptuar në çdo rast, dhe shkurtësia dhe pasuria e regjistrimit ju lejon të shihni një pjesë shumë më të madhe të programit në një ekran, gjë që përfundimisht e bën më të lehtë për t'u kuptuar.

Në këtë artikull ne implementojmë një program të plotë në Q dhe mund të dëshironi ta provoni. Për ta bërë këtë, do t'ju duhet Q-ja aktuale. Mund të shkarkoni versionin 32-bit falas në faqen e internetit të kompanisë kx – www.kx.com. Aty, nëse jeni të interesuar, do të gjeni informacion referencë për librin Q Q Për të Vdekshmit dhe artikuj të ndryshëm mbi këtë temë.

Formulimi i problemit

Ekziston një burim që dërgon një tabelë me të dhëna çdo 25 milisekonda. Meqenëse KDB+ përdoret kryesisht në financa, do të supozojmë se kjo është një tabelë e transaksioneve (tregtimeve), e cila ka kolonat e mëposhtme: kohë (koha në milisekonda), sym (përcaktimi i kompanisë në bursë - IBM, AAPL,…), çmimi (çmimi me të cilin janë blerë aksionet), madhësia (madhësia e transaksionit). Intervali 25 milisekonda është arbitrar, jo shumë i vogël dhe jo shumë i gjatë. Prania e tij do të thotë që të dhënat vijnë në shërbim tashmë të buferuara. Do të ishte e lehtë të zbatohej bufferimi në anën e shërbimit, duke përfshirë buferimin dinamik në varësi të ngarkesës aktuale, por për thjeshtësi, ne do të fokusohemi në një interval fiks.

Shërbimi duhet të numërojë çdo minutë për çdo simbol hyrës nga kolona Sym një grup funksionesh grumbulluese - çmimi maksimal, çmimi mesatar, madhësia e shumës, etj. informacione të dobishme. Për thjeshtësi, do të supozojmë se të gjitha funksionet mund të llogariten në mënyrë incrementale, d.m.th. për të marrë një vlerë të re, mjafton të njihni dy numra - vlerat e vjetra dhe ato hyrëse. Për shembull, funksionet max, mesatare, shuma e kanë këtë veti, por funksioni mesatar jo.

Ne gjithashtu do të supozojmë se rryma e të dhënave hyrëse është e porositur në kohë. Kjo do të na japë mundësinë të punojmë vetëm me minutën e fundit. Në praktikë, mjafton të mund të punosh me minutat aktuale dhe ato të mëparshme në rast se disa përditësime vonohen. Për thjeshtësi, ne nuk do ta shqyrtojmë këtë rast.

Funksionet e grumbullimit

Funksionet e kërkuara të grumbullimit janë renditur më poshtë. Kam marrë sa më shumë prej tyre për të rritur ngarkesën në shërbim:

  • i lartë – çmimi maksimal – çmimi maksimal për minutë.
  • i ulët – çmimi minimal – çmimi minimal për minutë.
  • Çmimi i parë – çmimi i parë – çmimi i parë për minutë.
  • çmimi i fundit – çmimi i fundit – çmimi i fundit për minutë.
  • firstSize - madhësia e parë - madhësia e parë e tregtisë për minutë.
  • lastSize - madhësia e fundit - madhësia e fundit e tregtisë në një minutë.
  • numTrades – numëroni i – numri i tregtimeve në minutë.
  • vëllimi – madhësia e shumës – shuma e madhësive të tregtisë për minutë.
  • pvolume – shuma e çmimit – shuma e çmimeve për minutë, e kërkuar për çmimin mesatar.
  • – shuma e çmimit të qarkullimit*madhësia – vëllimi total i transaksioneve në minutë.
  • avgÇmimi – pvolume%numTregtare – çmimi mesatar për minutë.
  • avgSize – vëllim%numTregtjet – madhësia mesatare e tregtisë për minutë.
  • vwap – qarkullim%volum – çmimi mesatar për minutë i ponderuar sipas madhësisë së transaksionit.
  • cumVolume – shuma e vëllimit – madhësia e akumuluar e transaksioneve gjatë gjithë kohës.

Le të diskutojmë menjëherë një pikë jo të dukshme - si të inicializohen këto kolona për herë të parë dhe për çdo minutë pasuese. Disa kolona të llojit firstPrice duhet të inicializohen në null çdo herë; vlera e tyre është e papërcaktuar. Llojet e tjera të vëllimit duhet të vendosen gjithmonë në 0. Ka edhe kolona që kërkojnë një qasje të kombinuar - për shembull, cumVolume duhet të kopjohet nga minuta e mëparshme, dhe për të parën vendoset në 0. Le t'i vendosim të gjithë këta parametra duke përdorur të dhënat e fjalorit lloji (analog me një rekord):

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

Për lehtësi, i shtova fjalorit sym dhe kohë, tani initWith është një rresht i gatshëm nga tabela përfundimtare e grumbulluar, ku mbetet për të vendosur simbolin dhe kohën e duhur. Mund ta përdorni për të shtuar rreshta të rinj në një tabelë.

Do të na duhen aggCols kur krijojmë një funksion grumbullimi. Lista duhet të përmbyset për shkak të renditjes në të cilën vlerësohen shprehjet në Q (nga e djathta në të majtë). Qëllimi është të sigurohet që llogaritja të shkojë nga e larta në cumVolume, pasi disa kolona varen nga ato të mëparshme.

Kolonat që duhet të kopjohen në një minutë të re nga ajo e mëparshme, kolona sym shtohet për lehtësi:

rollColumns:`sym`cumVolume;

Tani le t'i ndajmë kolonat në grupe sipas mënyrës se si duhet të përditësohen. Mund të dallohen tre lloje:

  1. Akumulatorët (vëllimi, qarkullimi,..) - duhet t'i shtojmë vlerën hyrëse asaj të mëparshme.
  2. Me një pikë të veçantë (e lartë, e ulët, ..) - vlera e parë në minutë merret nga të dhënat hyrëse, pjesa tjetër llogaritet duke përdorur funksionin.
  3. Pushoni. Gjithmonë llogaritet duke përdorur një funksion.

Le të përcaktojmë variabla për këto klasa:

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

Rendi i llogaritjes

Ne do të përditësojmë tabelën e përmbledhur në dy faza. Për efikasitet, ne fillimisht zvogëlojmë tabelën hyrëse në mënyrë që të ketë vetëm një rresht për çdo karakter dhe minutë. Fakti që të gjitha funksionet tona janë rritëse dhe shoqëruese garanton që rezultati i këtij hapi shtesë nuk do të ndryshojë. Ju mund ta zvogëloni tabelën duke përdorur zgjidhni:

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

Kjo metodë ka një disavantazh - grupi i kolonave të llogaritura është i paracaktuar. Për fat të mirë, në Q, zgjidhni zbatohet gjithashtu si një funksion ku mund të zëvendësoni argumentet e krijuara në mënyrë dinamike:

?[table;whereClause;byClause;selectClause]

Nuk do të përshkruaj në detaje formatin e argumenteve; në rastin tonë, vetëm shprehjet me dhe selekto do të jenë joparëndësishme dhe ato duhet të jenë fjalorë të shprehjeve të kolonave të formës. Kështu, funksioni i tkurrjes mund të përcaktohet si më poshtë:

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

Për qartësi, përdora funksionin e analizës, i cili kthen një varg me një shprehje Q në një vlerë që mund t'i kalohet funksionit eval dhe që kërkohet në funksionin e përzgjedhjes. Vini re gjithashtu se paraprocesi është përcaktuar si një projeksion (d.m.th., një funksion me argumente të përcaktuara pjesërisht) të funksionit të përzgjedhur, një argument (tabela) mungon. Nëse aplikojmë paraprocesin në një tabelë, do të marrim një tabelë të ngjeshur.

Faza e dytë është përditësimi i tabelës së përmbledhur. Le të shkruajmë së pari algoritmin në pseudokod:

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, është e zakonshme të përdoren funksionet e hartës/reduktimit në vend të sytheve. Por meqenëse Q është një gjuhë vektoriale dhe ne mund t'i zbatojmë lehtësisht të gjitha veprimet për të gjitha simbolet në të njëjtën kohë, atëherë në një përafrim të parë mund të bëjmë pa një lak fare, duke kryer veprime në të gjitha simbolet në të njëjtën kohë:

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

Por ne mund të shkojmë më tej, Q ka një operator unik dhe jashtëzakonisht të fuqishëm - operatorin e përgjithshëm të caktimit. Kjo ju lejon të ndryshoni një grup vlerash në një strukturë komplekse të dhënash duke përdorur një listë indeksesh, funksionesh dhe argumentesh. Në rastin tonë duket kështu:

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

Fatkeqësisht, për të caktuar një tabelë ju nevojitet një listë rreshtash, jo kolonash, dhe ju duhet të transpozoni matricën (lista e kolonave në listën e rreshtave) duke përdorur funksionin Flip. Kjo është e shtrenjtë për një tabelë të madhe, kështu që ne aplikojmë një detyrë të përgjithësuar për secilën kolonë veç e veç, duke përdorur funksionin e hartës (i cili duket si një apostrof):

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

Ne përsëri përdorim projeksionin e funksionit. Gjithashtu vini re se në Q, krijimi i një liste është gjithashtu një funksion dhe ne mund ta quajmë atë duke përdorur funksionin every(map) për të marrë një listë të listave.

Për të siguruar që grupi i kolonave të llogaritura nuk është i fiksuar, ne do të krijojmë shprehjen e mësipërme në mënyrë dinamike. Le të përcaktojmë së pari funksionet për të llogaritur secilën kolonë, duke përdorur variablat e rreshtit dhe inp për t'iu referuar të dhënave të grumbulluara dhe të dhëna:

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

Disa kolona janë të veçanta; vlera e tyre e parë nuk duhet të llogaritet nga funksioni. Mund të përcaktojmë se është i pari nga kolona e rreshtit[`numTrades] - nëse përmban 0, atëherë vlera është e para. Q ka një funksion të përzgjedhjes - ?[lista Boolean;list1;list2] - e cila zgjedh një vlerë nga lista 1 ose 2 në varësi të kushtit në argumentin e parë:

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

Këtu thirra një detyrë të përgjithësuar me funksionin tim (një shprehje në kllapa kaçurrelë). Ai merr vlerën aktuale (argumenti i parë) dhe një argument shtesë, të cilin e kaloj në parametrin e 4-të.

Le të shtojmë altoparlantët e baterisë veçmas, pasi funksioni është i njëjtë për ta:

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

Kjo është një detyrë normale sipas standardeve Q, por unë po caktoj një listë vlerash menjëherë. Së fundi, le të krijojmë funksionin kryesor:

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

Me këtë shprehje, unë krijoj në mënyrë dinamike një funksion nga një varg që përmban shprehjen që dhashë më sipër. Rezultati do të duket si ky:

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

Rendi i vlerësimit të kolonës është i përmbysur sepse në Q renditja e vlerësimit është nga e djathta në të majtë.

Tani kemi dy funksione kryesore të nevojshme për llogaritjet, duhet vetëm të shtojmë pak infrastrukturë dhe shërbimi është gati.

Hapat e fundit

Ne kemi funksionet e parapërpunimit dhe përditësimit të Agg që bëjnë të gjithë punën. Por është ende e nevojshme të sigurohet kalimi i saktë përmes minutave dhe të llogariten indekset për grumbullim. Para së gjithash, le të përcaktojmë funksionin 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
 }

Ne gjithashtu do të përcaktojmë funksionin e rrotullimit, i cili do të ndryshojë minutën aktuale:

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

Do të na duhet një funksion për të shtuar karaktere të reja:

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

Dhe së fundi, funksioni upd (emri tradicional për këtë funksion për shërbimet Q), i cili thirret nga klienti për të shtuar të dhëna:

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

Kjo eshte e gjitha. Këtu është kodi i plotë i shërbimit tonë, siç është premtuar, vetëm disa rreshta:

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

Testimi

Le të kontrollojmë performancën e shërbimit. Për ta bërë këtë, le ta ekzekutojmë atë në një proces të veçantë (vendosni kodin në skedarin service.q) dhe thërrasim funksionin init:

q service.q –p 5566

q)init[]

Në një tastierë tjetër, filloni procesin e dytë Q dhe lidheni me të parën:

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

Së pari, le të krijojmë një listë simbolesh - 10000 copë dhe të shtojmë një funksion për të krijuar një tabelë të rastësishme. Në konsolën e dytë:

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

Shtova tre simbole reale në listë për ta bërë më të lehtë kërkimin e tyre në tabelë. Funksioni rnd krijon një tabelë të rastësishme me n rreshta, ku koha ndryshon nga t në t+25 milisekonda.

Tani mund të provoni të dërgoni të dhëna në shërbim (shtoni dhjetë orët e para):

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

Mund të kontrolloni në shërbim që tabela është përditësuar:

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

Rezultati:

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

Le të kryejmë tani testimin e ngarkesës për të zbuluar se sa të dhëna mund të përpunojë shërbimi në minutë. Më lejoni t'ju kujtoj se kemi vendosur intervalin e përditësimit në 25 milisekonda. Prandaj, shërbimi duhet (mesatarisht) të përshtatet në të paktën 20 milisekonda për përditësim për t'u dhënë përdoruesve kohë për të kërkuar të dhëna. Futni sa vijon në procesin e dytë:

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 është dy minuta. Mund të provoni të vraponi së pari për 1000 rreshta çdo 25 milisekonda:

start 1000

Në rastin tim, rezultati është rreth disa milisekonda për përditësim. Kështu që unë do të rris menjëherë numrin e rreshtave në 10.000:

start 10000

Rezultati:

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

Përsëri, asgjë e veçantë, por kjo është 24 milionë rreshta në minutë, 400 mijë në sekondë. Për më shumë se 25 milisekonda, përditësimi u ngadalësua vetëm 5 herë, me sa duket kur minuta ndryshoi. Le të rritemi në 100.000:

start 100000

Rezultati:

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

Siç mund ta shihni, shërbimi mezi ia del mbanë, por megjithatë arrin të qëndrojë në këmbë. Një vëllim i tillë i të dhënave (240 milion rreshta në minutë) është jashtëzakonisht i madh; në raste të tilla, është e zakonshme të lëshohen disa klone (ose edhe dhjetëra klone) të shërbimit, secila prej të cilave përpunon vetëm një pjesë të personazheve. Megjithatë, rezultati është mbresëlënës për një gjuhë të interpretuar që fokusohet kryesisht në ruajtjen e të dhënave.

Mund të lindë pyetja se pse koha rritet në mënyrë jolineare me madhësinë e çdo përditësimi. Arsyeja është se funksioni shrink është në fakt një funksion C, i cili është shumë më efikas se updateAgg. Duke filluar nga një madhësi e caktuar përditësimi (rreth 10.000), updateAgg arrin tavanin e tij dhe më pas koha e ekzekutimit të tij nuk varet nga madhësia e përditësimit. Është për shkak të hapit paraprak Q që shërbimi është në gjendje të tresë vëllime të tilla të dhënash. Kjo nënvizon se sa e rëndësishme është të zgjidhni algoritmin e duhur kur punoni me të dhëna të mëdha. Një pikë tjetër është ruajtja e saktë e të dhënave në memorie. Nëse të dhënat nuk do të ruheshin në mënyrë kolone ose nuk do të porositeshin me kohë, atëherë do të njiheshim me një gjë të tillë si humbja e cache-it TLB - mungesa e një adrese faqeje memorie në cache-in e adresave të procesorit. Kërkimi për një adresë zgjat rreth 30 herë më shumë nëse nuk ka sukses, dhe nëse të dhënat shpërndahen, mund të ngadalësojë shërbimin disa herë.

Përfundim

Në këtë artikull, unë tregova se baza e të dhënave KDB+ dhe Q janë të përshtatshme jo vetëm për ruajtjen e të dhënave të mëdha dhe aksesin lehtësisht të tyre përmes përzgjedhjes, por edhe për krijimin e shërbimeve të përpunimit të të dhënave që janë në gjendje të tresin qindra miliona rreshta/gigabajt të dhënash edhe në një proces i vetëm Q. Vetë gjuha Q lejon zbatimin jashtëzakonisht konciz dhe efikas të algoritmeve që lidhen me përpunimin e të dhënave për shkak të natyrës së saj vektoriale, interpretuesit të integruar të dialektit SQL dhe një grupi shumë të suksesshëm funksionesh bibliotekare.

Do të vërej se sa më sipër është vetëm një pjesë e aftësive të Q; ai gjithashtu ka veçori të tjera unike. Për shembull, një protokoll jashtëzakonisht i thjeshtë IPC që fshin kufirin midis proceseve individuale Q dhe ju lejon të kombinoni qindra nga këto procese në një rrjet të vetëm, i cili mund të vendoset në dhjetëra serverë në pjesë të ndryshme të botës.

Burimi: www.habr.com

Shto një koment