À propos de ce qu'est la base KDB+, le langage de programmation Q, ainsi que de leurs forces et faiblesses, vous pouvez lire dans mon précédent et brièvement dans l'introduction. Dans cet article, nous allons mettre en place un service en Q qui traitera un flux de données entrant et calculera différentes fonctions agrégées par minute en temps réel (c'est-à-dire qu'il sera capable de faire tous les calculs avant la prochaine portion de données). La principale particularité de Q est qu'il s'agit d'un langage vectoriel qui permet de manipuler non pas des objets uniques, mais des tableaux, des tableaux de tableaux et d'autres objets composites complexes. Des langages comme Q et ses parents K, J, APL sont célèbres pour leur concision. Il n'est pas rare qu'un programme occupant plusieurs écrans de code dans un langage classique comme Java puisse être écrit avec eux en quelques lignes seulement. C'est justement cela que je veux démontrer dans cet article.

Introduction
KDB+ est une base de données en colonnes conçue pour des volumes de données très importants, organisés d'une manière spécifique (principalement par temps). Il est principalement utilisé dans les organisations financières – banques, fonds d'investissement, compagnies d'assurance. Le langage Q est le langage interne de KDB+ qui permet de travailler efficacement avec ces données. L'idéologie de Q est la concision et l'efficacité, au détriment de la clarté. Cela se justifie par le fait qu'un langage vectoriel sera de toute façon difficile à comprendre, et la concision permet de visualiser sur un seul écran une plus grande partie du programme, ce qui en facilite la compréhension.
Dans cet article, nous allons réaliser un programme complet en Q et il se peut que vous souhaitiez l'essayer en pratique. Pour cela, vous aurez besoin de Q. Vous pouvez télécharger la version 32 bits gratuite sur le site de l'entreprise kx – . Là-bas, si vous êtes intéressé, vous trouverez des informations de référence sur Q, le livre et divers articles sur ce sujet.
Définition du problème
Il existe une source qui envoie une table de données toutes les 25 millisecondes. Étant donné que KDB+ est principalement utilisé dans la finance, considérons que c'est une table de transactions (trades), qui contient les colonnes suivantes : time (temps en millisecondes), sym (symbole de l'entreprise en bourse – IBM, AAPL,…), prix (le prix auquel les actions ont été achetées), taille (la taille de la transaction). L'intervalle de 25 millisecondes a été choisi arbitrairement, il n'est ni trop petit ni trop grand. Cela signifie que les données arrivent dans le service déjà mises en mémoire tampon. Il aurait été facile de mettre en œuvre une mise en mémoire tampon côté service, y compris dynamique, en fonction de la charge actuelle, mais pour des raisons de simplicité, nous allons nous arrêter à un intervalle fixe.
Le service doit calculer par minute pour chaque symbole entrant de la colonne sym un ensemble de fonctions d'agrégation - prix max, prix moyen, somme taille, etc. informations utiles. Pour simplifier, nous supposerons que toutes les fonctions peuvent être calculées de manière incrémentale, c'est-à-dire que pour obtenir une nouvelle valeur, il suffit de connaître deux nombres - l'ancienne valeur et la valeur entrante. Par exemple, les fonctions max, moyenne, somme ont cette propriété, tandis que la fonction médiane ne l'a pas.
Nous supposerons également que le flux de données entrant est ordonné dans le temps. Cela nous permettra de travailler uniquement avec la dernière minute. En pratique, il suffit de pouvoir travailler avec la minute actuelle et la minute précédente, au cas où certaines mises à jour seraient retardées. Pour simplifier, nous ne considérerons pas ce cas.
Fonctions d'agrégation
Voici une liste des fonctions d'agrégation nécessaires. J'en ai pris autant que possible pour augmenter la charge sur le service :
- high – prix max – prix maximum par minute.
- low – prix min – prix minimum par minute.
- firstPrice – premier prix – premier prix par minute.
- lastPrice – dernier prix – dernier prix par minute.
- firstSize – première taille – première taille de transaction par minute.
- lastSize – dernière taille — dernière taille de transaction par minute.
- numTrades – nombre i – nombre de transactions par minute.
- volume – somme taille – somme des tailles de transactions par minute.
- pvolume – somme prix – somme des prix par minute, nécessaire pour avgPrice.
- turnover – somme prix*taille – volume total des transactions par minute.
- avgPrice – pvolume%numTrades – prix moyen par minute.
- avgSize – volume%numTrades – taille moyenne de transaction par minute.
- vwap – turnover%volume – prix moyen pondéré par la taille de la transaction par minute.
- cumVolume – somme volume – taille cumulée des transactions pour toute la durée.
Discutons dès maintenant d'un point non évident : comment initialiser ces colonnes pour la première fois et pour chaque minute suivante. Certaines colonnes comme firstPrice doivent être initialisées à la valeur null à chaque fois, leur valeur n'étant pas définie. D'autres comme volume doivent toujours être fixées à 0. Il y a aussi des colonnes qui nécessitent une approche combinée : par exemple, cumVolume doit être copié à partir de la minute précédente, tandis que pour la première, il doit être fixé à 0. Nous allons définir tous ces paramètres en utilisant le type de données dictionnaire (analogue à une paire clé-valeur) :
// 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 объяснен ниже
J'ai ajouté sym et time dans le dictionnaire pour plus de commodité, maintenant initWith est une ligne prête à partir de la table agrégée finale, où il reste à définir les bons sym et time. On peut l'utiliser pour ajouter de nouvelles lignes dans la table.
aggCols nous sera nécessaire lors de la création de la fonction d'agrégation. La liste doit être inversée en raison des particularités de l'ordre de calcul des expressions dans Q (de droite à gauche). L'objectif est d'assurer le calcul dans la direction de high à cumVolume, car certaines colonnes dépendent des précédentes.
Les colonnes à copier dans la nouvelle minute depuis la précédente, la colonne sym a été ajoutée pour plus de commodité :
rollColumns:`sym`cumVolume;
Nous allons maintenant diviser les colonnes en groupes selon comment elles doivent être mises à jour. Trois types peuvent être distingués :
- Accumulateurs (volume, turnover,..) – nous devons ajouter la valeur entrante à la précédente.
- Avec un point particulier (high, low, ..) – la première valeur dans la minute est prise à partir des données entrantes, les autres étant calculées à l'aide de la fonction.
- Les autres. Sont toujours calculées à l'aide de la fonction.
Définissons les variables pour ces classes :
accumulatorCols:`numTrades`volume`pvolume`turnover;
specialCols:`high`low`firstPrice`firstSize;
Ordre des calculs
Nous allons mettre à jour la table agrégée en deux étapes. Pour plus d'efficacité, nous allons d'abord réduire la table d'entrée pour qu'il ne reste qu'une ligne pour chaque symbole et chaque minute. Le fait que toutes nos fonctions soient incrémentales et associatives garantit que le résultat de cette étape supplémentaire ne changera pas. Nous pourrions réduire la table à l'aide d'un select :
select high:max price, low:min price … by sym,time.minute from table
Cette méthode a un inconvénient : l'ensemble des colonnes calculées est défini à l'avance. Heureusement, dans Q, le select est également implémenté comme une fonction, où l'on peut insérer des arguments créés dynamiquement.
?[table;whereClause;byClause;selectClause]
Je ne vais pas décrire en détail le format des arguments, dans notre cas, les seules expressions non triviales seront by et select et elles doivent être des dictionnaires du type columns!expressions. Ainsi, la fonction de compression peut être définie comme suit :
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 est une fonction map dans Q pour une liste
déprétraitement:? [;();`sym`time!`sym`time.minute;selExpression];
Pour plus de clarté, j'ai utilisé la fonction parse, qui transforme une chaîne avec une expression Q en une valeur pouvant être transmise à la fonction eval et dont nous avons besoin dans le select fonctionnel. Notons également que le prétraitement est défini comme une projection (c'est-à-dire une fonction avec des arguments partiellement définis) de la fonction select, un argument (la table) est manquant. Si nous appliquons le prétraitement à la table, nous obtiendrons une table compressée.
La deuxième étape consiste à mettre à jour la table agrégée. Écrivons d'abord l'algorithme en pseudocode :
pour chaque sym dans inputTable
idx: index de ligne dans la table agg pour sym+currentTime;
aggTable[idx;`high]: aggTable[idx;`high] | inputTable[sym;`high];
aggTable[idx;`volume]: aggTable[idx;`volume] + inputTable[sym;`volume];
…
Dans Q, il est habituel d'utiliser des fonctions map/reduce au lieu de boucles. Mais comme Q est un langage vectoriel et que toutes les opérations peuvent être appliquées simultanément à tous les symboles, dans un premier temps, nous pouvons nous passer complètement de boucle, en effectuant des opérations sur tous les symboles en même temps :
idx:calcIdx inputTable;
row:aggTable idx;
aggTable[idx;`high]: row[`high] | inputTable`high;
aggTable[idx;`volume]: row[`volume] + inputTable`volume;
…
Mais nous pouvons aller plus loin, dans Q, il existe un opérateur unique et extrêmement puissant – l'opérateur d'affectation généralisée. Il permet de modifier un ensemble de valeurs dans une structure de données complexe en utilisant une liste d'index, de fonctions et d'arguments. Dans notre cas, il se présente comme suit :
idx:calcIdx inputTable;
rows:aggTable idx;
// .[target;(idx0;idx1;..);function;argument] ~ target[idx 0;idx 1;…]: function[target[idx 0;idx 1;…];argument], dans notre cas, la fonction est l'affectation
.[aggTable;(idx;aggCols);:;flip (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];
Malheureusement, pour affecter dans la table, une liste de lignes est nécessaire, et il faut donc transposer la matrice (liste de colonnes en liste de lignes) à l'aide de la fonction flip. Pour une grande table, cela devient coûteux, donc à la place, nous appliquerons l'affectation généralisée à chaque colonne séparément, en utilisant la fonction map (qui ressemble à une apostrophe) :
.[aggTable;;:;]'[(idx;)each aggCols; (row[`high] | inputTable`high;row[`volume] + inputTable`volume;…)];
Nous utilisons à nouveau la projection de fonction. Notez également qu'en Q, la création d'une liste est aussi une fonction et nous pouvons l'appeler à l'aide de la fonction each(map) pour obtenir une liste de listes.
Pour que l'ensemble des colonnes calculées ne soit pas fixe, créons dynamiquement l'expression ci-dessus. Nous allons d'abord définir des fonctions pour le calcul de chaque colonne, en utilisant les variables row et inp pour faire référence aux données agrégées et d'entrée :
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");
Certaines colonnes sont spéciales, leur première valeur ne doit pas être calculée par la fonction. Nous pouvons déterminer que c'est la première selon la colonne row[`numTrades] – si elle est à 0, alors la valeur est première. En Q, il existe une fonction de sélection — ?[Boolean list;list1;list2] – qui sélectionne une valeur dans la liste 1 ou 2 en fonction de la condition dans le premier argument :
// high -> ?[isFirst;inp`high;row[`high]|inp`high]
// @ - тоже обобщенное присваивание для случая когда индекс неглубокий
@[`aggExpression;specialCols;{[x;y]"?[isFirst;inp`",y,";",x,"]"};string specialCols];
Ici, j'ai appelé une affectation généralisée avec ma fonction (expression entre accolades). Elle reçoit la valeur actuelle (premier argument) et un argument supplémentaire que je passe dans le quatrième paramètre.
Ajoutons également des colonnes d'accumulateur, car pour celles-ci, la fonction est la même :
// volume -> row[`volume]+inp`volume
aggExpression[accumulatorCols]:{"row[`",x,"]+inp`",x } each string accumulatorCols;
C'est une affectation normale au regard de Q, sauf que j'affecte directement une liste de valeurs. Enfin, créons la fonction principale :
// ":",/: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),")]}";
Avec cette expression, je crée dynamiquement une fonction à partir d'une chaîne qui contient l'expression que j'ai donnée ci-dessus. Le résultat ressemblera à ceci :
{[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])]}
L'ordre de calcul des colonnes est inversé, car en Q, l'ordre de calcul se fait de droite à gauche.
Nous avons maintenant deux fonctions principales nécessaires aux calculs, il nous reste à ajouter un peu d'infrastructure et le service sera prêt.
Étapes finales
Nous avons les fonctions preprocess et updateAgg, qui effectuent tout le travail. Mais il est également nécessaire d'assurer une transition correcte à travers les minutes et de calculer les index pour l'agrégation. Tout d'abord, définissons la fonction init :
init:{
tradeAgg:: 0#enlist[initWith]; // créons un tableau typé vide, enlist transforme le dictionnaire en tableau, et 0# signifie prendre 0 éléments de celui-ci
currTime::00:00; // commençons à 0, :: signifie que l'attribution se fait dans une variable globale
currSyms::`u#`symbol$(); // `u# - transforme la liste en arbre, pour accélérer la recherche d'éléments
offset::0; // index dans tradeAgg, où commence la minute actuelle
rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // cache pour les dernières valeurs des colonnes roll, tableau avec la clé sym
}
Nous allons également définir la fonction roll, qui changera la minute actuelle :
roll:{[tm]
if[currTime>tm; :init[]]; // si nous avons dépassé minuit, appelons simplement init
rollCache,::offset _ rollColumns#tradeAgg; // mettons à jour le cache – prenons les colonnes roll depuis aggTable, rognons, insérons dans rollCache
offset::count tradeAgg;
currSyms::`u#`$();
}
Nous aurons besoin d'une fonction pour ajouter de nouveaux symboles :
addSyms:{[syms]
currSyms,::syms; // ajoutons à la liste des connus
// ajoutons dans le tableau sym, time et rollColumns en utilisant l'attribution généralisée.
// La fonction ^ insère des valeurs par défaut pour les colonnes roll, si le symbole n'est pas dans le cache. value flip table renvoie la liste des colonnes dans le tableau.
`tradeAgg upsert @[count[syms]#enlist initWith;`sym`time,cols rc;:;(syms;currTime), (initWith cols rc)^value flip rc:rollCache ([] sym: syms)];
}
Et enfin, la fonction upd (nom traditionnel de cette fonction pour les services Q), qui est appelée par le client, pour ajouter des données :
upd:{[tblName;data] // tblName n'est pas nécessaire, mais le service traite généralement plusieurs tableaux
tm:exec distinct time from data:() xkey preprocess data; // prétraiter & calculez le temps
updMinute[data] each tm; // ajoutons des données pour chaque minute
};
updMinute:{[data;tm]
if[tmcurrTime; roll tm; currTime::tm]; // changeons la minute, si nécessaire
data:select from data where time=tm; // filtrage
if[count msyms:syms where not (syms:data`sym)in currSyms; addSyms msyms]; // nouveaux symboles
updateAgg[`tradeAgg;offset+currSyms?syms;data]; // mettons à jour le tableau agrégé. La fonction ? recherche l'indice des éléments de la liste de droite dans la liste de gauche.
};
Voilà. Voici le code complet de notre service, comme promis, en seulement quelques lignes :
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];
};
Test
Vérifions la performance du service. Pour cela, nous allons le lancer dans un processus séparé (placez le code dans le fichier service.q) et appelez la fonction init:
q service.q –p 5566
q)init[]
Dans une autre console, lancez un deuxième processus Q et connectez-vous au premier :
h:hopen `:host:5566
h:hopen 5566 // si les deux sont sur le même hôte
Commençons par créer une liste de symboles – 10000 pièces et ajoutons une fonction pour générer une table aléatoire. Dans la deuxième 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)}
J'ai ajouté trois vrais symboles à la liste pour qu'il soit plus facile de les retrouver dans la table. La fonction rnd crée une table aléatoire avec n lignes, où le temps varie de t à t+25 millisecondes.
Nous pouvons maintenant essayer d'envoyer des données au service (ajoutons les dix premières heures) :
{h (`upd;`trade;rnd[10000;x])} each `time$00:00 + til 60*10
Vous pouvez vérifier dans le service que la table a été mise à jour :
c 25 200
select from tradeAgg where sym=`AAPL
-20#select from tradeAgg where sym=`AAPL
Résultat :
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|2919Nous allons maintenant procéder à un test de charge pour déterminer combien de données le service peut gérer par minute. Rappelons que nous avons établi un intervalle de mise à jour de 25 millisecondes. Par conséquent, le service doit (en moyenne) s'inscrire au moins dans les 20 millisecondes par mise à jour, afin de laisser le temps aux utilisateurs de demander des données. Entrez ce qui suit dans le deuxième processus :
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 – c'est deux minutes. On peut essayer de commencer avec 1000 lignes toutes les 25 millisecondes :
start 1000
Dans mon cas, le résultat est d'environ quelques millisecondes par mise à jour. Donc, j'augmenterai immédiatement le nombre de lignes à 10 000 :
start 10000
Résultat :
min| 00:00:00.004
avg| 9.191458
med| 9f
max| 00:00:00.030
Encore rien de spécial, et pourtant cela représente 24 millions de lignes par minute, soit 400 000 par seconde. Plus de 25 millisecondes de mise à jour n'ont ralenti que 5 fois, apparemment lors du passage à la minute. Augmentons à 100 000 :
start 100000
Résultat :
min| 00:00:00.013
avg| 25.11083
med| 24f
max| 00:00:00.108
q)sum times
00:02:00.532
Comme nous le voyons, le service peine, mais il réussit néanmoins à tenir le coup. Un tel volume de données (240 millions de lignes par minute) est extrêmement élevé, dans ces cas, il est habituel de lancer plusieurs clones (ou même des dizaines de clones) du service, chacun traitant seulement une partie des symboles. Néanmoins, le résultat est impressionnant pour un langage interprété, qui est principalement orienté vers le stockage de données.
On peut se demander pourquoi le temps augmente de manière non linéaire avec la taille de chaque mise à jour. La raison est que la fonction de compression est en réalité une fonction C qui fonctionne beaucoup plus efficacement que updateAgg. À partir d'une certaine taille de mise à jour (environ 10 000), updateAgg atteint son plafond et son temps d'exécution ne dépend plus de la taille de la mise à jour. C'est grâce à l'étape préalable Q que le service est capable de traiter de tels volumes de données. Cela souligne l'importance de choisir le bon algorithme lorsqu'on travaille avec de grandes données. Un autre aspect est le bon stockage des données en mémoire. Si les données n'étaient pas stockées de manière colonne ou si elles n'étaient pas ordonnées dans le temps, nous serions confrontés à ce qu'on appelle un TLB cache miss - une absence d'adresse de page mémoire dans le cache d'adresses du processeur. La recherche d'une adresse prend environ 30 fois plus de temps en cas d'échec et, en cas de données éparpillées, cela peut ralentir le service de plusieurs fois.
Conclusion
Dans cet article, j'ai montré que la base KDB+ et Q ne sont pas seulement adaptées pour stocker de grandes données et y accéder facilement via des sélections, mais aussi pour créer des services de traitement de données capables de traiter des centaines de millions de lignes / des gigaoctets de données même dans un seul processus Q. Le langage Q permet de mettre en œuvre des algorithmes liés au traitement des données de manière extrêmement concise et efficace grâce à sa nature vectorielle, son interpréteur SQL intégré et un ensemble très réussi de fonctions de bibliothèque.
Je noterai que ce qui précède n'est qu'une partie des capacités de Q, il possède d'autres caractéristiques uniques. Par exemple, un protocole IPC extrêmement simple qui efface la frontière entre différents processus Q et permet de combiner des centaines de ces processus en un seul réseau, qui peut être réparti sur des dizaines de serveurs à travers le monde.
Source : habr.com
