Kenmerken van de Q- en KDB+-taal met behulp van het voorbeeld van een realtime service

In mijn vorige kunt u lezen wat de KDB+-basis en de Q-programmeertaal zijn en wat hun sterke en zwakke punten zijn статье en kort in de inleiding. In het artikel zullen we een service op Q implementeren die de binnenkomende gegevensstroom zal verwerken en elke minuut verschillende aggregatiefuncties zal berekenen in de "real-time" modus (dat wil zeggen dat er tijd is om alles te berekenen vóór het volgende deel van de gegevens). Het belangrijkste kenmerk van Q is dat het een vectortaal is waarmee je niet met afzonderlijke objecten kunt werken, maar met hun arrays, arrays van arrays en andere complexe objecten. Talen zoals Q en zijn verwanten K, J, APL staan ​​bekend om hun beknoptheid. Vaak kan een programma dat meerdere schermen met code in een bekende taal als Java beslaat, er in een paar regels op worden geschreven. Dit is wat ik in dit artikel wil aantonen.

Kenmerken van de Q- en KDB+-taal met behulp van het voorbeeld van een realtime service

Introductie

KDB+ is een kolomvormige database gericht op zeer grote hoeveelheden gegevens, geordend op een specifieke manier (voornamelijk op tijd). Het wordt voornamelijk gebruikt in financiële instellingen - banken, investeringsfondsen, verzekeringsmaatschappijen. De Q-taal is de interne taal van KDB+ waarmee u effectief met deze gegevens kunt werken. De Q-ideologie is beknoptheid en efficiëntie, terwijl de duidelijkheid wordt opgeofferd. Dit wordt gerechtvaardigd door het feit dat de vectortaal hoe dan ook moeilijk te begrijpen zal zijn, en door de beknoptheid en rijkdom van de opname kun je een veel groter deel van het programma op één scherm zien, wat het uiteindelijk gemakkelijker te begrijpen maakt.

In dit artikel implementeren we een volwaardig programma in Q en misschien wil je het eens uitproberen. Hiervoor heeft u de daadwerkelijke Q nodig. U kunt de gratis 32-bits versie downloaden op de kx-bedrijfswebsite - www.kx.com. Daar vindt u, als u geïnteresseerd bent, referentie-informatie over Q, het boek V Voor stervelingen en diverse artikelen over dit onderwerp.

Formulering van het probleem

Er is een bron die elke 25 milliseconden een tabel met gegevens verzendt. Omdat KDB+ voornamelijk in de financiële wereld wordt gebruikt, gaan we ervan uit dat dit een tabel met transacties (transacties) is, die de volgende kolommen heeft: time (tijd in milliseconden), sym (bedrijfsaanduiding op de beurs - IBM, AAPL,…), prijs (de prijs waartegen de aandelen zijn gekocht), omvang (omvang van de transactie). Het interval van 25 milliseconden is willekeurig, niet te klein en niet te lang. De aanwezigheid ervan betekent dat de gegevens al gebufferd naar de service komen. Het zou gemakkelijk zijn om buffering aan de servicekant te implementeren, inclusief dynamische buffering afhankelijk van de huidige belasting, maar voor de eenvoud zullen we ons concentreren op een vast interval.

De service moet elke minuut voor elk binnenkomend symbool uit de sym-kolom een ​​reeks aggregatiefuncties tellen: maximale prijs, gemiddelde prijs, somgrootte, enz. bruikbare informatie. Voor de eenvoud gaan we ervan uit dat alle functies incrementeel kunnen worden berekend, d.w.z. om een ​​nieuwe waarde te verkrijgen, volstaat het om twee getallen te kennen: de oude en de binnenkomende waarden. De functies max, gemiddelde, som hebben bijvoorbeeld deze eigenschap, maar de mediaanfunctie niet.

We gaan er ook van uit dat de binnenkomende gegevensstroom in de tijd is geordend. Dit geeft ons de mogelijkheid om alleen met de laatste minuut te werken. In de praktijk is het voldoende om met de huidige en voorgaande notulen te kunnen werken voor het geval sommige updates te laat komen. Voor de eenvoud zullen we dit geval niet beschouwen.

Aggregatiefuncties

De vereiste aggregatiefuncties worden hieronder vermeld. Ik heb er zoveel mogelijk genomen om de belasting van de dienst te vergroten:

  • hoog – maximale prijs – maximale prijs per minuut.
  • laag – min prijs – minimum prijs per minuut.
  • firstPrice – eerste prijs – eerste prijs per minuut.
  • lastPrice – laatste prijs – laatste prijs per minuut.
  • firstSize – eerste maat – eerste handelsgrootte per minuut.
  • lastSize – laatste maat – laatste handelsgrootte in een minuut.
  • numTrades – telling i – aantal transacties per minuut.
  • volume – somgrootte – som van handelsgroottes per minuut.
  • pvolume – somprijs – som van prijzen per minuut, vereist voor avgPrice.
  • – som omzetprijs*omvang – totaal aantal transacties per minuut.
  • avgPrice – pvolume%numTrades – gemiddelde prijs per minuut.
  • avgSize – volume%numTrades – gemiddelde handelsgrootte per minuut.
  • vwap – omzet%volume – gemiddelde prijs per minuut gewogen naar transactiegrootte.
  • cumVolume – somvolume – geaccumuleerde omvang van transacties over de gehele tijd.

Laten we meteen een niet voor de hand liggend punt bespreken: hoe u deze kolommen voor de eerste keer en voor elke volgende minuut kunt initialiseren. Sommige kolommen van het type firstPrice moeten elke keer op null worden geïnitialiseerd; hun waarde is niet gedefinieerd. Andere volumetypen moeten altijd op 0 worden ingesteld. Er zijn ook kolommen die een gecombineerde aanpak vereisen - cumVolume moet bijvoorbeeld van de vorige minuut worden gekopieerd en voor de eerste op 0 worden ingesteld. Laten we al deze parameters instellen met behulp van de woordenboekgegevens type (analoog aan een record):

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

Ik heb voor het gemak sym en tijd aan het woordenboek toegevoegd, nu is initWith een kant-en-klare regel uit de uiteindelijke geaggregeerde tabel, waar het overblijft om de juiste sym en tijd in te stellen. U kunt het gebruiken om nieuwe rijen aan een tabel toe te voegen.

We hebben aggCols nodig bij het maken van een aggregatiefunctie. De lijst moet worden omgekeerd vanwege de volgorde waarin expressies in Q worden geëvalueerd (van rechts naar links). Het doel is om ervoor te zorgen dat de berekening van hoog naar cumVolume gaat, omdat sommige kolommen afhankelijk zijn van eerdere kolommen.

Kolommen die van de vorige naar een nieuwe minuut moeten worden gekopieerd, voor het gemak is de sym-kolom toegevoegd:

rollColumns:`sym`cumVolume;

Laten we nu de kolommen in groepen verdelen op basis van hoe ze moeten worden bijgewerkt. Er zijn drie soorten te onderscheiden:

  1. Accumulatoren (volume, omzet,..) – we moeten de inkomende waarde optellen bij de vorige.
  2. Met een speciaal punt (hoog, laag, ..) – de eerste waarde in de minuut wordt uit de binnenkomende gegevens gehaald, de rest wordt berekend met behulp van de functie.
  3. Rest. Altijd berekend met behulp van een functie.

Laten we variabelen voor deze klassen definiëren:

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

Berekeningsvolgorde

We zullen de geaggregeerde tabel in twee fasen bijwerken. Voor de efficiëntie verkleinen we eerst de binnenkomende tabel zodat er voor elk teken en elke minuut slechts één rij is. Het feit dat al onze functies incrementeel en associatief zijn, garandeert dat het resultaat van deze extra stap niet zal veranderen. U kunt de tabel verkleinen met select:

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

Deze methode heeft een nadeel: de set berekende kolommen is vooraf gedefinieerd. Gelukkig is select in Q ook geïmplementeerd als een functie waarmee je dynamisch gemaakte argumenten kunt vervangen:

?[table;whereClause;byClause;selectClause]

Ik zal het formaat van de argumenten niet in detail beschrijven; in ons geval zullen alleen by- en geselecteerde expressies niet-triviaal zijn en zouden het woordenboeken van de formulierkolommen moeten zijn! De krimpfunctie kan dus als volgt worden gedefinieerd:

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

Voor de duidelijkheid heb ik de parse-functie gebruikt, die een string met een Q-expressie omzet in een waarde die kan worden doorgegeven aan de eval-functie en die vereist is in de functie select. Merk ook op dat preprocess wordt gedefinieerd als een projectie (dat wil zeggen een functie met gedeeltelijk gedefinieerde argumenten) van de select-functie; één argument (de tabel) ontbreekt. Als we preprocessing op een tabel toepassen, krijgen we een gecomprimeerde tabel.

De tweede fase is het bijwerken van de geaggregeerde tabel. Laten we het algoritme eerst in pseudocode schrijven:

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

In Q is het gebruikelijk om kaart-/reduceerfuncties te gebruiken in plaats van lussen. Maar aangezien Q een vectortaal is en we alle bewerkingen gemakkelijk op alle symbolen tegelijk kunnen toepassen, kunnen we bij een eerste benadering het helemaal zonder lus doen en bewerkingen op alle symbolen tegelijk uitvoeren:

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

Maar we kunnen verder gaan: Q heeft een unieke en extreem krachtige operator: de gegeneraliseerde toewijzingsoperator. Hiermee kunt u een reeks waarden in een complexe datastructuur wijzigen met behulp van een lijst met indices, functies en argumenten. In ons geval ziet het er als volgt uit:

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

Helaas heb je voor het toewijzen aan een tabel een lijst met rijen nodig, geen kolommen, en moet je de matrix (lijst met kolommen naar lijst met rijen) transponeren met behulp van de flip-functie. Dit is duur voor een grote tabel, dus in plaats daarvan passen we een algemene toewijzing toe op elke kolom afzonderlijk, met behulp van de kaartfunctie (die eruit ziet als een apostrof):

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

We gebruiken opnieuw functieprojectie. Merk ook op dat het maken van een lijst in Q ook een functie is en dat we deze kunnen aanroepen met de functie Each(map) om een ​​lijst met lijsten te krijgen.

Om ervoor te zorgen dat de reeks berekende kolommen niet vastligt, zullen we de bovenstaande expressie dynamisch maken. Laten we eerst functies definiëren om elke kolom te berekenen, waarbij we de rij- en inp-variabelen gebruiken om naar de geaggregeerde en invoergegevens te verwijzen:

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

Sommige kolommen zijn speciaal; hun eerste waarde mag niet door de functie worden berekend. We kunnen bepalen dat dit de eerste is door de kolom row[`numTrades] - als deze 0 bevat, dan is de waarde de eerste. Q heeft een selectiefunctie - ?[Booleaanse lijst;lijst1;lijst2] - die een waarde uit lijst 1 of 2 selecteert, afhankelijk van de voorwaarde in het eerste argument:

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

Hier riep ik een algemene opdracht aan met mijn functie (een uitdrukking tussen accolades). Het ontvangt de huidige waarde (het eerste argument) en een aanvullend argument, dat ik doorgeef in de vierde parameter.

Laten we batterijluidsprekers afzonderlijk toevoegen, omdat de functie voor hen hetzelfde is:

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

Dit is een normale toewijzing volgens Q-normen, maar ik wijs een lijst met waarden in één keer toe. Laten we ten slotte de hoofdfunctie maken:

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

Met deze expressie maak ik dynamisch een functie van een string die de expressie bevat die ik hierboven heb gegeven. Het resultaat ziet er als volgt uit:

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

De kolomevaluatievolgorde is omgekeerd omdat in Q de evaluatievolgorde van rechts naar links is.

Nu hebben we twee hoofdfuncties die nodig zijn voor berekeningen, we hoeven alleen maar een beetje infrastructuur toe te voegen en de service is klaar.

Laatste stappen

We hebben preprocess- en updateAgg-functies die al het werk doen. Maar het is nog steeds nodig om de juiste overgang door minuten te garanderen en indexen voor aggregatie te berekenen. Laten we eerst de init-functie definiëren:

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
 }

We zullen ook de roll-functie definiëren, die de huidige minuut zal veranderen:

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

We hebben een functie nodig om nieuwe karakters toe te voegen:

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

En tenslotte de upd-functie (de traditionele naam voor deze functie voor Q-services), die door de client wordt aangeroepen om gegevens toe te voegen:

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

Dat is alles. Hier is de volledige code van onze service, zoals beloofd, slechts een paar regels:

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

Testen

Laten we de prestaties van de service controleren. Om dit te doen, laten we het in een apart proces uitvoeren (plaats de code in het service.q-bestand) en roepen de init-functie aan:

q service.q –p 5566

q)init[]

Start in een andere console het tweede Q-proces en maak verbinding met de eerste:

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

Laten we eerst een lijst met symbolen maken - 10000 stuks en een functie toevoegen om een ​​willekeurige tabel te maken. In de tweede console:

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

Ik heb drie echte symbolen aan de lijst toegevoegd, zodat je ze gemakkelijker in de tabel kunt zoeken. De rnd-functie creëert een willekeurige tabel met n rijen, waarbij de tijd varieert van t tot t+25 milliseconden.

Nu kunt u proberen gegevens naar de service te verzenden (voeg de eerste tien uur toe):

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

U kunt in de service controleren of de tabel is bijgewerkt:

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

Resultaat:

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

Laten we nu belastingtests uitvoeren om erachter te komen hoeveel gegevens de service per minuut kan verwerken. Ik wil u eraan herinneren dat we het update-interval hebben ingesteld op 25 milliseconden. Dienovereenkomstig moet de dienst (gemiddeld) in minimaal 20 milliseconden per update passen om gebruikers de tijd te geven gegevens op te vragen. Voer het volgende in het tweede proces in:

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 is twee minuten. U kunt proberen eerst 1000 rijen elke 25 milliseconden uit te voeren:

start 1000

In mijn geval is het resultaat ongeveer een paar milliseconden per update. Dus ik verhoog het aantal rijen onmiddellijk naar 10.000:

start 10000

Resultaat:

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

Nogmaals, niets bijzonders, maar dit zijn 24 miljoen lijnen per minuut, 400 duizend per seconde. Gedurende meer dan 25 milliseconden werd de update slechts vijf keer vertraagd, blijkbaar toen de minuut veranderde. Laten we verhogen naar 5:

start 100000

Resultaat:

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

Zoals u ziet kan de dienst het nauwelijks aan, maar weet toch het hoofd boven water te houden. Een dergelijk gegevensvolume (240 miljoen rijen per minuut) is extreem groot; in dergelijke gevallen is het gebruikelijk om meerdere klonen (of zelfs tientallen klonen) van de dienst te lanceren, die elk slechts een deel van de karakters verwerken. Toch is het resultaat indrukwekkend voor een geïnterpreteerde taal die zich primair richt op gegevensopslag.

De vraag kan rijzen waarom de tijd niet-lineair groeit met de omvang van elke update. De reden is dat de krimpfunctie eigenlijk een C-functie is, die veel efficiënter is dan updateAgg. Vanaf een bepaalde updategrootte (ongeveer 10.000) bereikt updateAgg zijn plafond en is de uitvoeringstijd niet afhankelijk van de updategrootte. Het is dankzij de voorbereidende stap Q dat de dienst dergelijke hoeveelheden gegevens kan verwerken. Dit benadrukt hoe belangrijk het is om het juiste algoritme te kiezen bij het werken met big data. Een ander punt is de juiste opslag van gegevens in het geheugen. Als de gegevens niet in kolommen zouden worden opgeslagen of niet op tijd zouden worden geordend, zouden we bekend raken met zoiets als een TLB-cache-misser: de afwezigheid van een geheugenpagina-adres in de adrescache van de processor. Het zoeken naar een adres duurt ongeveer 30 keer langer als het niet lukt, en als de gegevens verspreid zijn, kan dit de service verschillende keren vertragen.

Conclusie

In dit artikel heb ik laten zien dat de KDB+- en Q-database niet alleen geschikt zijn voor het opslaan van grote gegevens en het gemakkelijk toegankelijk maken ervan via selectie, maar ook voor het creëren van gegevensverwerkingsdiensten die in staat zijn honderden miljoenen rijen/gigabytes aan gegevens te verwerken, zelfs in één enkel Q-proces. De Q-taal zelf maakt een uiterst beknopte en efficiënte implementatie mogelijk van algoritmen die verband houden met gegevensverwerking vanwege het vectorkarakter, de ingebouwde SQL-dialectinterpreter en een zeer succesvolle reeks bibliotheekfuncties.

Ik wil opmerken dat het bovenstaande slechts een deel is van wat Q kan doen, het heeft ook andere unieke kenmerken. Bijvoorbeeld een uiterst eenvoudig IPC-protocol dat de grens tussen individuele Q-processen opheft en je in staat stelt honderden van deze processen te combineren in één enkel netwerk, dat zich op tientallen servers in verschillende delen van de wereld kan bevinden.

Bron: www.habr.com

Voeg een reactie