Vlastnosti jazyka Q a KDB+ na příkladu služby v reálném čase

O tom, co je základ KDB+, programovací jazyk Q, jaké jsou jejich silné a slabé stránky, si můžete přečíst v mém předchozím článek a krátce v úvodu. V článku implementujeme službu na Q, která bude zpracovávat příchozí datový tok a každou minutu v režimu „real time“ vypočítávat různé agregační funkce (tj. stihne vše spočítat před další porcí dat). Hlavním rysem Q je, že se jedná o vektorový jazyk, který vám umožňuje pracovat nikoli s jednotlivými objekty, ale s jejich poli, poli polí a dalšími komplexními objekty. Jazyky jako Q a jeho příbuzné K, J, APL jsou známé svou stručností. Program, který zabírá několik obrazovek kódu ve známém jazyce, jako je Java, lze na nich často napsat v několika řádcích. To je to, co chci demonstrovat v tomto článku.

Vlastnosti jazyka Q a KDB+ na příkladu služby v reálném čase

úvod

KDB+ je sloupcová databáze zaměřená na velmi velká množství dat, uspořádaných specifickým způsobem (především podle času). Používá se především ve finančních institucích – banky, investiční fondy, pojišťovny. Jazyk Q je interní jazyk KDB+, který umožňuje efektivně pracovat s těmito daty. Ideologie Q je stručnost a efektivita, zatímco jasnost je obětována. To je odůvodněno tím, že vektorový jazyk bude v každém případě těžko srozumitelný a stručnost a bohatost záznamu umožňuje vidět mnohem větší část programu na jedné obrazovce, což v konečném důsledku usnadňuje jeho pochopení.

V tomto článku implementujeme plnohodnotný program v Q a možná ho budete chtít vyzkoušet. K tomu budete potřebovat skutečný Q. Bezplatnou 32bitovou verzi si můžete stáhnout na webu společnosti kx – www.kx.com. Tam, pokud máte zájem, najdete referenční informace o Q, knize Q Pro smrtelníky a různé články na toto téma.

Formulace problému

Existuje zdroj, který každých 25 milisekund odesílá tabulku s daty. Jelikož se KDB+ využívá především ve financích, budeme předpokládat, že se jedná o tabulku transakcí (obchodů), která má tyto sloupce: čas (čas v milisekundách), sym (označení společnosti na burze - IBM, AAPL,…), cena (cena, za kterou byly akcie nakoupeny), velikost (velikost transakce). Interval 25 milisekund je libovolný, není příliš malý ani příliš dlouhý. Jeho přítomnost znamená, že data přicházejí do služby již ve vyrovnávací paměti. Bylo by snadné implementovat buffering na straně služby, včetně dynamického bufferingu v závislosti na aktuální zátěži, ale pro jednoduchost se zaměříme na pevný interval.

Služba musí každou minutu počítat pro každý příchozí symbol ze sloupce sym sadu agregačních funkcí – max. cena, průměrná cena, velikost součtu atd. užitečné informace. Pro jednoduchost budeme předpokládat, že všechny funkce lze vypočítat inkrementálně, tzn. pro získání nové hodnoty stačí znát dvě čísla - staré a příchozí hodnoty. Tuto vlastnost mají například funkce max, průměr, součet, ale funkce medián nikoli.

Budeme také předpokládat, že příchozí datový tok je časově uspořádaný. To nám dá příležitost pracovat pouze na poslední chvíli. V praxi stačí umět pracovat s aktuální a předchozí minutou v případě, že se některé aktualizace opozdí. Pro jednoduchost nebudeme tento případ uvažovat.

Agregační funkce

Požadované agregační funkce jsou uvedeny níže. Vzal jsem jich co nejvíce, abych zvýšil zatížení služby:

  • vysoká – maximální cena – maximální cena za minutu.
  • nízká – minimální cena – minimální cena za minutu.
  • firstPrice – první cena – první cena za minutu.
  • lastPrice – poslední cena – poslední cena za minutu.
  • firstSize – první velikost – první velikost obchodu za minutu.
  • lastSize – poslední velikost – poslední velikost obchodu za minutu.
  • numTrades – count i – počet obchodů za minutu.
  • objem – velikost součtu – součet velikostí obchodů za minutu.
  • pvolume – sum price – suma cen za minutu, požadovaná pro avgPrice.
  • – suma obratu cena*velikost – celkový objem transakcí za minutu.
  • avgPrice – pvolume%numTrades – průměrná cena za minutu.
  • avgSize – volume%numTrades – průměrná velikost obchodu za minutu.
  • vwap – obrat%objem – průměrná cena za minutu vážená velikostí transakce.
  • cumVolume – součtový objem – kumulovaná velikost transakcí za celou dobu.

Okamžitě probereme jeden nezřejmý bod – jak tyto sloupce inicializovat poprvé a pro každou další minutu. Některé sloupce typu firstPrice musí být pokaždé inicializovány na hodnotu null, jejich hodnota není definována. Ostatní typy svazků musí být vždy nastaveny na 0. Existují také sloupce, které vyžadují kombinovaný přístup - například cumVolume je třeba zkopírovat z předchozí minuty a pro první nastavit na 0. Nastavme všechny tyto parametry pomocí údajů ze slovníku typ (analogicky k záznamu):

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

Do slovníku jsem pro pohodlí přidal sym a time, nyní je initWith hotový řádek z finální agregované tabulky, kde zbývá nastavit správný sym a čas. Můžete jej použít k přidání nových řádků do tabulky.

Při vytváření agregační funkce budeme potřebovat aggCols. Seznam musí být invertován kvůli pořadí, ve kterém jsou výrazy v Q vyhodnocovány (zprava doleva). Cílem je zajistit, aby výpočet přešel z vysoké na cumVolume, protože některé sloupce závisí na předchozích.

Sloupce, které je třeba zkopírovat do nové minuty z předchozí, je pro usnadnění přidán sloupec sym:

rollColumns:`sym`cumVolume;

Nyní si rozdělme sloupce do skupin podle toho, jak mají být aktualizovány. Lze rozlišit tři typy:

  1. Akumulátory (objem, obrat,..) – vstupní hodnotu musíme přičíst k předchozí.
  2. Se speciálním bodem (vysoký, nízký, ..) – první hodnota v minutě se bere z příchozích dat, zbytek se dopočítá pomocí funkce.
  3. Odpočinek. Vždy se počítá pomocí funkce.

Pojďme definovat proměnné pro tyto třídy:

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

Pořadí výpočtu

Agregovanou tabulku aktualizujeme ve dvou fázích. Pro efektivitu nejprve zmenšíme příchozí tabulku tak, aby pro každý znak a minutu byl pouze jeden řádek. Skutečnost, že všechny naše funkce jsou inkrementální a asociativní, zaručuje, že výsledek tohoto dodatečného kroku se nezmění. Stůl můžete zmenšit pomocí výběru:

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

Tato metoda má nevýhodu - sada počítaných sloupců je předdefinována. Naštěstí v Q je select implementován také jako funkce, kde můžete nahradit dynamicky vytvářené argumenty:

?[table;whereClause;byClause;selectClause]

Nebudu podrobně popisovat formát argumentů, v našem případě budou netriviální pouze výrazy by a select a měly by to být slovníky formulářových sloupců!výrazů. Funkci zmenšování lze tedy definovat takto:

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

Pro názornost jsem použil funkci parse, která změní řetězec s Q výrazem na hodnotu, kterou lze předat funkci eval a která je vyžadována ve funkci select. Všimněte si také, že preprocess je definován jako projekce (tj. funkce s částečně definovanými argumenty) funkce select, jeden argument (tabulka) chybí. Pokud aplikujeme preprocess na tabulku, dostaneme komprimovanou tabulku.

Druhou fází je aktualizace agregované tabulky. Nejprve napíšeme algoritmus v pseudokódu:

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

V Q je běžné používat funkce map/reduce namísto smyček. Ale protože Q je vektorový jazyk a můžeme snadno aplikovat všechny operace na všechny symboly najednou, pak se při první aproximaci obejdeme bez smyčky a provedeme operace se všemi symboly najednou:

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

Ale můžeme jít dál, Q má jedinečný a extrémně výkonný operátor – operátor zobecněného přiřazení. Umožňuje změnit sadu hodnot v komplexní datové struktuře pomocí seznamu indexů, funkcí a argumentů. V našem případě to vypadá takto:

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

Bohužel pro přiřazení k tabulce potřebujete seznam řádků, nikoli sloupců, a musíte transponovat matici (seznam sloupců na seznam řádků) pomocí funkce flip. To je drahé pro velkou tabulku, takže místo toho aplikujeme zobecněné přiřazení na každý sloupec zvlášť pomocí funkce map (která vypadá jako apostrof):

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

Opět použijeme projekci funkcí. Všimněte si také, že v Q je vytváření seznamu také funkcí a můžeme ji zavolat pomocí funkce every(map) a získat seznam seznamů.

Abychom zajistili, že sada počítaných sloupců nebude pevná, vytvoříme výše uvedený výraz dynamicky. Pojďme nejprve definovat funkce pro výpočet každého sloupce pomocí proměnných row a inp, které odkazují na agregovaná a vstupní data:

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

Některé sloupce jsou speciální, jejich první hodnotu by funkce neměla vypočítat. Že je první, můžeme určit podle sloupce row[`numTrades] - pokud obsahuje 0, pak je hodnota první. Q má funkci výběru - ?[Booleovský seznam;seznam1;seznam2] - která vybere hodnotu ze seznamu 1 nebo 2 v závislosti na podmínce v prvním argumentu:

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

Zde jsem nazval zobecněné přiřazení se svou funkcí (výraz ve složených závorkách). Přijme aktuální hodnotu (první argument) a další argument, který předám ve 4. parametru.

Přidejme reproduktory na baterie samostatně, protože funkce je pro ně stejná:

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

Toto je normální přiřazení podle standardů Q, ale přiřazuji seznam hodnot najednou. Nakonec vytvoříme hlavní funkci:

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

Tímto výrazem dynamicky vytvořím funkci z řetězce, který obsahuje výraz, který jsem uvedl výše. Výsledek bude vypadat takto:

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

Pořadí vyhodnocení sloupců je obrácené, protože v Q je pořadí vyhodnocení zprava doleva.

Nyní máme dvě hlavní funkce nutné pro výpočty, stačí přidat malou infrastrukturu a služba je připravena.

Závěrečné kroky

Máme funkce preprocess a updateAgg, které dělají veškerou práci. Stále je ale nutné zajistit správný přechod přes minuty a vypočítat indexy pro agregaci. Nejprve definujeme funkci 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
 }

Definujeme také funkci roll, která změní aktuální minutu:

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

Budeme potřebovat funkci pro přidání nových znaků:

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

A konečně funkce upd (tradiční název pro tuto funkci pro Q služby), kterou klient volá za účelem přidání dat:

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

To je vše. Zde je kompletní kód naší služby, jak jsme slíbili, jen pár řádků:

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

Testování

Pojďme zkontrolovat výkon služby. Chcete-li to provést, spusťte jej v samostatném procesu (kód vložte do souboru service.q) a zavolejte funkci init:

q service.q –p 5566

q)init[]

V jiné konzoli spusťte druhý Q proces a připojte se k prvnímu:

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

Nejprve si vytvoříme seznam symbolů – 10000 XNUMX kusů a přidáme funkci pro vytvoření náhodné tabulky. Ve druhé konzoli:

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

Do seznamu jsem přidal tři skutečné symboly, aby bylo snazší je v tabulce hledat. Funkce rnd vytvoří náhodnou tabulku s n řádky, kde se čas mění od t do t+25 milisekund.

Nyní můžete zkusit odeslat data do služby (přidejte prvních deset hodin):

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

Ve službě můžete zkontrolovat, zda byla tabulka aktualizována:

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

Výsledek:

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

Nyní provedeme zátěžové testování, abychom zjistili, kolik dat může služba zpracovat za minutu. Připomínám, že interval aktualizace jsme nastavili na 25 milisekund. Služba se tedy musí (v průměru) vejít do alespoň 20 milisekund na aktualizaci, aby uživatelé měli čas na vyžádání dat. Ve druhém procesu zadejte následující:

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 jsou dvě minuty. Můžete zkusit nejprve spustit 1000 řádků každých 25 milisekund:

start 1000

V mém případě je výsledek kolem několika milisekund na aktualizaci. Takže okamžitě zvýším počet řádků na 10.000 XNUMX:

start 10000

Výsledek:

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

Opět nic zvláštního, ale toto je 24 milionů řádků za minutu, 400 tisíc za sekundu. Na více než 25 milisekund se aktualizace zpomalila pouze 5krát, zřejmě při změně minuty. Zvyšme na 100.000 XNUMX:

start 100000

Výsledek:

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

Jak je vidět, služba si stěží poradí, ale přesto se dokáže udržet nad vodou. Takový objem dat (240 milionů řádků za minutu) je extrémně velký, v takových případech je běžné spustit několik klonů (nebo i desítky klonů) služby, z nichž každý zpracovává pouze část znaků. Přesto je výsledek působivý pro interpretovaný jazyk, který se zaměřuje především na ukládání dat.

Může vyvstat otázka, proč čas roste nelineárně s velikostí každé aktualizace. Důvodem je, že funkce shrink je ve skutečnosti funkce C, která je mnohem efektivnější než updateAgg. Počínaje určitou velikostí aktualizace (kolem 10.000 30) dosáhne updateAgg svého stropu a doba jeho provedení pak nezávisí na velikosti aktualizace. Díky předběžnému kroku Q je služba schopna strávit takové objemy dat. To zdůrazňuje, jak důležité je zvolit správný algoritmus při práci s velkými daty. Dalším bodem je správné uložení dat do paměti. Pokud by data nebyla ukládána sloupcově nebo nebyla řazena podle času, pak bychom se seznámili s takovou věcí, jako je TLB cache miss – absence adresy paměťové stránky v mezipaměti adres procesoru. Hledání adresy trvá při neúspěšném asi XNUMXx déle a pokud jsou data rozházená, může to službu několikrát zpomalit.

Závěr

V tomto článku jsem ukázal, že databáze KDB+ a Q jsou vhodné nejen pro ukládání velkých dat a snadný přístup k nim prostřednictvím select, ale také pro vytváření služeb zpracování dat, které jsou schopny strávit stovky milionů řádků/gigabajtů dat i v jeden jediný Q proces. Samotný jazyk Q umožňuje extrémně stručnou a efektivní implementaci algoritmů souvisejících se zpracováním dat díky své vektorové povaze, vestavěnému interpretu dialektů SQL a velmi úspěšné sadě knihovních funkcí.

Podotýkám, že výše uvedené je jen část toho, co Q dokáže, má také další jedinečné funkce. Například extrémně jednoduchý IPC protokol, který maže hranici mezi jednotlivými Q procesy a umožňuje spojit stovky těchto procesů do jediné sítě, která může být umístěna na desítkách serverů v různých částech světa.

Zdroj: www.habr.com

Přidat komentář