Trajtoj de la lingvo Q kaj KDB+ uzante la ekzemplon de realtempa servo

Vi povas legi pri kio estas la KDB+-bazo, la programlingvo Q, kiaj estas iliaj fortoj kaj malfortoj en mia antaŭa artikolo kaj mallonge en la enkonduko. En la artikolo, ni efektivigos servon sur Q, kiu prilaboros la envenantan datumfluon kaj kalkulos diversajn agregajn funkciojn ĉiuminute en "reala tempo" reĝimo (t.e., ĝi havos tempon por kalkuli ĉion antaŭ la sekva parto de datumoj). La ĉefa trajto de Q estas, ke ĝi estas vektora lingvo, kiu ebligas al vi funkcii ne kun unuopaj objektoj, sed kun iliaj tabeloj, tabeloj kaj aliaj kompleksaj objektoj. Lingvoj kiel Q kaj ĝiaj parencoj K, J, APL estas famaj pro sia koncizeco. Ofte, programo kiu prenas plurajn ekranojn de kodo en konata lingvo kiel Java povas esti skribita sur ili en kelkaj linioj. Jen kion mi volas montri en ĉi tiu artikolo.

Trajtoj de la lingvo Q kaj KDB+ uzante la ekzemplon de realtempa servo

Enkonduko

KDB+ estas kolumna datumbazo fokusita al tre grandaj kvantoj da datumoj, ordigitaj laŭ specifa maniero (ĉefe laŭ tempo). Ĝi estas uzata ĉefe en financaj institucioj - bankoj, investaj fondusoj, asekurentreprenoj. La Q-lingvo estas la interna lingvo de KDB+, kiu permesas vin efike labori kun ĉi tiuj datumoj. La Q-ideologio estas koncizeco kaj efikeco, dum klareco estas oferita. Tion pravigas la fakto, ke la vektora lingvo ĉiukaze estos malfacile komprenebla, kaj la koncizeco kaj riĉeco de la registrado permesas vidi multe pli grandan parton de la programo sur unu ekrano, kio finfine faciligas la komprenon.

En ĉi tiu artikolo ni efektivigas plentaŭgan Q-programon kaj vi eble volas provi ĝin. Por fari tion, vi bezonos Q mem. Vi povas elŝuti la senpagan 32-bitan version en la retejo de la kompanio kx - www.kx.com. Tie, se vi interesiĝas, vi trovos referencajn informojn pri Q, la libro Q Por Mortuloj kaj diversaj artikoloj pri tiu ĉi temo.

Formulado de la problemo

Estas fonto, kiu sendas tabelon kun datumoj ĉiujn 25 milisekundojn. Ĉar KDB+ estas uzata ĉefe en financo, ni supozos, ke ĉi tio estas tabelo de transakcioj (komercoj), kiu havas la sekvajn kolumnojn: tempo (tempo en milisekundoj), sym (firmaa nomo sur la borso - IBM, AAPL,…), prezo (la prezo je kiu la akcioj estis aĉetitaj), grandeco (grandeco de la transakcio). La 25 milisekunda intervalo estas arbitra, ne tro malgranda kaj ne tro longa. Lia ĉeesto signifas, ke la datumoj venas al la servo jam bufrita. Estus facile efektivigi bufradon ĉe la serva flanko, inkluzive de dinamika bufrado depende de la nuna ŝarĝo, sed por simpleco, ni koncentriĝos pri fiksa intervalo.

La servo devas kalkuli ĉiun minuton por ĉiu envenanta simbolo de la sim-kolumno aron da agregaciaj funkcioj - maksimuma prezo, mezumprezo, sumgrandeco ktp. utilajn informojn. Por simpleco, ni supozos ke ĉiuj funkcioj povas esti kalkulitaj pliige, t.e. por akiri novan valoron, sufiĉas scii du nombrojn - la malnovan kaj la envenantajn valorojn. Ekzemple, la funkcioj max, mezumo, sumo havas ĉi tiun posedaĵon, sed la meza funkcio ne havas.

Ni ankaŭ supozos, ke la envenanta datumfluo estas tempo ordigita. Ĉi tio donos al ni la ŝancon labori nur kun la lasta minuto. En la praktiko, sufiĉas povi labori kun la nunaj kaj antaŭaj minutoj, se iuj ĝisdatigoj malfruos. Por simpleco, ni ne konsideros ĉi tiun kazon.

Agregaj funkcioj

La postulataj agregaj funkcioj estas listigitaj malsupre. Mi prenis kiel eble plej multajn el ili por pliigi la ŝarĝon de la servo:

  • alta – maksimuma prezo – maksimuma prezo por minuto.
  • low – min price – minimuma prezo por minuto.
  • firstPrice – unua prezo – unua prezo por minuto.
  • lastPrice - lasta prezo - lasta prezo por minuto.
  • firstSize - unua grandeco - unua komerca grandeco por minuto.
  • lastSize - lasta grandeco - lasta komerca grandeco en minuto.
  • numTrades - count i - nombro da komercoj por minuto.
  • volume - sum size - sumo de komercaj grandecoj por minuto.
  • pvolume - sum price - sumo de prezoj por minuto, necesa por avgPrice.
  • – suma spezoprezo*grandeco – totala volumeno de transakcioj por minuto.
  • avgPrice - pvolume%numTrades - meza prezo por minuto.
  • avgSize - volume%numTrades - meza komerca grandeco por minuto.
  • vwap - spezo%volumo - meza prezo por minuto pezbalancita de transakcia grandeco.
  • cumVolume - suma volumo - amasigita grandeco de transakcioj dum la tuta tempo.

Ni tuj diskutu unu neevideblan punkton - kiel pravalorigi ĉi tiujn kolumnojn unuafoje kaj por ĉiu posta minuto. Kelkaj kolumnoj de la firstPrice-tipo devas esti pravaligitaj al nulo ĉiufoje; ilia valoro estas nedifinita. Aliaj volumtipoj ĉiam devas esti agordita al 0. Ekzistas ankaŭ kolumnoj kiuj postulas kombinitan aliron - ekzemple, cumVolume devas esti kopiita de la antaŭa minuto, kaj por la unua agordita al 0. Ni agordu ĉiujn ĉi tiujn parametrojn uzante la vortarajn datumojn. tajpu (analoga al rekordo):

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

Mi aldonis sym kaj time al la vortaro por oportuno, nun initWith estas preta linio de la fina agregtabelo, kie restas agordi la ĝustajn sym kaj tempon. Vi povas uzi ĝin por aldoni novajn vicojn al tabelo.

Ni bezonos aggCols dum kreado de agregacia funkcio. La listo devas esti inversigita pro la ordo en kiu esprimoj en Q estas taksitaj (de dekstre al maldekstre). La celo estas certigi, ke la kalkulo iras de alta al cumVolume, ĉar iuj kolumnoj dependas de antaŭaj.

Kolumnoj kiuj devas esti kopiitaj al nova minuto de la antaŭa, la sym-kolumno estas aldonita por oportuno:

rollColumns:`sym`cumVolume;

Nun ni dividu la kolumnojn en grupojn laŭ kiel ili devus esti ĝisdatigitaj. Oni povas distingi tri tipojn:

  1. Akumuliloj (volumo, spezo,..) – ni devas aldoni la envenantan valoron al la antaŭa.
  2. Kun speciala punkto (alta, malalta, ..) - la unua valoro en la minuto estas prenita de la envenantaj datumoj, la ceteraj estas kalkulitaj per la funkcio.
  3. Ripozu. Ĉiam kalkulita per funkcio.

Ni difinu variablojn por ĉi tiuj klasoj:

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

Ordo de kalkulado

Ni ĝisdatigos la kunigitan tabelon en du etapoj. Por efikeco, ni unue ŝrumpas la envenantan tabelon tiel ke ekzistas nur unu vico por ĉiu signo kaj minuto. La fakto, ke ĉiuj niaj funkcioj estas pliigaj kaj asociaj garantias, ke la rezulto de ĉi tiu plia paŝo ne ŝanĝiĝos. Vi povus malgrandigi la tablon uzante elektu:

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

Ĉi tiu metodo havas malavantaĝon - la aro de kalkulitaj kolumnoj estas antaŭdifinita. Feliĉe, en Q, select ankaŭ estas efektivigita kiel funkcio, kie vi povas anstataŭigi dinamike kreitajn argumentojn:

?[table;whereClause;byClause;selectClause]

Mi ne detale priskribos la formaton de la argumentoj;en nia kazo, nur per kaj elektaj esprimoj estos ne bagatelaj kaj ili estu vortaroj de la formo kolumnoj!esprimoj. Tiel, la ŝrumpa funkcio povas esti difinita jene:

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

Por klareco, mi uzis la analizan funkcion, kiu transformas ĉenon kun Q-esprimo en valoron, kiu povas esti transdonita al la eval-funkcio kaj kiu estas postulata en la funkcio select. Notu ankaŭ, ke antaŭprocezo estas difinita kiel projekcio (t.e., funkcio kun parte difinitaj argumentoj) de la elekta funkcio, unu argumento (la tabelo) mankas. Se ni aplikas antaŭprocezon al tablo, ni ricevos kunpremitan tabelon.

La dua etapo ĝisdatigas la kunigitan tabelon. Ni unue skribu la algoritmon en pseŭdokodo:

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

En Q, estas ofte uzi map/redukti funkciojn anstataŭe de bukloj. Sed ĉar Q estas vektora lingvo kaj ni povas facile apliki ĉiujn operaciojn al ĉiuj simboloj samtempe, tiam al unua aproksimado ni povas tute sen buklo, farante operaciojn sur ĉiuj simboloj samtempe:

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

Sed ni povas iri plu, Q havas unikan kaj ekstreme potencan funkciigiston - la ĝeneraligitan asignofunkciigiston. Ĝi permesas vin ŝanĝi aron da valoroj en kompleksa datumstrukturo uzante liston de indeksoj, funkcioj kaj argumentoj. En nia kazo ĝi aspektas jene:

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

Bedaŭrinde, por atribui al tabelo vi bezonas liston de vicoj, ne kolumnoj, kaj vi devas transmeti la matricon (listo de kolumnoj al listo de vicoj) uzante la flip-funkcion. Ĉi tio estas multekosta por granda tabelo, do anstataŭe ni aplikas ĝeneraligitan taskon al ĉiu kolumno aparte, uzante la mapfunkcion (kiu aspektas kiel apostrofo):

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

Ni denove uzas funkcioprojekcion. Ankaŭ notu, ke en Q, krei liston ankaŭ estas funkcio kaj ni povas nomi ĝin uzante la ĉiu(mapo) funkcion por ricevi liston de listoj.

Por certigi, ke la aro de kalkulitaj kolumnoj ne estas fiksita, ni kreos la supran esprimon dinamike. Ni unue difinu funkciojn por kalkuli ĉiun kolumnon, uzante la vicon kaj inp-variablojn por raporti al la agregitaj kaj eniga datumoj:

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

Kelkaj kolumnoj estas specialaj; ilia unua valoro ne estu kalkulita per la funkcio. Ni povas determini, ke ĝi estas la unua per la vico[`numTrades] kolumno - se ĝi enhavas 0, tiam la valoro estas unua. Q havas elektan funkcion - ?[Bulea listo;listo1;listo2] - kiu elektas valoron el listo 1 aŭ 2 depende de la kondiĉo en la unua argumento:

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

Ĉi tie mi vokis ĝeneraligitan taskon kun mia funkcio (esprimo en krampoj). Ĝi ricevas la nunan valoron (la unua argumento) kaj plian argumenton, kiun mi pasas en la 4-a parametro.

Ni aldonu bateriajn laŭtparolilojn aparte, ĉar la funkcio estas la sama por ili:

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

Ĉi tio estas normala tasko laŭ Q-normoj, sed mi atribuas liston de valoroj samtempe. Fine, ni kreu la ĉefan funkcion:

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

Kun ĉi tiu esprimo, mi dinamike kreas funkcion de ĉeno kiu enhavas la esprimon, kiun mi donis supre. La rezulto aspektos tiel:

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

La kolumna taksa ordo estas inversa ĉar en Q la taksa ordo estas de dekstre maldekstren.

Nun ni havas du ĉefajn funkciojn necesajn por kalkuloj, ni nur bezonas aldoni iom da infrastrukturo kaj la servo estas preta.

Finaj paŝoj

Ni havas antaŭprocesajn kaj ĝisdatigitajn funkciojn de Agg, kiuj faras la tutan laboron. Sed ankoraŭ necesas certigi la ĝustan transiron tra minutoj kaj kalkuli indeksojn por agregado. Antaŭ ĉio, ni difinu la init-funkcion:

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
 }

Ni ankaŭ difinos la rulfunkcion, kiu ŝanĝos la nunan minuton:

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

Ni bezonos funkcion por aldoni novajn signojn:

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

Kaj finfine, la upd-funkcio (la tradicia nomo por ĉi tiu funkcio por Q-servoj), kiu estas vokita de la kliento por aldoni datumojn:

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

Tio estas ĉio. Jen la kompleta kodo de nia servo, kiel promesite, nur kelkaj linioj:

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

Testado

Ni kontrolu la agadon de la servo. Por fari tion, ni rulu ĝin en aparta procezo (metu la kodon en la dosieron service.q) kaj voku la init-funkcion:

q service.q –p 5566

q)init[]

En alia konzolo, komencu la duan Q-procezon kaj konektu al la unua:

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

Unue, ni kreu liston de simboloj - 10000 XNUMX pecoj kaj aldonu funkcion por krei hazardan tabelon. En la dua konzolo:

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

Mi aldonis tri verajn simbolojn al la listo por faciligi serĉi ilin en la tabelo. La rnd-funkcio kreas hazardan tabelon kun n vicoj, kie la tempo varias de t al t+25 milisekundoj.

Nun vi povas provi sendi datumojn al la servo (aldonu la unuajn dek horojn):

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

Vi povas kontroli en la servo, ke la tabelo estas ĝisdatigita:

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

Rezulto:

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

Ni nun faru ŝarĝtestadon por ekscii kiom da datumoj la servo povas prilabori por minuto. Mi memorigu vin, ke ni fiksis la ĝisdatigan intervalon al 25 milisekundoj. Sekve, la servo devas (averaĝe) konveni en almenaŭ 20 milisekundojn per ĝisdatigo por doni al uzantoj tempon peti datumojn. Enigu la jenon en la dua procezo:

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 estas du minutoj. Vi povas provi funkcii unue por 1000 vicoj ĉiujn 25 milisekundojn:

start 1000

En mia kazo, la rezulto estas ĉirkaŭ kelkaj milisekundoj por ĝisdatigo. Do mi tuj pliigos la nombron da vicoj al 10.000:

start 10000

Rezulto:

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

Denove, nenio speciala, sed ĉi tio estas 24 milionoj da linioj por minuto, 400 mil por sekundo. Dum pli ol 25 milisekundoj, la ĝisdatigo malrapidiĝis nur 5 fojojn, ŝajne kiam la minuto ŝanĝiĝis. Ni pliigu al 100.000:

start 100000

Rezulto:

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

Kiel vi povas vidi, la servo apenaŭ povas elteni, sed tamen ĝi sukcesas resti flosante. Tia volumo de datumoj (240 milionoj da vicoj je minuto) estas ekstreme granda; en tiaj kazoj, estas kutime lanĉi plurajn klonojn (aŭ eĉ dekduojn da klonoj) de la servo, ĉiu el kiuj prilaboras nur parton de la karakteroj. Tamen, la rezulto estas impona por interpretita lingvo, kiu fokusiĝas ĉefe al datumstokado.

La demando povas ekesti kial la tempo kreskas ne-linie kun la grandeco de ĉiu ĝisdatigo. La kialo estas, ke la ŝrumpa funkcio estas fakte C-funkcio, kiu estas multe pli efika ol updateAgg. Komencante de certa ĝisdatigo grandeco (ĉirkaŭ 10.000), updateAgg atingas sian plafonon kaj tiam ĝia ekzekuttempo ne dependas de la ĝisdatigo grandeco. Estas pro la prepara paŝo Q, ke la servo kapablas digesti tiajn volumojn da datumoj. Ĉi tio reliefigas kiom grava estas elekti la ĝustan algoritmon kiam oni laboras kun grandaj datumoj. Alia punkto estas la ĝusta konservado de datumoj en memoro. Se la datumoj ne estus stokitaj kolone aŭ ne estus ordigitaj laŭ tempo, tiam ni familiariĝus kun tia afero kiel TLB-kaŝmemoro-malsukceso - la foresto de memorpaĝa adreso en la procesoro-adreskaŝmemoro. Serĉado de adreso daŭras ĉirkaŭ 30 fojojn pli longe se malsukcesa, kaj se la datumoj estas disaj, ĝi povas bremsi la servon plurfoje.

konkludo

En ĉi tiu artikolo, mi montris, ke la datumbazo KDB+ kaj Q taŭgas ne nur por stoki grandajn datumojn kaj facile aliri ĝin per select, sed ankaŭ por krei datumtraktadservojn, kiuj kapablas digesti centojn da milionoj da vicoj/gigabajtoj da datumoj eĉ en ĉi tiu artikolo. unu ununura Q-procezo. La Q-lingvo mem ebligas ekstreme koncizan kaj efikan efektivigon de algoritmoj rilataj al datumtraktado pro sia vektora naturo, enkonstruita SQL-dialekta interpretilo kaj tre sukcesa aro de bibliotekaj funkcioj.

Mi rimarkos, ke ĉi-supra estas nur parto de la kapabloj de la Q; ĝi ankaŭ havas aliajn unikajn funkciojn. Ekzemple, ekstreme simpla IPC-protokolo, kiu forigas la limon inter individuaj Q-procezoj kaj permesas vin kombini centojn da ĉi tiuj procezoj en ununuran reton, kiu povas troviĝi sur dekoj da serviloj en malsamaj partoj de la mondo.

fonto: www.habr.com

Aldoni komenton