De mogelijkheden van de Q-taal en KDB+ aan de hand van een real-time dienst

Wat KDB+ is, de programmeertaal Q, en wat hun sterke en zwakke punten zijn, kunt u lezen in mijn vorige artikel en kort in de inleiding. In dit artikel implementeren we een service in Q die een binnenkomende datastroom verwerkt en minuut-voor-minuut verschillende aggregatiefuncties in ‘realtime’ berekent (d.w.z. het moet alle berekeningen maken voordat de volgende datastroom binnenkomt). De belangrijkste eigenschap van Q is dat het een vector-taal is, die het mogelijk maakt om niet met enkele objecten te werken, maar met arrays, arrays van arrays en andere complexe objecten. Talen zoals Q en de verwante K, J, APL staan bekend om hun beknoptheid. Vaak kan een programma dat meerdere schermen code in een gebruikelijke taal zoals Java vereist, in enkele regels geschreven worden in deze talen. Dit is precies wat ik in dit artikel wil demonstreren.

De mogelijkheden van de Q-taal en KDB+ aan de hand van een real-time dienst

Inleiding

KDB+ is een kolom-georiënteerde database, gericht op zeer grote hoeveelheden gegevens, gestructureerd op een bepaalde manier (vooral op tijd). Het wordt voornamelijk gebruikt in financiële instellingen – banken, beleggingsfondsen, verzekeringsmaatschappijen. De programmeertaal Q is de interne taal van KDB+, waarmee effectief met deze gegevens gewerkt kan worden. De ideologie van Q is beknoptheid en efficiëntie, waarbij duidelijkheid wordt opgeofferd. Dit wordt gerechtvaardigd door het feit dat een vector-taal in ieder geval moeilijk te begrijpen zal zijn, en de beknoptheid en densiteit van de notatie maakt het mogelijk veel meer van het programma op één scherm te zien, wat uiteindelijk de begrijpelijkheid vergemakkelijkt.

In dit artikel implementeren we een volwaardig programma in Q en u zult misschien willen proberen het in de praktijk te gebruiken. Hiervoor heeft u Q nodig. De gratis 32-bit versie kunt u downloaden op de website van het bedrijf kx – www.kx.com. Daar vindt u ook, indien u geïnteresseerd bent, naslaginformatie over Q, het boek Q For Mortals en diverse artikelen over dit onderwerp.

Taakstelling

Er is een bron die elke 25 milliseconden een tabel met gegevens stuurt. Aangezien KDB+ voornamelijk in de financiën wordt toegepast, gaan we ervan uit dat dit een tabel met transacties (trades) is, waarin de volgende kolommen staan: time (tijd in milliseconden), sym (ticker van het bedrijf op de beurs – IBM, AAPL,…), price (prijs, waartegen de aandelen zijn gekocht), size (grootte van de transactie). Het interval van 25 milliseconden is willekeurig gekozen, het is niet te klein en niet te groot. Het betekent dat de gegevens al gebufferd in de service aankomen. Het zou eenvoudig zijn om buffering aan de kant van de service te implementeren, inclusief dynamische buffering die afhankelijk is van de huidige belasting, maar voor de eenvoud blijven we bij een vast interval.

De service moet per minuut voor elk binnenkomend symbool uit de kolom sym een set aggregatiefuncties berekenen - max price, avg price, sum size en andere nuttige informatie. Voor de eenvoud nemen we aan dat alle functies incrementeel kunnen worden berekend, dat wil zeggen dat om een nieuw waarde te krijgen, het voldoende is om twee getallen te kennen - de oude en het binnenkomende waarde. Bijvoorbeeld, de functies max, average, sum hebben deze eigenschap, maar de mediaanfunctie niet.

We veronderstellen ook dat de inkomende gegevensstroom op tijd is gesorteerd. Dit geeft ons de mogelijkheid om alleen met de laatste minuut te werken. In de praktijk is het voldoende om met de huidige en de voorgaande minuut te werken voor het geval er updates zijn vertraagd. Voor de eenvoud zullen we deze situatie niet overwegen.

Aggregatiefuncties

Hieronder staan de noodzakelijke aggregatiefuncties. Ik heb ze zoveel mogelijk genomen om de belasting op de service te verhogen:

  • high – max price – maximale prijs per minuut.
  • low – min price – minimale prijs per minuut.
  • firstPrice – first price – eerste prijs per minuut.
  • lastPrice – last price – laatste prijs per minuut.
  • firstSize – first size – eerste grootte van de transactie per minuut.
  • lastSize – last size – laatste grootte van de transactie per minuut.
  • numTrades – count i – aantal transacties per minuut.
  • volume – sum size – totale grootte van transacties per minuut.
  • pvolume – sum price – totale prijzen per minuut, nodig voor avgPrice.
  • turnover – sum price*size – totale omzet van transacties per minuut.
  • avgPrice – pvolume%numTrades – gemiddelde prijs per minuut.
  • avgSize – volume%numTrades – gemiddelde grootte van de transactie per minuut.
  • vwap – turnover%volume – gewogen gemiddelde prijs per minuut op basis van de grootte van de transactie.
  • cumVolume – sum volume – cumulatieve grootte van transacties over alle tijd.

Laten we meteen een minder voor de hand liggend punt bespreken: hoe deze kolommen voor de eerste keer en voor elke volgende minuut te initialiseren. Sommige kolommen, zoals firstPrice, moeten telkens weer worden geïnitialiseerd met de waarde null; hun waarde is niet gedefinieerd. Andere, zoals volume, moeten altijd op 0 worden ingesteld. Daarnaast zijn er kolommen die een gecombineerde aanpak vereisen – bijvoorbeeld, cumVolume moet worden gekopieerd van de voorgaande minuut, en voor de eerste moet het op 0 worden ingesteld. We zullen al deze parameters instellen met behulp van het datatype dictionary (vergelijkbaar met 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 sym en time aan de dictionary toegevoegd voor het gemak; nu is initWith een kant-en-klare regel uit de finale geaggregeerde tabel, waar alleen de juiste sym en time moeten worden ingesteld. Deze kan worden gebruikt voor het toevoegen van nieuwe rijen aan de tabel.

aggCols hebben we nodig bij het aanmaken van de aggregatiefunctie. De lijst moet omgekeerd worden vanwege de kenmerken van de volgorde van berekeningen in Q (van rechts naar links). Het doel is om de berekening richting high naar cumVolume te waarborgen, aangezien sommige kolommen afhankelijk zijn van eerdere resultaten.

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

rollColumns:`sym`cumVolume;

Laten we de kolommen nu indelen in groepen op basis van hoe ze moeten worden geüpdatet. We kunnen drie types onderscheiden:

  1. Accumulators (volume, turnover,..) – we moeten de binnenkomende waarde bij de vorige optellen.
  2. Met een speciale waarde (high, low, ..) – de eerste waarde in de minuut komt uit de binnenkomende gegevens, de overige worden berekend met behulp van een functie.
  3. De overige. Worden altijd berekend met behulp van een functie.

Laten we variabelen definiëren voor deze klassen:

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

Volgorde van berekeningen

We gaan de geaggregeerde tabel in twee stappen bijwerken. Voor de efficiëntie zullen we eerst de binnenkomende tabel verkleinen zodat er nog maar één regel voor elk symbool en minuut overblijft. Het feit dat al onze functies incrementeel en associatief zijn, garandeert ons dat het resultaat van deze extra stap niet verandert. De tabel zou kunnen worden verkleind met behulp van select:

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

Dit som heeft een nadeel – de set van te berekenen kolommen is van tevoren gedefinieerd. Gelukkig is select in Q ook geïmplementeerd als een functie, waar dynamisch aangemaakte argumenten kunnen worden ingevoerd:

?[table;whereClause;byClause;selectClause]

Ik zal het argumentformaat niet in detail beschrijven; in ons geval zullen alleen de by- en select-expressies niet triviaal zijn en ze moeten woordenboeken zijn van het type columns!expressies. Zo kan de samenvattende functie als volgt worden gedefinieerd:

selExpression:`high`low`firstPrice`lastPrice`firstSize`lastSize`numTrades`volume`pvolume`turnover!parse each ("max prijs";"min prijs";"eerste prijs";"laatste prijs";"eerste grootte";"laatste grootte";"tellen i";"som grootte";"som prijs";"som prijs* grootte"); // each is de mapfunctie in Q voor één lijst
preprocess:?[;();`sym`time!`sym`time.minute;selExpression];

Voor de duidelijkheid heb ik de functie parse gebruikt, die een Q-expressie in een waarde omzet die kan worden doorgegeven aan de eval-functie en die nodig is in de functionele select. Laten we ook opmerken dat preprocess is gedefinieerd als een projectie (d.w.z. een functie met gedeeltelijk gedefinieerde argumenten) van de selectfunctie, één argument (tabel) ontbreekt. Als we preprocess op de tabel toepassen, krijgen we een samengevoegde tabel.

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

for each sym in inputTable
  idx: rij-index in de aggtabel voor sym+currentTime;
  aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
  aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
  …

In Q worden in plaats van lussen meestal map/reduce-functies gebruikt. Maar omdat Q een vector-taal is en we alle operaties gerust op alle symbolen tegelijk kunnen toepassen, kunnen we in eerste instantie zelfs zonder lus werken, terwijl we operaties 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; in Q bestaat er een unieke en uiterst krachtige operator - de operator voor algemene toewijzing. Deze stelt ons in staat om een set waarden in een complexe datastructuur te wijzigen met behulp van een lijst van indexen, functies en argumenten. In ons geval ziet het er als volgt uit:

idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);functie;argument] ~ target[idx 0;idx 1;…]: functie[target[idx 0;idx 1;…];argument], in ons geval is de functie - toewijzing
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];

Helaas is voor toewijzing in de tabel een lijst van rijen nodig en niet van kolommen, waardoor we de matrix (lijst van kolommen naar lijst van rijen) moeten transponeren met behulp van de flip-functie. Voor een grote tabel is dit kostbaar, daarom passen we de algemene toewijzing toe op elke kolom afzonderlijk met behulp van de mapfunctie (die eruitziet als een apostrof):

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

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

Om ervoor te zorgen dat de set van berekende kolommen niet vastligt, zullen we de uitdrukking dynamisch hierboven maken. Eerst definiëren we functies om elke kolom te berekenen, waarbij we de variabelen row en inp 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 bijzonder, hun eerste waarde mag niet door de functie worden berekend. We kunnen bepalen dat het de eerste is op de kolom row[`numTrades] - als deze 0 is, dan is de waarde eerste. In Q is er een selectiefunctie - ?[Boolean list;list1;list2] - die waarde kiest uit lijst 1 of 2 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 heb ik algemene toewijzing aangeroepen met mijn functie (de uitdrukking in accolades). Deze ontvangt de huidige waarde (eerste argument) en een extra argument die ik als vierde parameter doorgeef.

Voeg apart de accumulatiekolommen toe, aangezien de functie voor hen dezelfde is:

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

Dit is een gebruikelijke toewijzing in termen van Q, alleen geef ik meteen een lijst van waarden toe. Laten we tenslotte 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 uitdrukking maak ik dynamisch een functie van de string die de uitdrukking bevat die ik hierboven noemde. Het resultaat zal er als volgt uitzien:

{[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 volgorde van de berekening van kolommen is omgekeerd, omdat in Q de volgorde van berekening van rechts naar links is.

Nu hebben we twee hoofd functies die nodig zijn voor de berekeningen, we moeten nog wat infrastructuur toevoegen en de service is klaar.

Laatste stappen

We hebben de functies preprocess en updateAgg die al het werk doen. Maar we moeten ook zorgen voor een juiste overgang door de minuten en de indexen voor aggregatie berekenen. Laten we eerst de functie init definiëren:

init:{
  tradeAgg:: 0#enlist[initWith]; // we create an empty typed table, enlist converts the dictionary into a table, and 0# means to take 0 elements from it
  currTime::00:00; // we start at 0, :: indicates that assignment is to a global variable
  currSyms::`u#`symbol$(); // `u# - converts the list into a tree for faster element search
  offset::0; // index in tradeAgg where the current minute starts 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // cache for the latest values of roll columns, table with key sym
 }

Laten we ook de functie roll definiëren, die de huidige minuut zal veranderen:

roll:{[tm]
  if[currTime>tm; :init[]]; // als het middernacht is gepasseerd, roep gewoon init aan
  rollCache,::offset _ rollColumns#tradeAgg; // update de cache – neem roll kolommen uit aggTable, snijd, voeg in rollCache
  offset::count tradeAgg;
  currSyms::`u#`$();
 }

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

addSyms:{[syms]
  currSyms,::syms; // voegen we toe aan de lijst met bekende
  // voegen we toe aan de tabel sym, tijd en rollColumns door gebruik te maken van algemene toewijzing.
  // De functie ^ plaatst standaardwaarden voor roll kolommen, als het symbool niet in de cache staat. value flip table retourneert een lijst van kolommen in de tabel.
  `tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
 }

En tot slot, de functie upd (de traditionele naam voor deze functie voor Q-diensten), die door de klant wordt aangeroepen om gegevens toe te voegen:

upd:{[tblName;data] // tblName hebben we niet nodig, maar meestal verwerkt de dienst meerdere tabellen 
  tm:exec distinct time from data:() xkey preprocess data; // preprocess & calc time
  updMinute[data] each tm; // voeg gegevens toe voor elke minuut
};
updMinute:{[data;tm]
  if[tmcurrTime; roll tm; currTime::tm]; // veranderen we de minuut indien nodig
  data:select from data where time=tm; // filtering
  if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // nieuwe symbolen
  updateAgg[`tradeAgg;offset+currSyms?syms;data]; // update de geaggregeerde tabel. De functie ? zoekt de index van elementen in de lijst rechts in de lijst links.
 };

Dat is alles. Hier is de volledige code van onze dienst, 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[tmcurrTime; 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. We starten deze in een apart proces (plaats de code in het bestand service.q) en roepen de functie init aan:

q service.q –p 5566

q)init[]

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

h:hopen `:host:5566
h:hopen 5566 // als beide op dezelfde host staan

Laten we eerst een lijst met symbolen maken – 10000 stuks – en een functie toevoegen voor het maken van een willekeurige tabel. 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 ze gemakkelijker in de tabel te vinden zijn. De functie rnd creëert een willekeurige tabel met n rijen, waarbij de tijd varieert van t tot t+25 milliseconden.

Nu kunnen we proberen gegevens naar de service te sturen (laten we de eerste tien uur toevoegen):

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

We kunnen controleren in de service 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 een belastingstest uitvoeren om te ontdekken hoeveel gegevens de service per minuut kan verwerken. Ter herinnering, we hebben een interval voor updates ingesteld van 25 milliseconden. De service moet (gemiddeld) in ieder geval binnen de 20 milliseconden per update blijven om gebruikers de tijd te geven om 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 - dat is twee minuten. We kunnen proberen eerst 1000 regels elke 25 milliseconden te draaien:

start 1000

In mijn geval krijg ik een resultaat van enkele milliseconden per update. Dus ga ik het aantal regels meteen verhogen naar 10.000:

start 10000

Resultaat:

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

Weer niets bijzonders, terwijl dit 24 miljoen regels per minuut is, 400.000 per seconde. Meer dan 25 milliseconden update vertraagde alleen 5 keer, blijkbaar bij het wisselen van minuut. Laten we het verhogen naar 100.000:

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 we zien, maakt de service het net, maar hij slaagt er desalniettemin in om boven water te blijven. Dit volume gegevens (240 miljoen regels per minuut) is enorm, en in dergelijke gevallen is het gebruikelijk om meerdere klonen (of zelfs tientallen klonen) van de service te draaien, waarvan elk alleen een deel van de symbolen verwerkt. Desondanks is het resultaat indrukwekkend voor een geïnterpreteerde taal die zich in de eerste plaats richt op gegevensopslag.

Er kan een vraag rijzen waarom de tijd niet-lineair toeneemt met de grootte van elke update. De reden is dat de compressiefunctie feitelijk een C-functie is die veel efficiënter werkt dan updateAgg. Vanaf een bepaalde grootte van de update (rond de 10.000) bereikt updateAgg zijn plafond en is de uitvoeringstijd niet langer afhankelijk van de grootte van de update. Juist door de voorafgaande stap Q kan de service dergelijke hoeveelheden gegevens verwerken. Dit benadrukt hoe belangrijk het is om, bij het werken met grote gegevens, het juiste algoritme te kiezen. Een ander punt is de juiste opslag van gegevens in het geheugen. Als de gegevens niet kolomgewijs waren opgeslagen of niet chronologisch waren geordend, zouden we te maken krijgen met iets dat TLB cache miss wordt genoemd – het ontbreken van het geheugenadres in de adrescache van de processor. Het zoeken naar een adres kost ongeveer 30 keer meer tijd bij een mislukking en in het geval van verspreide gegevens kan het de service meerdere keren vertragen.

Conclusie

In dit artikel heb ik laten zien dat de KDB+ database en Q niet alleen geschikt zijn voor het opslaan van grote gegevens en het eenvoudige toegang er toe via select, maar ook voor het creëren van gegevensverwerkingsdiensten die honderden miljoenen regels/gigabytes aan gegevens kunnen verwerken, zelfs in één enkele Q-proces. De taal Q maakt het mogelijk om algoritmen die verband houden met gegevensverwerking buitengewoon kort en efficiënt te implementeren dankzij zijn vectoriële aard, ingebouwde SQL-dialectinterpretator en een zeer handige set bibliotheekfuncties.

Ik wil opmerken dat het bovenstaande slechts een deel van de mogelijkheden van Q is; het heeft ook andere unieke kenmerken. Bijvoorbeeld, een extreem eenvoudig IPC-protocol dat de grens tussen afzonderlijke Q-processen vervaagt en het mogelijk maakt om honderden van deze processen te combineren in een enkel netwerk, dat zich op tientallen servers over de hele wereld kan bevinden.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster