Магчымасці мовы Q і KDB+ на прыкладзе сэрвісу рэальнага часу

Аб тым, што такое база KDB+, мова праграмавання Q, якія ў іх ёсць моцныя і слабыя бакі, можна прачытаць у маёй папярэдняй артыкуле і коратка ва ўводзінах. У артыкуле ж мы рэалізуем на Q сэрвіс, які будзе апрацоўваць уваходны струмень дадзеных і вылічваць штохвілінна розныя якія агрэгуюць функцыі ў рэжыме "рэальнага часу" (г.зн. будзе паспяваць усё палічыць да наступнай порцыі дадзеных). Галоўная асаблівасць Q складаецца ў тым, што гэта вектарная мова, які дазваляе апераваць не адзінкавымі аб'ектамі, а іх масівамі, масівамі масіваў і іншымі складанаскладанымі аб'ектамі. Такія мовы як Q і роднасныя яму K, J, APL знакамітыя сваёй сцісласцю. Нярэдка праграму, якая займае некалькі экранаў кода на звыклай мове тыпу Java, можна запісаць на іх у некалькі радкоў. Менавіта гэта я і хачу прадэманстраваць у гэтым артыкуле.

Магчымасці мовы Q і KDB+ на прыкладзе сэрвісу рэальнага часу

Увядзенне

KDB+ - гэта калоначная база дадзеных, арыентаваная на вельмі вялікія аб'ёмы дадзеных, спарадкаваныя пэўным чынам (у першую чаргу па часе). Выкарыстоўваецца яна, у першую чаргу, у фінансавых арганізацыях - банках, інвестыцыйных фондах, страхавых кампаніях. Мова Q – гэта ўнутраная мова KDB+, якая дазваляе эфектыўна працаваць з гэтымі дадзенымі. Ідэалогія Q - гэта сцісласць і эфектыўнасць, зразумеласць пры гэтым прыносіцца ў ахвяру. Абгрунтоўваецца гэта тым, што вектарная мова ў любым выпадку будзе складаная для ўспрымання, а сцісласць і насычанасць запісу дазваляе ўбачыць на адным экране значна большую частку праграмы, што ў выніку палягчае яе разуменне.

У артыкуле мы рэалізуем паўнавартасную праграму на Q і вам, магчыма, захочацца паспрабаваць яе ў справе. Для гэтага вам спатрэбіцца ўласна Q. Спампаваць бясплатную 32-бітную версію можна на сайце кампаніі kx - www.kx.com. Там жа, калі вам цікава, вы знойдзеце даведачную інфармацыю па Q, кнігу Q For Mortals і разнастайныя артыкулы на гэтую тэму.

Пастаноўка задачы

Ёсць крыніца, якая дасылае кожныя 25 мілісекунд табліцу з дадзенымі. Паколькі KDB+ прымяняецца ў першую чаргу ў фінансах, будзем лічыць, што гэта табліца здзелак (trades), у якой ёсць наступныя калонкі: time (час у мілісекундах), sym (абазначэнне кампаніі на біржы – IBM, AAPL,…), price (кошт, па якой набытыя акцыі), size (памер здзелкі). Інтэрвал 25 мілісекунд абраны адвольна, ён не занадта маленькі і не занадта вялікі. Яго наяўнасць азначае, што дадзеныя прыходзяць у сэрвіс ужо буферызаваныя. Можна было б лёгка рэалізаваць буферызацыю на боку сэрвісу, у тым ліку дынамічную, якая залежыць ад бягучай нагрузкі, але для прастаты спынімся на фіксаваным інтэрвале.

Сэрвіс павінен лічыць штохвілінна для кожнага ўваходнага знака з калонкі sym набор якія агрэгуюць функцый – max price, avg price, sum size і да т.п. карысную інфармацыю. Для прастаты мы пакладзем, што ўсе функцыі можна вылічаць інкрыментальна, г.зн. для атрымання новага значэння дастаткова ведаць два лікі - старое і ўваходнае значэння. Напрыклад, функцыі max, average, sum валодаюць гэтай уласцівасцю, а функцыя медыяна няма.

Таксама мы выкажам здагадку, што ўваходны струмень дадзеных спарадкаваны па часе. Гэта дасць нам магчымасць працаваць толькі з апошняй хвілінай. На практыцы дастаткова ўмець працаваць з бягучай і папярэдняй хвілінамі на выпадак, калі нейкія апдэйты запазніліся. Для прастаты мы не будзем разглядаць гэты выпадак.

Агрэгавальныя функцыі

Ніжэй пералічаны неабходныя якія агрэгуюць функцыі. Я ўзяў іх як мага больш, каб павялічыць нагрузку на сэрвіс:

  • high - max price - максімальная цана за хвіліну.
  • low - min price - мінімальная цана за хвіліну.
  • firstPrice - first price - першая цана за хвіліну.
  • lastPrice - last price - апошні кошт за хвіліну.
  • firstSize - first size - першы памер здзелкі за хвіліну.
  • lastSize - last size - апошні памер здзелкі за хвіліну.
  • numTrades - count i - колькасць здзелак за хвіліну.
  • volume - sum size - сума памераў здзелак за хвіліну.
  • pvolume - sum price - сума коштаў за хвіліну, неабходна для avgPrice.
  • turnover - sum price * size - сумарны аб'ём здзелак за хвіліну.
  • avgPrice - pvolume%numTrades - сярэдні кошт у хвіліну.
  • avgSize - volume%numTrades - сярэдні памер здзелкі за хвіліну.
  • vwap - turnover%volume - узважаны па памеры здзелкі сярэдні кошт за хвіліну.
  • cumVolume - sum volume - назапашаны памер здзелак за ўвесь час.

Адразу абмяркуем адзін невідавочны момант - як ініцыялізаваць гэтыя калонкі ў першы раз і для кожнай наступнай хвіліны. Некаторыя калонкі тыпу firstPrice кожны раз трэба ініцыялізаваць значэннем null, іх значэнне не вызначана. Іншыя тыпу volume трэба ўсталёўваць заўсёды ў 0. Яшчэ ёсць калонкі, якія патрабуюць камбінаванага падыходу - напрыклад, cumVolume неабходна капіяваць з папярэдняй хвіліны, а для першай усталяваць у 0. Задамо ўсе гэтыя параметры выкарыстоўваючы тып дадзеных слоўнік (аналаг запісу):

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

Я дадаў sym і time у слоўнік для выгоды, зараз initWith - гэта гатовы радок з фінальнай агрэгаванай табліцы, дзе засталося задаць правільныя sym і time. Можна выкарыстоўваць яе для дадання новых радкоў у табліцу.

aggCols нам спатрэбіцца пры стварэнні якая агрэгуе функцыі. Спіс неабходна інвертаваць з-за асаблівасцяў парадку вылічэнняў выразаў у Q (справа налева). Мэта - забяспечыць вылічэнне ў напрамку ад high да cumVolume, паколькі некаторыя калонкі залежаць ад папярэдніх.

Калонкі, якія трэба скапіяваць у новую хвіліну з папярэдняй, калонка sym дададзеная для зручнасці:

rollColumns:`sym`cumVolume;

Цяпер падзелім калонкі на групы паводле таго, як іх трэба абнаўляць. Можна вылучыць тры тыпы:

  1. Акумулятары (volume, turnover,..) – мы павінны дадаць уваходнае значэнне да папярэдняга.
  2. З асаблівай кропкай (high, low, ..) - першае значэнне ў хвіліне бярэцца з уваходных дадзеных, астатнія лічацца з дапамогай функцыі.
  3. Астатнія. Заўсёды лічацца з дапамогай функцыі.

Вызначым зменныя для гэтых класаў:

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

Парадак вылічэнняў

Абнаўляць агрэгаваную табліцу мы будзем у два этапы. Для эфектыўнасці мы спачатку ўціснем уваходную табліцу так, каб тамака застаўся адзін радок для кожнага знака і хвіліны. Тое, што ўсе нашыя функцыі інкрыментальныя і асацыятыўныя, гарантуе нам, што вынік ад гэтага дадатковага кроку не зменіцца. Уціснуць табліцу можна было б з дапамогай селекту:

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

У гэтага спосабу ёсць мінус - набор вылічаюцца калонак зададзены загадзя. На шчасце, у Q селект рэалізаваны і як функцыя, куды можна падставіць дынамічна створаныя аргументы:

?[table;whereClause;byClause;selectClause]

Я не буду падрабязна апісваць фармат аргументаў, у нашым выпадку нетрывіяльнымі будуць толькі by і select выразы і яны павінны быць слоўнікамі выгляду columns!expressions. Такім чынам, ужывальную функцыю можна задаць так:

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

Для зразумеласці я выкарыстоўваў функцыю parse, якая ператварае радок з Q выразам у значэнне, якое можа быць перададзена ў функцыю eval і якое патрабуецца ў функцыянальным селекце. Таксама адзначым, што preprocess зададзена як праекцыя (г.зн. функцыя з часткова вызначанымі аргументамі) функцыі селект, адзін аргумент (табліца) адсутнічае. Калі мы прыменім preprocess да табліцы, то атрымаем сціснутую табліцу.

Другі этап - гэта абнаўленне агрэгаванай табліцы. Напішам спачатку алгарытм у псеўдакодзе:

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

У Q замест цыклаў прынята выкарыстоўваць функцыі map/reduce. Але паколькі Q - вектарная мова і ўсе аперацыі мы можам спакойна ўжываць да ўсіх знакаў адразу, то ў першым набліжэнні мы можам абыйсціся наогул без цыклу, прарабляючы аперацыі са ўсімі сімваламі адразу:

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

Але мы можам пайсці і далей, у Q ёсць унікальны і выключна магутны аператар - аператар абагульненага прысвойвання. Ён дазваляе змяніць набор значэнняў у складанай структуры даных выкарыстоўваючы спіс індэксаў, функцый і аргументаў. У нашым выпадку ён выглядае так:

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

Нажаль, для прысваення ў табліцу патрэбен спіс радкоў, а не калонак, і прыходзіцца транспанаваць матрыцу (спіс калонак у спіс радкоў) з дапамогай функцыі flip. Для вялікай табліцы гэта накладна, таму замест гэтага ўжывальны абагульненае прысвойванне да кожнай калонкі асобна, выкарыстаючы функцыю map (якая выглядае як апостраф):

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

Мы зноў выкарыстоўваем праекцыю функцыі. Таксама заўважце, што ў Q стварэнне спісу - гэта таксама функцыя і мы можам выклікаць яе з дапамогай функцыі each(map), каб атрымаць спіс спісаў.

Каб набор вылічаных калонак не быў фіксаваны, створым выраз вышэй дынамічна. Вызначым спачатку функцыі для вылічэння кожнай калонкі, выкарыстоўваючы зменныя row і inp для спасылкі на агрэгаваныя і ўваходныя дадзеныя:

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

Некаторыя калонкі адмысловыя, іх першае значэнне не павінна вылічацца функцыяй. Мы можам вызначыць, што яно першае па калонцы row[`numTrades] - калі ў ёй 0, тое значэнне першае. У Q ёсць функцыя выбару -?

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

Тут я выклікаў абагульненае прысвойванне з маёй функцыяй (выраз у фігурных дужках). У яе перадаецца бягучае значэнне (першы аргумент) і дадатковы аргумент, які я перадаю ў 4-м параметры.

Асобна дадамо акумулятарныя калонкі, паколькі для іх функцыя адна і тая ж:

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

Гэта звычайнае па мерках Q прысвойванне, толькі прысвойваю я адразу спіс значэнняў. Нарэшце, створым галоўную функцыю:

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

Гэтым выразам я дынамічна ствараю функцыю з радка, які змяшчае выраз, які я прыводзіў вышэй. Вынік будзе выглядаць так:

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

Парадак вылічэнні калонак інвертаваны, паколькі ў Q парадак вылічэння справа налева.

Цяпер у нас ёсць дзве асноўныя функцыі, неабходныя для вылічэнняў, засталося дадаць крыху інфраструктуры і сэрвіс гатовы.

Фінальныя крокі

У нас ёсьць функцыі preprocess і updateAgg, якія робяць усю працу. Але неабходна яшчэ забяспечыць правільны пераход праз хвіліны і вылічыць азначнікі для агрэгацыі. У першую чаргу вызначым функцыю 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
 }

Таксама вызначым функцыю roll, якая будзе мяняць бягучую хвіліну:

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

Нам спатрэбіцца функцыя для дадання новых сімвалаў:

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

І, нарэшце, функцыя upd (традыцыйная назва гэтай функцыі для Q сэрвісаў), якая выклікаецца кліентам, для дадання дадзеных:

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

Вось і ўсё. Вось поўны код нашага сэрвісу, як і абяцалася, усяго некалькі радкоў:

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

Тэставанне

Праверым прадукцыйнасць сэрвісу. Для гэтага запусцім яго ў асобным працэсе (змясціце код у файл service.q) і выклічце функцыю init:

q service.q –p 5566

q)init[]

У іншай кансолі запусціце другі Q працэс і падлучыцеся да першага:

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

Спачатку створым спіс знакаў - 10000 штук і дадамо функцыю для стварэння выпадковай табліцы. У другой кансолі:

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

Я дадаў у спіс сімвалаў тры сапраўдныя, каб было зручней шукаць іх у табліцы. Функцыя rnd стварае выпадковую табліцу з n радкамі, дзе час мяняецца ад t да t+25 мілісекунд.

Цяпер можна паспрабаваць паслаць дадзеныя ў сэрвіс (дадамо першыя дзесяць гадзін):

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

Можна праверыць у сэрвісе, што табліца абнавілася:

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

Вынік:

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

Правядзем зараз нагрузачнае тэсціраванне, каб высветліць колькі дадзеных сэрвіс можа апрацоўваць у хвіліну. Нагадаю, што мы ўстанавілі інтэрвал для апдэйтаў у 25 мілісекунд. Адпаведна, сэрвіс павінен (у сярэднім) укладвацца хаця б у 20 мілісекунд на апдэйт, каб даць час карыстальнікам запытаць дадзеныя. Увядзіце наступнае ў другім працэсе:

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 - гэта дзве хвіліны. Можна паспрабаваць запусціць спачатку для 1000 радкоў кожныя 25 мілісекунд:

start 1000

У маім выпадку вынік атрымліваецца ў раёне пары мілісекунд на апдэйт. Так што я адразу павялічу колькасць радкоў да 10.000:

start 10000

Вынік:

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

Зноў нічога асаблівага, а гэта 24 мільёны радкоў у хвіліну, 400 тысяч у секунду. Больш за 25 мілісекунд апдэйт тармазіў толькі 5 разоў, мабыць пры змене хвіліны. Павялічым да 100.000:

start 100000

Вынік:

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

Як бачны, сэрвіс ледзь спраўляецца, але тым не менш яму ўдаецца ўтрымацца на плаву. Такі аб'ём дадзеных (240 мільёнаў радкоў у хвіліну) надзвычай вялікі, у такіх выпадках прынята запускаць некалькі клонаў (ці нават дзясяткаў клонаў) сэрвісу, кожны з якіх апрацоўвае толькі частка знакаў. Тым не менш, вынік уражлівы для інтэрпрэтаванай мовы, якая арыентаваная ў першую чаргу на захоўванне дадзеных.

Можа ўзнікнуць пытанне, чаму час расце нелінейна разам з памерам кожнага апдэйта. Чыннік у тым, што ўціскальная функцыя – гэта фактычна З функцыя, якая працуе значна больш эфектыўна updateAgg. Пачынальна з нейкага памеру апдэйта (у раёне 10.000), updateAgg дасягае сваёй столі і далей яе час выканання не залежыць ад памеру апдэйта. Менавіта за рахунак папярэдняга кроку Q сэрвіс у стане пераварваць такія аб'ёмы дадзеных. Гэта падкрэслівае, наколькі важна, працуючы з вялікімі дадзенымі, выбіраць правільны алгарытм. Яшчэ адзін момант - правільнае захоўванне дадзеных у памяці. Калі б дадзеныя захоўваліся не па-калонкава ці не былі спарадкаваны па часе, то мы б пазнаёміліся з такой рэччу, як TLB cache miss - адсутнасць адрасы старонкі памяці ў кэшы адрасоў працэсара. Пошук адраса займае дзесьці ў 30 разоў больш часу ў выпадку няўдачы і ў выпадку безуважлівых дадзеных можа запаволіць сэрвіс у некалькі разоў.

Заключэнне

У гэтым артыкуле я паказаў, што база KDB+ і Q прыдатныя не толькі для захоўвання вялікіх дадзеных і простага доступу да іх праз селект, але і для стварэння сэрвісаў апрацоўкі дадзеных, якія здольныя пераварваць сотні мільёнаў радкоў/гігабайты дадзеных нават у адным асобна ўзятым Q працэсе . Сама мова Q дазваляе выключна коратка і эфектыўна рэалізоўваць алгарытмы, злучаныя з апрацоўкай дадзеных за рахунак сваёй вектарнай прыроды, убудаванага інтэрпрэтатара дыялекту SQL і вельмі ўдалага набору бібліятэчных функцый.

Я заўважу, што выкладзенае вышэй, гэта толькі частка магчымасцяў Q, у яго ёсць і іншыя ўнікальныя асаблівасці. Напрыклад, надзвычай просты IPC пратакол, які сцірае мяжу паміж асобнымі Q працэсамі і дазваляе аб'ядноўваць сотні гэтых працэсаў у адзіную сетку, якая можа размяшчацца на дзясятках сервераў у розных канцах святла.

Крыніца: habr.com

Дадаць каментар