Funktioner i Q- och KDB+-språket med exemplet på en realtidstjänst

Du kan läsa om vad KDB+-basen, Q-programmeringsspråket är, vilka deras styrkor och svagheter är i mina tidigare artikeln och kort i inledningen. I artikeln kommer vi att implementera en tjänst på Q som kommer att bearbeta den inkommande dataströmmen och beräkna olika aggregeringsfunktioner varje minut i "realtidsläge" (dvs den kommer att hinna beräkna allt innan nästa del av data). Huvudfunktionen hos Q är att det är ett vektorspråk som låter dig arbeta inte med enstaka objekt, utan med deras arrayer, arrayer av arrayer och andra komplexa objekt. Språk som Q och dess släktingar K, J, APL är kända för sin korthet. Ofta kan ett program som tar upp flera skärmar med kod på ett välbekant språk som Java skrivas på dem på några rader. Detta är vad jag vill visa i den här artikeln.

Funktioner i Q- och KDB+-språket med exemplet på en realtidstjänst

Inledning

KDB+ är en kolumnär databas fokuserad på mycket stora mängder data, ordnade på ett specifikt sätt (främst efter tid). Det används främst i finansiella institutioner - banker, investeringsfonder, försäkringsbolag. Q-språket är det interna språket i KDB+ som låter dig arbeta effektivt med dessa data. Q-ideologin är korthet och effektivitet, medan tydlighet offras. Detta motiveras av att vektorspråket i alla fall kommer att vara svårt att förstå, och inspelningens korthet och rikedom gör att du kan se en mycket större del av programmet på en skärm, vilket i slutändan gör det lättare att förstå.

I den här artikeln implementerar vi ett fullfjädrat program i Q och du kanske vill prova det. För att göra detta behöver du den faktiska Q. Du kan ladda ner den kostnadsfria 32-bitarsversionen på kx företagets webbplats – www.kx.com. Där hittar du, om du är intresserad, referensinformation om Q, boken Q För dödliga och olika artiklar om detta ämne.

Problem uttalande

Det finns en källa som skickar en tabell med data var 25:e millisekund. Eftersom KDB+ främst används inom finans, kommer vi att anta att detta är en tabell över transaktioner (affärer), som har följande kolumner: tid (tid i millisekunder), sym (företagsbeteckning på börsen - IBM, AAPL,...), pris (pris till vilket aktier köptes), storlek (transaktionsstorlek). Intervallet på 25 millisekunder är godtyckligt, inte för litet och inte för långt. Dess närvaro innebär att data kommer till tjänsten som redan är buffrad. Det skulle vara enkelt att implementera buffring på tjänstesidan, inklusive dynamisk buffring beroende på aktuell belastning, men för enkelhetens skull kommer vi att fokusera på ett fast intervall.

Tjänsten måste räkna varje minut för varje inkommande symbol från sym-kolumnen en uppsättning aggregeringsfunktioner - maxpris, medelpris, summastorlek, etc. användbar information. För enkelhetens skull kommer vi att anta att alla funktioner kan beräknas inkrementellt, d.v.s. för att få ett nytt värde räcker det att känna till två siffror - det gamla och det inkommande värdet. Till exempel har funktionerna max, medel, summa denna egenskap, men medianfunktionen har inte.

Vi kommer också att anta att den inkommande dataströmmen är tidsbeställd. Detta ger oss möjligheten att bara arbeta med sista minuten. I praktiken räcker det med att kunna arbeta med nuvarande och tidigare minuter ifall vissa uppdateringar är försenade. För enkelhetens skull kommer vi inte att överväga detta fall.

Aggregationsfunktioner

De obligatoriska aggregeringsfunktionerna listas nedan. Jag tog så många av dem som möjligt för att öka belastningen på tjänsten:

  • högt – maxpris – maxpris per minut.
  • lågt – lägsta pris – minimipris per minut.
  • firstPrice – första pris – första pris per minut.
  • lastPrice – sista pris – sista pris per minut.
  • firstSize – första storleken – första handelsstorlek per minut.
  • lastSize – sista storleken – sista handelsstorlek på en minut.
  • numTrades – räkna i – antal avslut per minut.
  • volym – summastorlek – summan av handelsstorlekar per minut.
  • pvolym – summapris – summa av priser per minut, krävs för avgPrice.
  • – summa omsättningspris*storlek – total volym transaktioner per minut.
  • avgPrice – pvolume%antalTrades – genomsnittspris per minut.
  • avgSize – volym%antalTrades – genomsnittlig handelsstorlek per minut.
  • vwap – omsättning%volym – genomsnittligt pris per minut viktat efter transaktionsstorlek.
  • cumVolume – summavolym – ackumulerad storlek på transaktioner över hela tiden.

Låt oss omedelbart diskutera en icke-uppenbar punkt - hur man initierar dessa kolumner för första gången och för varje efterföljande minut. Vissa kolumner av typen firstPrice måste initieras till null varje gång, deras värde är odefinierat. Andra volymtyper måste alltid ställas in på 0. Det finns också kolumner som kräver ett kombinerat tillvägagångssätt - till exempel måste cumVolume kopieras från föregående minut, och för den första ställas in på 0. Låt oss ställa in alla dessa parametrar med hjälp av ordboksdata typ (analogt med en post):

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

Jag lade till sym och tid i ordboken för enkelhets skull, nu är initWith en färdig rad från den slutliga aggregerade tabellen, där det återstår att ställa in rätt sym och tid. Du kan använda den för att lägga till nya rader i en tabell.

Vi kommer att behöva aggCols när vi skapar en aggregeringsfunktion. Listan måste inverteras på grund av den ordning i vilken uttryck i Q utvärderas (från höger till vänster). Målet är att säkerställa att beräkningen går från hög till cumVolume, eftersom vissa kolumner beror på tidigare.

Kolumner som behöver kopieras till en ny minut från den föregående, sym-kolumnen läggs till för enkelhetens skull:

rollColumns:`sym`cumVolume;

Låt oss nu dela upp kolumnerna i grupper efter hur de ska uppdateras. Tre typer kan särskiljas:

  1. Ackumulatorer (volym, omsättning,..) – vi måste lägga till det inkommande värdet till det föregående.
  2. Med en speciell punkt (hög, låg, ..) – det första värdet i minuten tas från inkommande data, resten beräknas med funktionen.
  3. Resten. Beräknas alltid med hjälp av en funktion.

Låt oss definiera variabler för dessa klasser:

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

Beräkningsordning

Vi kommer att uppdatera den aggregerade tabellen i två steg. För effektiviteten krymper vi först den inkommande tabellen så att det bara finns en rad för varje tecken och minut. Det faktum att alla våra funktioner är inkrementella och associativa garanterar att resultatet av detta ytterligare steg inte kommer att förändras. Du kan krympa tabellen genom att välja:

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

Denna metod har en nackdel - uppsättningen av beräknade kolumner är fördefinierad. Lyckligtvis är select också implementerat i Q som en funktion där du kan ersätta dynamiskt skapade argument:

?[table;whereClause;byClause;selectClause]

Jag kommer inte att beskriva i detalj formatet för argumenten, i vårt fall kommer endast av och utvalda uttryck att vara icke-triviala och de bör vara ordböcker med formkolumner!uttryck. Således kan krympningsfunktionen definieras enligt följande:

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

För tydlighetens skull använde jag funktionen parse, som förvandlar en sträng med ett Q-uttryck till ett värde som kan skickas till eval-funktionen och som krävs i funktionsvalet. Notera också att förprocess definieras som en projektion (dvs. en funktion med delvis definierade argument) av select-funktionen, ett argument (tabellen) saknas. Om vi ​​tillämpar förprocess på en tabell får vi en komprimerad tabell.

Det andra steget är att uppdatera den aggregerade tabellen. Låt oss först skriva algoritmen i pseudokod:

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

I Q är det vanligt att använda map/reduce-funktioner istället för loops. Men eftersom Q är ett vektorspråk och vi enkelt kan tillämpa alla operationer på alla symboler samtidigt, så kan vi till en första approximation klara oss utan en loop alls och utföra operationer på alla symboler samtidigt:

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

Men vi kan gå längre, Q har en unik och extremt kraftfull operatör - den generaliserade uppdragsoperatören. Det låter dig ändra en uppsättning värden i en komplex datastruktur med hjälp av en lista med index, funktioner och argument. I vårt fall ser det ut så här:

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

Tyvärr, för att tilldela en tabell behöver du en lista med rader, inte kolumner, och du måste transponera matrisen (lista över kolumner till lista med rader) med vändningsfunktionen. Detta är dyrt för en stor tabell, så istället tillämpar vi en generaliserad tilldelning för varje kolumn separat, med hjälp av kartfunktionen (som ser ut som en apostrof):

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

Vi använder återigen funktionsprojektion. Notera också att i Q är att skapa en lista också en funktion och vi kan kalla den med funktionen each(map) för att få en lista med listor.

För att säkerställa att uppsättningen av beräknade kolumner inte är fixerad kommer vi att skapa ovanstående uttryck dynamiskt. Låt oss först definiera funktioner för att beräkna varje kolumn, med hjälp av rad- och inp-variablerna för att referera till aggregerade data och indata:

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

Vissa kolumner är speciella, deras första värde bör inte beräknas av funktionen. Vi kan fastställa att det är den första genom kolumnen row[`numTrades] - om den innehåller 0, så är värdet först. Q har en valfunktion - ?[Boolean list;list1;list2] - som väljer ett värde från lista 1 eller 2 beroende på villkoret i det första argumentet:

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

Här kallade jag en generaliserad uppgift med min funktion (ett uttryck i lockiga hängslen). Den får det aktuella värdet (det första argumentet) och ett ytterligare argument, som jag skickar i den 4:e parametern.

Låt oss lägga till batterihögtalare separat, eftersom funktionen är densamma för dem:

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

Detta är en normal tilldelning enligt Q-standarder, men jag tilldelar en lista med värden på en gång. Låt oss slutligen skapa huvudfunktionen:

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

Med detta uttryck skapar jag dynamiskt en funktion från en sträng som innehåller uttrycket jag gav ovan. Resultatet kommer att se ut så här:

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

Kolumnutvärderingsordningen är inverterad eftersom utvärderingsordningen i Q är från höger till vänster.

Nu har vi två huvudfunktioner som behövs för beräkningar, vi behöver bara lägga till lite infrastruktur och tjänsten är klar.

Sista stegen

Vi har förprocess och updateAgg-funktioner som gör allt arbete. Men det är fortfarande nödvändigt att säkerställa korrekt övergång genom minuter och beräkna index för aggregering. Först av allt, låt oss definiera init-funktionen:

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
 }

Vi kommer också att definiera roll-funktionen, som kommer att ändra den aktuella minuten:

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

Vi kommer att behöva en funktion för att lägga till nya tecken:

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

Och slutligen, upd-funktionen (det traditionella namnet för denna funktion för Q-tjänster), som anropas av klienten för att lägga till data:

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

Det är allt. Här är den fullständiga koden för vår tjänst, som utlovat, bara några rader:

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

testning

Låt oss kontrollera tjänstens prestanda. För att göra detta, låt oss köra det i en separat process (lägg koden i filen service.q) och anropa init-funktionen:

q service.q –p 5566

q)init[]

I en annan konsol, starta den andra Q-processen och anslut till den första:

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

Låt oss först skapa en lista med symboler - 10000 XNUMX stycken och lägga till en funktion för att skapa en slumpmässig tabell. I den andra konsolen:

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

Jag lade till tre riktiga symboler i listan för att göra det lättare att leta efter dem i tabellen. Funktionen rnd skapar en slumpmässig tabell med n rader, där tiden varierar från t till t+25 millisekunder.

Nu kan du prova att skicka data till tjänsten (lägg till de första tio timmarna):

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

Du kan kontrollera i tjänsten att tabellen har uppdaterats:

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

Resultat:

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

Låt oss nu utföra belastningstestning för att ta reda på hur mycket data tjänsten kan bearbeta per minut. Låt mig påminna dig om att vi ställer in uppdateringsintervallet till 25 millisekunder. Följaktligen måste tjänsten (i genomsnitt) passa in i minst 20 millisekunder per uppdatering för att ge användarna tid att begära data. Ange följande i den andra processen:

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 är två minuter. Du kan prova att köra först i 1000 rader var 25:e millisekund:

start 1000

I mitt fall är resultatet runt ett par millisekunder per uppdatering. Så jag kommer omedelbart att öka antalet rader till 10.000 XNUMX:

start 10000

Resultat:

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

Återigen, inget speciellt, men det här är 24 miljoner rader per minut, 400 tusen per sekund. Under mer än 25 millisekunder saktade uppdateringen ner endast 5 gånger, tydligen när minuten ändrades. Låt oss öka till 100.000 XNUMX:

start 100000

Resultat:

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

Som du kan se klarar tjänsten knappt, men ändå lyckas den hålla sig flytande. En sådan datavolym (240 miljoner rader per minut) är extremt stor, i sådana fall är det vanligt att lansera flera kloner (eller till och med dussintals kloner) av tjänsten, som var och en endast bearbetar en del av karaktärerna. Ändå är resultatet imponerande för ett tolkat språk som i första hand fokuserar på datalagring.

Frågan kan uppstå om varför tiden växer icke-linjärt med storleken på varje uppdatering. Anledningen är att krympfunktionen faktiskt är en C-funktion, vilket är mycket effektivare än updateAgg. Med utgångspunkt från en viss uppdateringsstorlek (cirka 10.000 30) når updateAgg sitt tak och då beror dess exekveringstid inte på uppdateringsstorleken. Det är på grund av det preliminära steget Q som tjänsten kan smälta sådana datavolymer. Detta belyser hur viktigt det är att välja rätt algoritm när man arbetar med big data. En annan punkt är korrekt lagring av data i minnet. Om data inte lagrades kolumnärt eller inte ordnades efter tid, skulle vi bli bekanta med något sådant som en TLB-cachemiss - frånvaron av en minnessidaadress i processoradresscachen. Att söka efter en adress tar cirka XNUMX gånger längre tid om det misslyckas, och om informationen är spridd kan det sakta ner tjänsten flera gånger.

Slutsats

I den här artikeln visade jag att databaserna KDB+ och Q är lämpliga inte bara för att lagra stora data och enkelt komma åt dem genom att välja, utan också för att skapa databehandlingstjänster som kan smälta hundratals miljoner rader/gigabyte med data även i en enda Q-process. Q-språket i sig tillåter extremt koncis och effektiv implementering av algoritmer relaterade till databehandling på grund av dess vektornatur, inbyggda SQL-dialekttolkare och en mycket framgångsrik uppsättning biblioteksfunktioner.

Jag kommer att notera att ovanstående bara är en del av vad Q kan göra, det har också andra unika funktioner. Till exempel ett extremt enkelt IPC-protokoll som raderar gränsen mellan individuella Q-processer och låter dig kombinera hundratals av dessa processer till ett enda nätverk, som kan placeras på dussintals servrar i olika delar av världen.

Källa: will.com

Lägg en kommentar