Q un KDB+ valodas iespējas, izmantojot reāllaika pakalpojuma piemēru

Par to, kas ir KDB+ bāze, Q programmēšanas valoda, kādas ir to stiprās un vājās puses, varat izlasīt manā iepriekšējā raksts un īsi ievadā. Rakstā uz Q ieviesīsim servisu, kas apstrādās ienākošo datu straumi un ik minūti “reālā laika” režīmā aprēķinās dažādas agregācijas funkcijas (t.i., būs laiks visu izrēķināt pirms nākamās datu daļas). Q galvenā iezīme ir tā, ka tā ir vektoru valoda, kas ļauj darboties nevis ar atsevišķiem objektiem, bet gan ar to masīviem, masīvu masīviem un citiem sarežģītiem objektiem. Tādas valodas kā Q un tās radinieki K, J, APL ir slavenas ar savu īsumu. Bieži vien programmu, kas aizņem vairākus koda ekrānus pazīstamā valodā, piemēram, Java, tajos var ierakstīt dažās rindās. Tas ir tas, ko es vēlos parādīt šajā rakstā.

Q un KDB+ valodas iespējas, izmantojot reāllaika pakalpojuma piemēru

Ievads

KDB+ ir kolonnu datubāze, kas koncentrējas uz ļoti lielu datu apjomu, kas sakārtots noteiktā veidā (galvenokārt pēc laika). To galvenokārt izmanto finanšu iestādēs - bankās, investīciju fondos, apdrošināšanas sabiedrībās. Q valoda ir KDB+ iekšējā valoda, kas ļauj efektīvi strādāt ar šiem datiem. Q ideoloģija ir īsums un efektivitāte, vienlaikus tiek upurēta skaidrība. Tas ir pamatots ar to, ka vektorvaloda jebkurā gadījumā būs grūti saprotama, un ieraksta īsums un bagātība ļauj vienā ekrānā redzēt daudz lielāku programmas daļu, kas galu galā padara to vieglāk uztveramu.

Šajā rakstā mēs ieviešam pilnvērtīgu programmu Q, un jūs varētu vēlēties to izmēģināt. Lai to izdarītu, jums būs nepieciešams faktiskais Q. Jūs varat lejupielādēt bezmaksas 32 bitu versiju uzņēmuma kx vietnē – www.kx.com. Tur, ja jūs interesē, jūs atradīsiet atsauces informāciju par Q, grāmatu Q Mirstīgajiem un dažādi raksti par šo tēmu.

Problēmas paziņojums

Ir avots, kas ik pēc 25 milisekundēm nosūta tabulu ar datiem. Tā kā KDB+ galvenokārt tiek izmantots finansēs, pieņemsim, ka šī ir darījumu (darījumu) tabula, kurā ir šādas kolonnas: laiks (laiks milisekundēs), sym (uzņēmuma apzīmējums biržā - IBM, AAPL,…), cena (cena, par kādu tika iegādātas akcijas), lielums (darījuma apjoms). 25 milisekundes intervāls ir patvaļīgs, ne pārāk mazs un ne pārāk garš. Tā klātbūtne nozīmē, ka dati pakalpojumā nonāk jau buferizēti. Būtu viegli ieviest buferizāciju servisa pusē, tostarp dinamisko buferizāciju atkarībā no pašreizējās slodzes, taču vienkāršības labad mēs koncentrēsimies uz fiksētu intervālu.

Pakalpojumam katru minūti katram ienākošajam simbolam no simbola kolonnas ir jāskaita apkopošanas funkciju kopa - maksimālā cena, vidējā cena, summas lielums utt. noderīga informācija. Vienkāršības labad pieņemsim, ka visas funkcijas var aprēķināt pakāpeniski, t.i. lai iegūtu jaunu vērtību, pietiek zināt divus skaitļus - veco un ienākošo vērtību. Piemēram, funkcijām max, vidējais, summa ir šis īpašums, bet mediānai funkcijai nav.

Mēs arī pieņemsim, ka ienākošā datu straume ir noteikta pēc laika. Tas mums dos iespēju strādāt tikai ar pēdējo brīdi. Praksē pietiek ar iespēju strādāt ar pašreizējo un iepriekšējām minūtēm gadījumā, ja daži atjauninājumi kavējas. Vienkāršības labad mēs šo gadījumu neapskatīsim.

Apkopošanas funkcijas

Tālāk ir norādītas nepieciešamās apkopošanas funkcijas. Es paņēmu pēc iespējas vairāk no tiem, lai palielinātu pakalpojuma slodzi:

  • high – max price – maksimālā cena par minūti.
  • zemā – minimālā cena – minimālā cena minūtē.
  • firstPrice – pirmā cena – pirmā cena par minūti.
  • lastPrice – pēdējā cena – pēdējā cena par minūti.
  • firstSize – pirmais izmērs – pirmais tirdzniecības lielums minūtē.
  • lastSize – pēdējais izmērs – pēdējais tirdzniecības lielums minūtē.
  • numTrades – count i – darījumu skaits minūtē.
  • apjoms – summas lielums – tirdzniecības lielumu summa minūtē.
  • pvolume – summa cena – cenu summa minūtē, kas nepieciešama avgPrice.
  • – summa apgrozījuma cena*lielums – kopējais darījumu apjoms minūtē.
  • avgPrice – pvolume%numTrades – vidējā cena minūtē.
  • avgSize – volume%numTrades – vidējais tirdzniecības apjoms minūtē.
  • vwap – apgrozījums%apjoms – vidējā cena minūtē, kas svērta pēc darījuma lieluma.
  • cumVolume – summas apjoms – uzkrātais darījumu apjoms visā laika periodā.

Tūlīt apspriedīsim vienu nepārprotamu punktu - kā inicializēt šīs kolonnas pirmo reizi un katru nākamo minūti. Dažas tipa firstPrice kolonnas katru reizi ir jāinicializē uz nulli; to vērtība nav definēta. Citiem skaļuma veidiem vienmēr jābūt iestatītiem uz 0. Ir arī kolonnas, kurām nepieciešama kombinēta pieeja - piemēram, cumVolume ir jākopē no iepriekšējās minūtes, bet pirmajam jāiestata uz 0. Iestatīsim visus šos parametrus, izmantojot vārdnīcas datus. tips (analogs ierakstam):

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

Ērtības labad vārdnīcai pievienoju sym un time, tagad initWith ir gatava rinda no galīgās apkopotās tabulas, kur atliek iestatīt pareizo sym un laiku. Varat to izmantot, lai tabulai pievienotu jaunas rindas.

Veidojot apkopošanas funkciju, mums būs nepieciešami aggCols. Saraksts ir jāapgriež, ņemot vērā secību, kādā tiek novērtētas izteiksmes Q (no labās uz kreiso). Mērķis ir nodrošināt, lai aprēķins pāriet no augsta uz cumVolume, jo dažas kolonnas ir atkarīgas no iepriekšējām.

Kolonnas, kuras jākopē uz jaunu minūti no iepriekšējās, ērtības labad tiek pievienota simbola kolonna:

rollColumns:`sym`cumVolume;

Tagad sadalīsim kolonnas grupās atkarībā no tā, kā tās būtu jāatjaunina. Var izdalīt trīs veidus:

  1. Akumulatori (apjoms, apgrozījums,..) – ienākošā vērtība jāpievieno iepriekšējai.
  2. Ar speciālu punktu (augsts, zems, ..) – pirmā vērtība minūtē tiek ņemta no ienākošajiem datiem, pārējās tiek aprēķinātas, izmantojot funkciju.
  3. Atpūta. Vienmēr aprēķina, izmantojot funkciju.

Definēsim mainīgos lielumus šīm klasēm:

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

Aprēķinu secība

Mēs atjaunināsim apkopoto tabulu divos posmos. Lai nodrošinātu efektivitāti, mēs vispirms samazinām ienākošo tabulu tā, lai katrai rakstzīmei un minūtei būtu tikai viena rinda. Tas, ka visas mūsu funkcijas ir pakāpeniskas un asociatīvas, garantē, ka šīs papildu darbības rezultāts nemainīsies. Tabulu var samazināt, izmantojot atlasi:

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

Šai metodei ir trūkums - aprēķināto kolonnu kopa ir iepriekš noteikta. Par laimi, programmā Q atlase ir ieviesta arī kā funkcija, kurā varat aizstāt dinamiski izveidotos argumentus:

?[table;whereClause;byClause;selectClause]

Sīki neaprakstīšu argumentu formātu, mūsu gadījumā tikai by un select izteiksmes būs netriviālas un tām vajadzētu būt veidlapu kolonnu!izteiksmju vārdnīcām. Tādējādi saraušanās funkciju var definēt šādi:

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

Skaidrības labad es izmantoju parsēšanas funkciju, kas pārvērš virkni ar Q izteiksmi par vērtību, kuru var nodot funkcijai eval un kas ir nepieciešama funkcijas atlasē. Ņemiet vērā arī to, ka priekšapstrāde ir definēta kā atlases funkcijas projekcija (t.i., funkcija ar daļēji definētiem argumentiem), trūkst viena argumenta (tabulas). Ja tabulai piemērosim priekšapstrādi, mēs iegūsim saspiestu tabulu.

Otrais posms ir apkopotās tabulas atjaunināšana. Vispirms uzrakstīsim algoritmu pseidokodā:

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

Programmā Q parasti tiek izmantotas kartes/samazināt funkcijas, nevis cilpas. Bet, tā kā Q ir vektoru valoda un mēs varam viegli pielietot visas darbības visiem simboliem vienlaikus, tad ar pirmo tuvinājumu mēs varam iztikt bez cilpas, veicot darbības ar visiem simboliem vienlaikus:

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

Bet mēs varam iet tālāk, Q ir unikāls un ārkārtīgi spēcīgs operators - vispārināto uzdevumu operators. Tas ļauj mainīt vērtību kopu sarežģītā datu struktūrā, izmantojot indeksu, funkciju un argumentu sarakstu. Mūsu gadījumā tas izskatās šādi:

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

Diemžēl, lai piešķirtu tabulai, jums ir nepieciešams rindu saraksts, nevis kolonnas, un jums ir jātransponē matrica (kolonnu saraksts uz rindu sarakstu), izmantojot apvēršanas funkciju. Tas ir dārgi lielai tabulai, tāpēc mēs katrai kolonnai atsevišķi piemērojam vispārinātu piešķiršanu, izmantojot kartes funkciju (kas izskatās kā apostrofs):

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

Mēs atkal izmantojam funkciju projekciju. Ņemiet vērā arī to, ka programmā Q saraksta izveide ir arī funkcija, un mēs to varam izsaukt, izmantojot funkciju every(map), lai iegūtu sarakstu sarakstu.

Lai nodrošinātu, ka aprēķināto kolonnu kopa nav fiksēta, mēs izveidosim iepriekš minēto izteiksmi dinamiski. Vispirms definēsim funkcijas katras kolonnas aprēķināšanai, izmantojot rindu un inp mainīgos, lai atsauktos uz apkopotajiem un ievades datiem:

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

Dažas kolonnas ir īpašas; funkcijai nevajadzētu aprēķināt to pirmo vērtību. Mēs varam noteikt, ka tas ir pirmais pēc kolonnas row[`numTrades] - ja tajā ir 0, tad vērtība ir pirmā. Q ir atlases funkcija — ?[Būla saraksts;saraksts1;saraksts2] —, kas atlasa vērtību no 1. vai 2. saraksta atkarībā no nosacījuma pirmajā argumentā:

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

Šeit es nosaucu vispārinātu uzdevumu ar savu funkciju (izteiksme cirtainajās lencēs). Tas saņem pašreizējo vērtību (pirmo argumentu) un papildu argumentu, kuru es nododu 4. parametrā.

Pievienosim akumulatora skaļruņus atsevišķi, jo tiem ir vienāda funkcija:

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

Tas ir parasts Q standartu piešķiršana, bet es piešķiru vērtību sarakstu uzreiz. Visbeidzot, izveidosim galveno funkciju:

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

Izmantojot šo izteiksmi, es dinamiski izveidoju funkciju no virknes, kas satur manis norādīto izteiksmi. Rezultāts izskatīsies šādi:

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

Kolonnas novērtēšanas secība ir apgriezta, jo Q vērtēšanas secība ir no labās uz kreiso pusi.

Tagad mums ir divas galvenās funkcijas, kas nepieciešamas aprēķiniem, tikai jāpievieno nedaudz infrastruktūras un pakalpojums ir gatavs.

Pēdējie soļi

Mums ir priekšapstrādes un updateAgg funkcijas, kas veic visu darbu. Bet joprojām ir nepieciešams nodrošināt pareizu pāreju pa minūtēm un aprēķināt indeksus apkopošanai. Vispirms definēsim init funkciju:

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
 }

Mēs arī definēsim ritināšanas funkciju, kas mainīs pašreizējo minūti:

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

Mums būs nepieciešama funkcija, lai pievienotu jaunas rakstzīmes:

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

Un visbeidzot, funkcija upd (tradicionālais šīs funkcijas nosaukums Q pakalpojumiem), ko klients izsauc, lai pievienotu datus:

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

Tas ir viss. Šeit ir pilns mūsu pakalpojuma kods, kā solīts, tikai dažas rindiņas:

initWith:`sym`time`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover`avgPrice`avgSize`vwap`cumVolume!(`;00:00;0n;0n;0n;0n;0N;0N;0;0;0.0;0.0;0n;0n;0n;0);
aggCols:reverse key[initWith] except `sym`time;
rollColumns:`sym`cumVolume;

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

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max price";"min price";"first price";"last price";"first size";"last size";"count i";"sum size";"sum price";"sum price*size");
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

aggExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`avgPrice`avgSize`vwap`cumVolume!("row[`high]|inp`high";"row[`low]&inp`low";"row`firstPrice";"inp`lastPrice";"row`firstSize";"inp`lastSize";"pvolume%numTrades";"volume%numTrades";"turnover%volume";"row[`cumVolume]+inp`volume");
@[`aggExpression;specialCols;{"?[isFirst;inp`",y,";",x,"]"};string specialCols];
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
updateAgg:value "{[aggTable;idx;inp] row:aggTable idx; isFirst_0=row`numTrades; .[aggTable;;:;]'[(idx;)each aggCols;(",(";"sv string[aggCols],'":",/:aggExpression aggCols),")]}"; / '

init:{
  tradeAgg::0#enlist[initWith];
  currTime::00:00;
  currSyms::`u#`symbol$();
  offset::0;
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg;
 };
roll:{[tm]
  if[currTime>tm; :init[]];
  rollCache,::offset _ rollColumns#tradeAgg;
  offset::count tradeAgg;
  currSyms::`u#`$();
 };
addSyms:{[syms]
  currSyms,::syms;
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime),(initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 };

upd:{[tblName;data] updMinute[data] each exec distinct time from data:() xkey preprocess data};
updMinute:{[data;tm]
  if[tm<>currTime; roll tm; currTime::tm];
  data:select from data where time=tm;
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms];
  updateAgg[`tradeAgg;offset+currSyms?syms;data];
 };

Testēšana

Pārbaudīsim pakalpojuma darbību. Lai to izdarītu, palaidīsim to atsevišķā procesā (ievietojiet kodu service.q failā) un izsaucam init funkciju:

q service.q –p 5566

q)init[]

Citā konsolē sāciet otro Q procesu un izveidojiet savienojumu ar pirmo:

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

Vispirms izveidosim simbolu sarakstu – 10000 XNUMX gabalu un pievienosim funkciju, lai izveidotu nejaušu tabulu. Otrajā konsolē:

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

Sarakstam pievienoju trīs reālus simbolus, lai būtu vieglāk tos meklēt tabulā. Funkcija rnd izveido nejaušu tabulu ar n rindām, kur laiks svārstās no t līdz t+25 milisekundēm.

Tagad varat mēģināt nosūtīt datus pakalpojumam (pievienojiet pirmās desmit stundas):

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

Pakalpojumā var pārbaudīt, vai tabula ir atjaunināta:

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

Rezultāts:

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

Tagad veiksim slodzes testēšanu, lai noskaidrotu, cik daudz datu pakalpojums var apstrādāt minūtē. Atgādināšu, ka mēs iestatījām atjaunināšanas intervālu uz 25 milisekundēm. Attiecīgi pakalpojumam (vidēji) vienam atjauninājumam ir jāietilpst vismaz 20 milisekundēs, lai lietotājiem būtu laiks pieprasīt datus. Otrajā procesā ievadiet:

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 ir divas minūtes. Varat mēģināt vispirms izpildīt 1000 rindas ik pēc 25 milisekundēm:

start 1000

Manā gadījumā rezultāts ir aptuveni pāris milisekundes vienā atjauninājumā. Tāpēc es nekavējoties palielināšu rindu skaitu līdz 10.000 XNUMX:

start 10000

Rezultāts:

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

Atkal nekas īpašs, bet tas ir 24 miljoni līniju minūtē, 400 tūkstoši sekundē. Vairāk nekā 25 milisekundes atjauninājums palēninājās tikai 5 reizes, acīmredzot, kad mainījās minūte. Palielināsim līdz 100.000 XNUMX:

start 100000

Rezultāts:

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

Kā redzat, serviss knapi tiek galā, bet tomēr izdodas noturēties virs ūdens. Šāds datu apjoms (240 miljoni rindu minūtē) ir ārkārtīgi liels, tādos gadījumos ierasts palaist vairākus pakalpojuma klonus (vai pat desmitiem klonu), no kuriem katrs apstrādā tikai daļu rakstzīmju. Tomēr rezultāts ir iespaidīgs interpretētajai valodai, kas galvenokārt koncentrējas uz datu glabāšanu.

Var rasties jautājums, kāpēc laiks pieaug nelineāri ar katra atjauninājuma lielumu. Iemesls ir tāds, ka saraušanās funkcija faktiski ir C funkcija, kas ir daudz efektīvāka nekā updateAgg. Sākot no noteikta atjauninājuma lieluma (apmēram 10.000 30), updateAgg sasniedz savus griestus un tad tā izpildes laiks nav atkarīgs no atjauninājuma lieluma. Tas ir saistīts ar sākotnējo darbību Q, ka pakalpojums spēj sagremot šādus datu apjomus. Tas parāda, cik svarīgi ir izvēlēties pareizo algoritmu, strādājot ar lielajiem datiem. Vēl viens punkts ir pareiza datu glabāšana atmiņā. Ja dati netiktu glabāti kolonniski vai nebūtu sakārtoti pēc laika, tad mēs iepazītos ar tādu lietu kā TLB kešatmiņas miss - atmiņas lapas adreses neesamība procesora adreses kešatmiņā. Adreses meklēšana, ja tā ir neveiksmīga, aizņem apmēram XNUMX reizes ilgāku laiku, un, ja dati ir izkaisīti, tas var vairākas reizes palēnināt pakalpojumu.

Secinājums

Šajā rakstā es parādīju, ka KDB+ un Q datubāze ir piemērota ne tikai lielu datu glabāšanai un ērtai piekļuvei tiem caur Select, bet arī tādu datu apstrādes pakalpojumu izveidei, kas spēj sagremot simtiem miljonu rindu/gigabaitu datu pat viens Q process. Pati Q valoda ļauj ārkārtīgi kodolīgi un efektīvi ieviest ar datu apstrādi saistītos algoritmus, pateicoties tās vektora raksturam, iebūvētajam SQL dialektu tulkam un ļoti veiksmīgam bibliotēkas funkciju komplektam.

Es atzīmēšu, ka iepriekš minētais ir tikai daļa no tā, ko var darīt Q, tam ir arī citas unikālas funkcijas. Piemēram, ārkārtīgi vienkāršs IPC protokols, kas dzēš robežu starp atsevišķiem Q procesiem un ļauj apvienot simtiem šo procesu vienā tīklā, kas var atrasties uz desmitiem serveru dažādās pasaules malās.

Avots: www.habr.com

Pievieno komentāru