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

Mund të lexoni rreth asaj se çfarë janë baza e të dhënave KDB+ dhe gjuha e programimit Q, si dhe cilat janë pikat e forta dhe të dobëta të tyre në postimin tim të mëparshëm. artikull dhe shkurtimisht në hyrje. Në këtë artikull, ne do të implementojmë një shërbim në Q që do të përpunojë një rrjedhë të dhënash hyrëse dhe do të llogarisë funksione të ndryshme të agregimit minutë pas minute në "kohë reale" (d.m.th., do të ketë kohë të llogarisë gjithçka para grumbullit tjetër të të dhënave). Karakteristika kryesore e Q është se është një gjuhë vektoriale, që na lejon të operojmë jo në objekte të vetme, por në vargje prej tyre, vargje vargjesh dhe objekte të tjera komplekse. Gjuhë si Q dhe gjuhët e lidhura me të, K, J dhe APL, janë të njohura për shkurtësinë e tyre. Shpesh, një program që do të zinte disa ekrane kodi në një gjuhë të njohur si Java mund të shkruhet në disa rreshta. Kjo është pikërisht ajo që dua të demonstroj 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 në formë kolone e projektuar për vëllime shumë të mëdha të dhënash të organizuara në një mënyrë specifike (kryesisht sipas kohës). Përdoret kryesisht në institucione financiare si bankat, fondet e investimeve dhe kompanitë e sigurimeve. Q është gjuha e brendshme e KDB+, duke lejuar punë efikase me këto të dhëna. Filozofia e Q është shkurtësia dhe efikasiteti, duke sakrifikuar qartësinë. Kjo ndodh sepse një gjuhë e bazuar në vektor do të ishte e vështirë për t'u kuptuar, ndërsa shkurtësia dhe pasuria lejojnë që një pjesë shumë më e madhe e programit të shfaqet në një ekran të vetëm, duke e bërë në fund të fundit më të lehtë për t'u kuptuar.

Në këtë artikull, do të implementojmë një program të plotë në Q, dhe ndoshta do të dëshironi ta provoni. Për ta bërë këtë, do t'ju duhet vetë Q. Mund ta shkarkoni versionin falas 32-bit nga faqja e internetit e kx - www.kx.comAtje, nëse jeni të interesuar, do të gjeni edhe informacion referues mbi Q, një libër Q Për të Vdekurit dhe artikuj të ndryshëm mbi këtë temë.

Formulimi i problemit

Ekziston një burim që dërgon një tabelë të dhënash çdo 25 milisekonda. Meqenëse KDB+ përdoret kryesisht në financë, do të supozojmë se është një tabelë tregtie me kolonat e mëposhtme: koha (koha në milisekonda), sim (simboli i kompanisë në bursë - IBM, AAPL,…), çmimi (çmimi me të cilin u blenë aksionet) dhe madhësia (madhësia e transaksionit). Intervali prej 25 milisekondash u zgjodh në mënyrë arbitrare; nuk është as shumë i vogël dhe as shumë i madh. Prania e tij do të thotë që të dhënat që mbërrijnë në shërbim janë tashmë të ruajtura në memorje. Do të ishte e lehtë të zbatohej ruajtja në memorje nga ana e shërbimit, duke përfshirë ruajtjen dinamike në memorje bazuar në ngarkesën aktuale, por për thjeshtësi, do të përmbahemi me një interval të fiksuar.

Shërbimi duhet të llogarisë një grup funksionesh agregimi - çmimi maksimal, çmimi mesatar, madhësia e shumës dhe informacione të tjera të dobishme - për minutë për çdo simbol hyrës nga kolona e simboleve. Për thjeshtësi, supozojmë se të gjitha funksionet mund të llogariten në mënyrë graduale, që do të thotë se për të marrë një vlerë të re, mjaftojnë dy numra - vlera e vjetër dhe vlera hyrëse. Për shembull, funksionet maksimale, mesatare dhe shuma e kanë këtë veti, por funksioni median jo.

Gjithashtu, do të supozojmë se rrjedha e të dhënave hyrëse është e renditur sipas kohës. Kjo do të na lejojë të punojmë vetëm me minutën më të fundit. Në praktikë, mjafton të jemi në gjendje të punojmë me minutat aktuale dhe të mëparshme në rast se ndonjë përditësim është i vonuar. Për thjeshtësi, nuk do ta shqyrtojmë këtë rast.

Funksionet e agregimit

Më poshtë janë funksionet e kërkuara të agregimit. Unë përfshiva sa më shumë të ishte e mundur për të rritur ngarkesën e shërbimit:

  • i lartë – çmimi maksimal – çmimi maksimal për minutë.
  • i ulët – çmimi minimal – çmimi minimal për minutë.
  • firstPrice – çmimi i parë – çmimi i parë për minutë.
  • lastPrice – çmimi i fundit – çmimi i fundit për minutë.
  • firstSize – madhësia e parë – madhësia e parë e transaksionit për minutë.
  • lastSize – madhësia e fundit — madhësia e transaksionit të fundit për minutë.
  • numTrades – numërimi i – numri i tregtive në minutë.
  • vëllimi – shuma madhësia – shuma e madhësive të transaksioneve për minutë.
  • pvolume – shuma e çmimit – shuma e çmimeve për minutë, e kërkuar për avgPrice.
  • xhiroja – shuma çmimi*madhësia – vëllimi total i transaksioneve për minutë.
  • avgPrice – pvolume%numTrades – çmimi mesatar për minutë.
  • avgSize – vëllimi%numTrades – madhësia mesatare e tregtisë për minutë.
  • vwap – qarkullimi%vëllimi – çmimi mesatar për minutë i ponderuar sipas madhësisë së tregtisë.
  • cumVolume – shuma e vëllimit – vëllimi i akumuluar i transaksioneve gjatë gjithë periudhës.

Le të diskutojmë menjëherë një pikë jo të qartë: si t'i inicializojmë këto kolona herën e parë dhe për çdo minutë pasuese. Disa kolona, ​​siç është firstPrice, duhet të inicializohen në null çdo herë; vlera e tyre është e papërcaktuar. Të tjera, siç është volume, duhet të vendosen gjithmonë në 0. Ekzistojnë gjithashtu kolona që kërkojnë një qasje të kombinuar - për shembull, cumVolume duhet të kopjohet nga minuta e mëparshme dhe të vendoset në 0 për minutën e parë. Do t'i përcaktojmë të gjithë këta parametra duke përdorur llojin e të dhënave të fjalorit (ngjashëm 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, shtova simbolin dhe kohën në fjalor. Tani initWith është një rresht i gatshëm nga tabela përfundimtare e përmbledhur, ku mbetet të specifikohet simboli dhe koha e saktë. Mund ta përdorni për të shtuar rreshta të rinj në tabelë.

Do të na duhen aggCols kur krijojmë funksionin agregat. Lista duhet të përmbyset për shkak të rendit të vlerësimit të shprehjeve në Q (nga e djathta në të majtë). Qëllimi është të sigurohet që vlerësimi të vazhdojë nga high në cumVolume, pasi disa kolona varen nga kolonat e mëparshme.

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

rollColumns:`sym`cumVolume;

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

  1. Akumulatorët (vëllimi, xhiroja,..) – duhet ta shtojmë vlerën hyrëse te ajo e mëparshmja.
  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. Pjesa tjetër llogaritet gjithmonë duke përdorur funksionin.

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

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

Renditja e llogaritjes

Do ta përditësojmë tabelën përmbledhëse në dy hapa. Për efikasitet, së pari do ta zvogëlojmë tabelën e të dhënave në mënyrë që të përmbajë një rresht për secilin simbol dhe minutë. Fakti që të gjitha funksionet tona janë inkrementale dhe asociative garanton që rezultati nuk do të ndryshojë me këtë hap shtesë. Mund ta zvogëlojmë tabelën duke përdorur një deklaratë select:

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

Kjo metodë ka një pengesë: bashkësia e kolonave të llogaritura është e paracaktuar. Për fat të mirë, Q gjithashtu zbaton select si një funksion që pranon argumente të gjeneruara dinamikisht:

?[table;whereClause;byClause;selectClause]

Nuk do ta përshkruaj formatin e argumentit në detaje; në rastin tonë, të vetmet shprehje jo-triviale janë shprehjet by dhe select, dhe ato duhet të jenë fjalorë të shprehjeve të formës columns!. Kështu, funksioni i kompresimit 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 parse, i cili konverton një varg që përmban shprehjen Q në një vlerë që mund t'i kalohet funksionit eval dhe kërkohet në funksionin select. Gjithashtu, vini re se parapërpunimi përcaktohet si një projeksion (domethënë, një funksion me argumente pjesërisht të përcaktuara) i funksionit select; mungon një argument (tabela). Nëse e aplikojmë parapërpunimin në një tabelë, marrim një tabelë të kompresuar.

Hapi i dytë është përditësimi i tabelës së përmbledhur. Le ta 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 map/reduce në vend të sytheve. Por, meqenëse Q është një gjuhë vektoriale dhe ne mund t'i zbatojmë në mënyrë të sigurt të gjitha operacionet në të gjitha simbolet menjëherë, ne mundemi, si përafrim i parë, ta eliminojmë plotësisht ciklin duke kryer operacione në të gjitha simbolet menjëherë:

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

Por mund të shkojmë edhe më tej. Q ka një operator unik dhe jashtëzakonisht të fuqishëm - operatorin e caktimit të përgjithësuar. Ai ju lejon të modifikoni një bashkësi 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, caktimi i një tabele kërkon një listë rreshtash, jo kolonash, dhe kërkon transpozimin e matricës (lista e kolonave në një listë rreshtash) duke përdorur funksionin përmbys. Për një tabelë të madhe, kjo është e kushtueshme, kështu që në vend të kësaj, ne aplikojmë një caktim të përgjithësuar për secilën kolonë individualisht duke përdorur funksionin map (i cili duket si një apostrof):

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

Po përdorim përsëri projeksionin e funksionit. Gjithashtu, vini re se në Q, krijimi i listës është gjithashtu një funksion dhe mund ta thërrasim atë duke përdorur each(map) për të marrë një listë listash.

Për të shmangur një grup të caktuar kolonash të llogaritura, le ta krijojmë shprehjen e mësipërme në mënyrë dinamike. Së pari, përcaktoni funksione për të llogaritur secilën kolonë, duke përdorur variablat rresht dhe inp për t'iu referuar të dhënave të agreguara dhe të të dhënave hyrëse:

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ë që është e para nga kolona row[`numTrades]—nëse është 0, atëherë vlera është e para. Q ka një funksion select—?[Boolean list;list1;list2]—i cili 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 kam thirrur një caktim të përgjithshëm me funksionin tim (shprehja në kllapa kaçurrela). I është kaluar vlera aktuale (argumenti i parë) dhe një argument shtesë, të cilin e kaloj në parametrin e katërt.

Le t'i shtojmë altoparlantët me bateri veçmas, pasi ato kryejnë të njëjtin funksion:

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

Ky është një caktim tipik sipas standardeve Q, por unë po caktoj një listë vlerash menjëherë. Së fundmi, 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 dinamikisht një funksion nga një varg që përmban shprehjen që dhashë më sipër. Rezultati do të duket kështu:

{[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ë kolonave është i përmbysur, meqenëse në Q rendi i vlerësimit është nga e djathta në të majtë.

Tani kemi dy funksione kryesore të nevojshme për informatikën, e tëra çfarë mbetet është të shtojmë pak infrastrukturë dhe shërbimi është gati.

Hapat e fundit

Ne kemi funksionet parapërpunim dhe updateAgg që bëjnë të gjithë punën. Por prapëseprapë duhet të sigurojmë tranzicione të duhura midis minutave dhe të llogarisim indekset për agregimin. Së pari, le të përkufizojmë 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
 }

Gjithashtu do të përcaktojmë një funksion rrotullimi që 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ë fundmi, funksioni upd (emri tradicional i këtij funksioni 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]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

Kaq ishte. Ja kodi i plotë për shërbimin tonë, siç premtuam, 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 ta testojmë performancën e shërbimit. Për ta bërë këtë, niseni atë në një proces të veçantë (vendosni kodin në skedarin service.q) dhe thirrni funksionin init:

q service.q –p 5566

q)init[]

Në një tastierë tjetër, nisni një proces të dytë Q dhe lidheni me të parin:

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

Së pari, le të krijojmë një listë karakteresh—10000 prej tyre—dhe të shtojmë një funksion për të gjeneruar 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 kohët variojnë nga t në t+25 milisekonda.

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

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

Mund të kontrolloni në shërbim nëse 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

Tani le të kryejmë një test ngarkese për të përcaktuar se sa të dhëna mund të përpunojë shërbimi në minutë. Si kujtesë, ne e caktuam intervalin e përditësimit në 25 milisekonda. Prandaj, shërbimi duhet (mesatarisht) të përditësohet brenda të paktën 20 milisekondave 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 ta ekzekutoni fillimisht për 1000 rreshta çdo 25 milisekonda:

start 1000

Në rastin tim, rezultati është rreth disa milisekonda për përditësim. Pra, do ta 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,000 në sekondë. Përditësimi u ngadalësua vetëm për më shumë se 25 milisekonda pesë herë, me sa duket për shkak të ndryshimit të minutës. Le ta rrisim atë 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 shohim, shërbimi mezi po përballon, por prapë arrin të qëndrojë mbi ujë. Ky vëllim i të dhënave (240 milionë rreshta në minutë) është jashtëzakonisht i madh; në raste të tilla, është e zakonshme të lançohen disa klone (ose edhe dhjetëra) të shërbimit, secili duke përpunuar vetëm një nëngrup të karaktereve. Megjithatë, rezultati është mbresëlënës për një gjuhë të interpretuar që është përqendruar kryesisht në ruajtjen e të dhënave.

Dikush mund të pyesë veten pse koha rritet në mënyrë jolineare me madhësinë e çdo përditësimi. Arsyeja është se funksioni i kompresimit është në thelb 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 kufirin e tij, dhe pas kësaj, koha e ekzekutimit të tij është e pavarur nga madhësia e përditësimit. Është pikërisht për shkak të hapit paraprak Q që shërbimi është në gjendje të përpunojë vëllime të tilla të dhënash. Kjo nënvizon rëndësinë e zgjedhjes së algoritmit të duhur kur punoni me të dhëna të mëdha. Një konsideratë tjetër është ruajtja e duhur e të dhënave në memorie. Nëse të dhënat nuk do të ruheshin në kolona ose të renditura sipas kohës, do të hasnim diçka të quajtur një dështim në memorjen e përkohshme TLB - një dështim për të gjetur një adresë të faqes së memories në memorjen e përkohshme të adresës së procesorit. Kërkimet e adresave zgjasin afërsisht 30 herë më shumë nëse nuk kanë sukses, dhe në rastin e të dhënave të shpërndara, kjo mund ta ngadalësojë shërbimin disa herë.

Përfundim

Në këtë artikull, unë demonstrova se KDB+ dhe Q janë të përshtatshme jo vetëm për ruajtjen e grupeve të mëdha të të dhënave dhe aksesimin e lehtë të tyre nëpërmjet deklaratave të selektimit, por edhe për krijimin e shërbimeve të përpunimit të të dhënave të afta për të përpunuar qindra miliona rreshta/gigabajt të dhënash edhe në një proces të vetëm Q. Vetë gjuha Q lejon një zbatim jashtëzakonisht konciz dhe efikas të algoritmeve të përpunimit të të dhënave për shkak të natyrës së saj vektoriale, interpretuesit të integruar SQL dhe një grupi shumë të suksesshëm të funksioneve të bibliotekës.

Dua të theksoj se sa më sipër është vetëm një shembull i aftësive të Q; ai ka edhe karakteristika të tjera unike. Për shembull, një protokoll IPC jashtëzakonisht i thjeshtë që fshin kufijtë midis proceseve individuale të Q dhe lejon që qindra prej këtyre proceseve të lidhen në një rrjet të vetëm, i cili mund të përfshijë dhjetëra servera në të gjithë botën.

Burimi: www.habr.com

Bleni një host të besueshëm për faqet me mbrojtje DDoS, serverë VPS VDS 🔥 Bleni hosting të besueshëm të faqeve të internetit me mbrojtje DDoS, servera VPS VDS | ProHoster