Аб тым, што такое база KDB+, мова праграмавання Q, якія ў іх ёсць моцныя і слабыя бакі, можна прачытаць у маёй папярэдняй
Увядзенне
KDB+ - гэта калоначная база дадзеных, арыентаваная на вельмі вялікія аб'ёмы дадзеных, спарадкаваныя пэўным чынам (у першую чаргу па часе). Выкарыстоўваецца яна, у першую чаргу, у фінансавых арганізацыях - банках, інвестыцыйных фондах, страхавых кампаніях. Мова Q – гэта ўнутраная мова KDB+, якая дазваляе эфектыўна працаваць з гэтымі дадзенымі. Ідэалогія Q - гэта сцісласць і эфектыўнасць, зразумеласць пры гэтым прыносіцца ў ахвяру. Абгрунтоўваецца гэта тым, што вектарная мова ў любым выпадку будзе складаная для ўспрымання, а сцісласць і насычанасць запісу дазваляе ўбачыць на адным экране значна большую частку праграмы, што ў выніку палягчае яе разуменне.
У артыкуле мы рэалізуем паўнавартасную праграму на Q і вам, магчыма, захочацца паспрабаваць яе ў справе. Для гэтага вам спатрэбіцца ўласна Q. Спампаваць бясплатную 32-бітную версію можна на сайце кампаніі kx -
Пастаноўка задачы
Ёсць крыніца, якая дасылае кожныя 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;
Цяпер падзелім калонкі на групы паводле таго, як іх трэба абнаўляць. Можна вылучыць тры тыпы:
- Акумулятары (volume, turnover,..) – мы павінны дадаць уваходнае значэнне да папярэдняга.
- З асаблівай кропкай (high, low, ..) - першае значэнне ў хвіліне бярэцца з уваходных дадзеных, астатнія лічацца з дапамогай функцыі.
- Астатнія. Заўсёды лічацца з дапамогай функцыі.
Вызначым зменныя для гэтых класаў:
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