O tym, czym jest baza KDB+, język programowania Q, jakie mają mocne i słabe strony, można przeczytać w moim wcześniejszym i krótko we wprowadzeniu. W artykule zaimplementujemy na Q usługę, która będzie przetwarzać napływające dane i obliczać co minutę różne funkcje agregujące w czasie rzeczywistym (tzn. zdąży obliczyć wszystko przed kolejną porcją danych). Główną cechą Q jest to, że jest to język wektorowy, który pozwala operować nie pojedynczymi obiektami, ale ich tablicami, tablicami tablic i innymi złożonymi obiektami. Takie języki jak Q i pokrewne mu K, J, APL słyną z zwięzłości. Często program zajmujący kilka ekranów kodu w powszechnym języku typu Java można zapisać w kilku linijkach. Właśnie to chciałbym zaprezentować w tym artykule.

Wprowadzenie
KDB+ to kolumnowa baza danych, zaprojektowana do obsługi bardzo dużych wolumenów danych, uporządkowanych w określony sposób (przede wszystkim według czasu). Jest używana głównie w organizacjach finansowych - bankach, funduszach inwestycyjnych, firmach ubezpieczeniowych. Język Q to wewnętrzny język KDB+, pozwalający efektywnie pracować z tymi danymi. Ideologia Q to zwięzłość i efektywność, przy tym zrozumiałość jest poświęcana. Argumentuje się to tym, że język wektorowy i tak będzie trudny do zrozumienia, a zwięzłość i bogatość zapisu pozwala zobaczyć na jednym ekranie znacznie większą część programu, co ostatecznie ułatwia jego zrozumienie.
W artykule zaimplementujemy pełnoprawny program w Q i być może zechcesz go wypróbować. W tym celu będziesz potrzebować samego Q. Bezpłatną 32-bitową wersję możesz pobrać ze strony firmy kx - . Tam też, jeśli chcesz, znajdziesz informacje pomocnicze na temat Q, książkę i różnorodne artykuły na ten temat.
Sformułowanie zadania
Jest źródło, które co 25 milisekund przesyła tabelę z danymi. Ponieważ KDB+ jest stosowane głównie w finansach, załóżmy, że to tabela transakcji (trades), która ma następujące kolumny: time (czas w milisekundach), sym (oznaczenie firmy na giełdzie - IBM, AAPL,…), price (cena, po której kupiono akcje), size (rozmiar transakcji). Interwał 25 milisekund został wybrany losowo, nie jest ani za mały, ani za duży. Jego obecność oznacza, że dane są już buforowane w serwisie. Buforowanie po stronie serwisu, w tym dynamiczne, zależne od aktualnego obciążenia, można by łatwo zaimplementować, ale dla uproszczenia ograniczymy się do ustalonego interwału.
Serwis powinien co minutę obliczać dla każdego przychodzącego znaku z kolumny sym zestaw funkcji agregujących – max price, avg price, sum size itp. przydatne informacje. Dla uproszczenia przyjmiemy, że wszystkie funkcje można obliczać inkrementalnie, tzn. do uzyskania nowej wartości wystarczy znać dwie liczby – starą i nową. Na przykład, funkcje max, average, sum mają tę właściwość, a funkcja mediana nie.
Zakładamy także, że przychodzący strumień danych jest uporządkowany według czasu. To da nam możliwość pracy tylko z ostatnią minutą. W praktyce wystarczy umieć pracować z bieżącą i poprzednią minutą na wypadek, gdyby jakieś aktualizacje się opóźniły. Dla uproszczenia nie będziemy rozważać tego przypadku.
Funkcje agregujące
Poniżej wymienione są niezbędne funkcje agregujące. Wziąłem ich jak najwięcej, aby zwiększyć obciążenie serwisu:
- high – max price – maksymalna cena za minutę.
- low – min price – minimalna cena za minutę.
- firstPrice – first price – pierwsza cena za minutę.
- lastPrice – last price – ostatnia cena za minutę.
- firstSize – first size – pierwszy rozmiar transakcji za minutę.
- lastSize – last size – ostatni rozmiar transakcji za minutę.
- numTrades – count i – liczba transakcji za minutę.
- volume – sum size – suma rozmiarów transakcji za minutę.
- pvolume – sum price – suma cen za minutę, potrzebna do avgPrice.
- turnover – sum price*size – łączny wolumen transakcji za minutę.
- avgPrice – pvolume%numTrades – średnia cena za minutę.
- avgSize – volume%numTrades – średni rozmiar transakcji za minutę.
- vwap – turnover%volume – ważona średnia cena za minutę.
- cumVolume – sum volume – skumulowany rozmiar transakcji przez cały czas.
Na początku omówimy jeden nieoczywisty aspekt – jak zainicjować te kolumny po raz pierwszy oraz na każdą kolejną minutę. Niektóre kolumny, takie jak firstPrice, muszą być każdorazowo inicjowane wartością null, ponieważ ich wartość nie jest zdefiniowana. Inne, takie jak volume, należy zawsze ustawiać na 0. Są też kolumny, które wymagają podejścia mieszane – na przykład, cumVolume należy kopiować z poprzedniej minuty, a dla pierwszej ustawić na 0. Ustalimy wszystkie te parametry, używając typu danych słownik (odpowiednik zapisu):
// 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 объяснен ниже
Dodałem sym i time do słownika dla wygody, teraz initWith to gotowy wiersz z finalnej zaggregowanej tabeli, w której należy ustawić poprawne sym i time. Można go wykorzystać do dodawania nowych wierszy do tabeli.
aggCols będą nam potrzebne przy tworzeniu funkcji agregującej. Listę należy zainwertować z powodu specjalnych zasad obliczania wyrażeń w Q (od prawej do lewej). Celem jest zapewnienie obliczeń w kierunku od high do cumVolume, ponieważ niektóre kolumny zależą od poprzednich.
Kolumny, które należy skopiować do nowej minuty z poprzedniej, kolumna sym została dodana dla wygody:
rollColumns:`sym`cumVolume;
Teraz podzielimy kolumny na grupy w zależności od tego, jak powinny być aktualizowane. Można wyróżnić trzy typy:
- Akumulatory (volume, turnover,..) – musimy dodać wartość przychodząca do poprzedniej.
- Ze szczególnym punktem (high, low, ..) – pierwsza wartość w minucie pochodzi z danych przychodzących, pozostałe obliczane są za pomocą funkcji.
- Pozostałe. Zawsze obliczane są za pomocą funkcji.
Określmy zmienne dla tych klas:
accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;
Kolejność obliczeń
Aktualizować zaggregowaną tabelę będziemy w dwóch etapach. Dla efektywności najpierw zmniejszymy wejściową tabelę, aby pozostawiła tylko jeden wiersz dla każdego symbolu i minuty. Ponieważ wszystkie nasze funkcje są inkrementalne i asocjacyjne, gwarantuje nam to, że wynik z tego dodatkowego kroku nie ulegnie zmianie. Tabelę można by zmniejszyć za pomocą selektu:
select high:max price, low:min price … by sym,time.minute from table
Ta metoda ma jeden minus – zestaw obliczanych kolumn jest określony z góry. Na szczęście w Q selekt jest również zaimplementowany jako funkcja, do której można wprowadzić dynamicznie tworzone argumenty:
?[table;whereClause;byClause;selectClause]
Nie będę szczegółowo opisywał formatu argumentów; w naszym przypadku nie-trivialnymi będą tylko wyrażenia by i select, a one muszą być słownikami w postaci columns!expressions. W ten sposób funkcję kompresującą można zdefiniować w następujący sposób:
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 to funkcja map w Q dla jednej listy
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];
Aby to było zrozumiałe, użyłem funkcji parse, która przekształca ciąg ze wyrażeniem Q na wartość, która może być przekazana do funkcji eval i jest wymagana w selekcji funkcjonalnej. Zauważmy również, że preprocess jest zdefiniowana jako projekcja (tzn. funkcja z częściowo określonymi argumentami) funkcji select, jeden argument (tabela) jest brakujący. Jeśli zastosujemy preprocess do tabeli, to otrzymamy skompresowaną tabelę.
Drugi etap to aktualizacja tabeli agregowanej. Najpierw napiszemy algorytm w pseudokodzie:
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];
…
W Q zamiast pętli preferuje się użycie funkcji map/reduce. Ale ponieważ Q to język wektorowy i wszystkie operacje możemy spokojnie stosować do wszystkich symboli na raz, to w pierwszym przybliżeniu możemy obejść się bez pętli, wykonując operacje na wszystkich symbolach jednocześnie:
idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…
Możemy jednak pójść dalej, w Q jest unikalny i niezwykle potężny operator - operator uogólnionego przypisania. Pozwala on na zmianę zestawu wartości w złożonej strukturze danych, używając listy indeksów, funkcji i argumentów. W naszym przypadku wygląda to tak:
idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], w naszym przypadku funkcja to przypisanie
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];
Niestety, do przypisania w tabeli potrzebna jest lista wierszy, a nie kolumn, i musimy transponować macierz (lista kolumn na listę wierszy) przy użyciu funkcji flip. Dla dużej tabeli jest to kosztowne, dlatego zamiast tego zastosujemy uogólnione przypisanie do każdej kolumny osobno, używając funkcji map (która wygląda jak apostrof):
[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)]
Ponownie używamy projekcji funkcji. Zauważ, że w Q tworzenie listy również jest funkcją, a my możemy ją wywołać za pomocą funkcji each(map), aby uzyskać listę list.
Aby zestaw obliczanych kolumn nie był stały, stworzymy powyższe wyrażenie dynamicznie. Najpierw zdefiniujemy funkcje do obliczania każdej kolumny, używając zmiennych row i inp do odniesienia się do danych agregowanych i wejściowych:
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");
Niektóre kolumny są szczególne, ich pierwsza wartość nie powinna być obliczana przez funkcję. Możemy określić, że jest pierwsza według kolumny row[`numTrades] – jeśli w niej jest 0, to wartość jest pierwsza. W Q istnieje funkcja wyboru — ?[lista logiczna;lista1;lista2] – która wybiera wartość z listy 1 lub 2 w zależności od warunku w pierwszym argumencie:
// high -> ?[isFirst;inp`high;row[`high]|inp`high]
// @ - тоже обобщенное присваивание для случая когда индекс неглубокий
@[`aggExpression;specialCols;{[x;y]"?[isFirst;inp`",y,";",x,"]"};string specialCols];
Tutaj wywołałem uogólnione przypisanie z moją funkcją (wyrażenie w nawiasach klamrowych). Jest w niej przekazywana bieżąca wartość (pierwszy argument) oraz dodatkowy argument, który przekazuję w 4. parametrze.
Osobno dodamy kolumny akumulatorowe, ponieważ dla nich funkcja jest jedna i ta sama:
// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
To przypisanie jest typowe dla Q, tylko że od razu przypisuję listę wartości. W końcu stworzymy główną funkcję:
// ":",/: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),")]}";
Tym wyrażeniem dynamicznie tworzę funkcję ze stringa, który zawiera wyrażenie, które podałem powyżej. Wynik będzie wyglądał następująco:
{[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])]}
Kolejność obliczania kolumn jest odwrócona, ponieważ w Q kolejność obliczania jest od prawej do lewej.
Teraz mamy dwie podstawowe funkcje potrzebne do obliczeń, wystarczy dodać trochę infrastruktury i usługa będzie gotowa.
Ostatnie kroki
Mamy funkcje preprocess i updateAgg, które wykonują całą pracę. Ale musimy również zapewnić prawidłowe przejście przez minuty i obliczyć indeksy dla agregacji. Najpierw zdefiniujmy funkcję init:
init:{
tradeAgg:: 0#enlist[initWith]; // tworzymy pustą typizowaną tabelę, enlist przekształca słownik w tabelę, a 0# oznacza wzięcie 0 elementów z niej
currTime::00:00; // zaczynamy od 0, :: oznacza przypisanie do zmiennej globalnej
currSyms::`u#`symbol$(); // `u# - przekształca listę w drzewo, aby przyspieszyć wyszukiwanie elementów
offset::0; // indeks w tradeAgg, gdzie zaczyna się bieżąca minuta
rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // cache dla ostatnich wartości kolumn roll, tabela z kluczem sym
}
Zdefiniujemy również funkcję roll, która zmieni bieżącą minutę:
roll:{[tm]
if[currTime>tm; :init[]]; // jeśli przekroczono północ, po prostu wywołujemy init
rollCache,::offset _ rollColumns#tradeAgg; // zaktualizujemy cache – weź rolki z aggTable, przytnij, wstaw do rollCache
offset::count tradeAgg;
currSyms::`u#`$();
}
Będziemy potrzebować funkcji do dodawania nowych symboli:
addSyms:{[syms]
currSyms,::syms; // dodamy do listy znanych
// dodamy do tabeli sym, time i rollColumns korzystając ze złożonego przypisania.
// Funkcja ^ podstawia domyślne wartości dla kolumn roll, jeśli symbol nie znajduje się w cache. value flip table zwraca listę kolumn w tabeli.
`tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
}
I wreszcie funkcja upd (tradycyjna nazwa tej funkcji dla usług Q), która jest wywoływana przez klienta w celu dodania danych:
upd:{[tblName;data] // tblName nie jest nam potrzebne, ale zazwyczaj usługa obsługuje kilka tabel
tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
updMinute[data] each tm; // dodamy dane dla każdej minuty
};
updMinute:{[data;tm]
if[tmcurrTime; roll tm; currTime::tm]; // zmienimy minutę, jeśli to konieczne
data:select from data where time=tm; // filtracja
if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // nowe symbole
updateAgg[`tradeAgg;offset+currSyms?syms;data]; // zaktualizujemy skonsolidowaną tabelę. Funkcja ? szuka indeksu elementów listy po prawej stronie w liście po lewej stronie.
};
I to wszystko. Oto pełny kod naszego serwisu, jak obiecano, zaledwie kilka lini.
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];
};
Testowanie
Sprawdzimy wydajność serwisu. W tym celu uruchomimy go w osobnym procesie (umieść kod w pliku service.q) i wywołamy funkcję init:
q service.q –p 5566
q)init[]
W innej konsoli uruchom drugi proces Q i połącz się z pierwszym:
h:hopen `:host:5566
h:hopen 5566 // jeśli oba są na tym samym hoście
Najpierw utwórzmy listę symboli – 10000 sztuk i dodajmy funkcję do tworzenia losowej tabeli. W drugiej konsoli:
syms:`IBM`AAPL`GOOG,-9997?`8
rnd:{[n;t] ([] sym:n?syms; time:t+asc n#til 25; price:n?10f; size:n?10)}
Dodałem do listy symboli trzy prawdziwe, aby było łatwiej je znaleźć w tabeli. Funkcja rnd tworzy losową tabelę z n wierszami, gdzie czas zmienia się od t do t+25 milisekund.
Teraz możemy spróbować wysłać dane do serwisu (dodamy pierwsze dziesięć godzin):
{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10
Można sprawdzić w serwisie, że tabela została zaktualizowana:
c 25 200
select from tradeAgg where sym=`AAPL
-20#select from tradeAgg where sym=`AAPL
Wynik:
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|2919Przeprowadzimy teraz test obciążeniowy, aby ustalić, ile danych serwis może obsłużyć na minutę. Przypominam, że ustaliliśmy interwał aktualizacji na 25 milisekund. Odpowiednio, serwis powinien (średnio) mieścić się przynajmniej w 20 milisekundach na aktualizację, aby dać czas użytkownikom na zażądanie danych. Wprowadź następujące w drugim procesie:
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 – to dwie minuty. Można spróbować uruchomić najpierw dla 1000 wierszy co 25 milisekund:
start 1000
W moim przypadku wynik wynosi około kilku milisekund na aktualizację. Dlatego od razu zwiększę liczbę wierszy do 10.000:
start 10000
Wynik:
min| 00:00:00.004
avg| 9.191458
med| 9f
max| 00:00:00.030
Znowu nic szczególnego, a przecież to 24 miliony wierszy na minutę, 400 tysięcy na sekundę. Ponad 25 milisekund aktualizacja spowolniła tylko 5 razy, najwyraźniej przy zmianie minuty. Zwiększymy do 100.000:
start 100000
Wynik:
min| 00:00:00.013
avg| 25.11083
med| 24f
max| 00:00:00.108
q)sum times
00:02:00.532
Jak widać, serwis ledwo sobie radzi, ale mimo to udaje mu się utrzymać na powierzchni. Taka ilość danych (240 milionów wierszy na minutę) jest niesłychanie wielka, w takich przypadkach powszechnie uruchamia się kilka klonów (lub nawet dziesiątki klonów) serwisu, z których każdy przetwarza tylko część symboli. Niemniej jednak, wynik jest imponujący dla interpretowanego języka, który jest zorientowany przede wszystkim na przechowywanie danych.
Może pojawić się pytanie, dlaczego czas rośnie nieliniowo w miarę zwiększania się rozmiaru każdej aktualizacji. Powód tkwi w tym, że funkcja kompresji to w rzeczywistości funkcja C, która działa znacznie efektywniej niż updateAgg. Po osiągnięciu pewnego rozmiaru aktualizacji (około 10 000) updateAgg osiąga swój sufit, a czas jego wykonania przestaje zależeć od rozmiaru aktualizacji. Dzięki wstępnemu krokowi Q serwis jest w stanie przetwarzać takie ilości danych. To podkreśla, jak ważne jest, aby przy pracy z dużymi danymi wybierać odpowiedni algorytm. Kolejna kwestia to poprawne przechowywanie danych w pamięci. Gdyby dane były przechowywane nie w kolumnach lub nie były uporządkowane czasowo, moglibyśmy zetknąć się z problemem TLB cache miss - brakiem adresu strony pamięci w pamięci podręcznej adresów procesora. W przypadku niepowodzenia szukanie adresu zajmuje około 30 razy więcej czasu i w przypadku rozproszonych danych może spowolnić serwis kilkakrotnie.
Podsumowanie
W tym artykule pokazałem, że baza KDB+ i Q są odpowiednie nie tylko do przechowywania dużych danych i prostego dostępu do nich za pomocą zapytań, ale również do tworzenia serwisów przetwarzania danych, które mogą przetwarzać setki milionów wierszy/gigabajtów danych, nawet w jednym pojedynczym procesie Q. Sam język Q pozwala na wyjątkowo zwięzłe i efektywne wdrażanie algorytmów związanych z przetwarzaniem danych dzięki swojej wektorowej naturze, wbudowanemu interpreterowi dialektu SQL oraz bardzo udanemu zestawowi funkcji bibliotecznych.
Zauważę, że to, co powyżej, to tylko część możliwości Q, ma on także inne unikalne cechy. Na przykład niezwykle prosty protokół IPC, który zaciera granicę między poszczególnymi procesami Q i pozwala łączyć setki tych procesów w jedną sieć, która może być rozlokowana na dziesiątkach serwerów w różnych częściach świata.
Źródło: habr.com
