Fonctionnalités du langage Q et KDB+ à l'aide de l'exemple d'un service temps réel

Vous pouvez découvrir ce que sont la base KDB+, le langage de programmation Q, quelles sont leurs forces et leurs faiblesses dans mon précédent article et brièvement dans l'introduction. Dans l'article, nous allons implémenter un service sur Q qui traitera le flux de données entrant et calculera diverses fonctions d'agrégation chaque minute en mode « temps réel » (c'est-à-dire qu'il aura le temps de tout calculer avant la prochaine portion de données). La principale caractéristique de Q est qu'il s'agit d'un langage vectoriel qui vous permet d'opérer non pas avec des objets uniques, mais avec leurs tableaux, tableaux de tableaux et autres objets complexes. Des langages tels que Q et ses parents K, J, APL sont réputés pour leur brièveté. Souvent, un programme qui occupe plusieurs écrans de code dans un langage familier comme Java peut y être écrit en quelques lignes. C'est ce que je souhaite démontrer dans cet article.

Fonctionnalités du langage Q et KDB+ à l'aide de l'exemple d'un service temps réel

introduction

KDB+ est une base de données en colonnes axée sur de très grandes quantités de données, classées de manière spécifique (principalement par temps). Il est principalement utilisé dans les institutions financières – banques, fonds d’investissement, compagnies d’assurance. Le langage Q est le langage interne de KDB+ qui vous permet de travailler efficacement avec ces données. L’idéologie Q est synonyme de brièveté et d’efficacité, tandis que la clarté est sacrifiée. Ceci est justifié par le fait que le langage vectoriel sera de toute façon difficile à comprendre, et la brièveté et la richesse de l'enregistrement permettent de voir une partie beaucoup plus importante du programme sur un seul écran, ce qui le rend finalement plus facile à comprendre.

Dans cet article, nous implémentons un programme à part entière dans Q et vous voudrez peut-être l'essayer. Pour ce faire, vous aurez besoin du véritable Q. Vous pouvez télécharger la version gratuite 32 bits sur le site Web de la société kx – www.kx.com. Vous y trouverez, si vous êtes intéressé, des informations de référence sur Q, le livre Q pour les mortels et divers articles sur ce sujet.

Formulation du problème

Il existe une source qui envoie une table contenant des données toutes les 25 millisecondes. Puisque KDB+ est principalement utilisé en finance, nous supposerons qu'il s'agit d'un tableau de transactions (trades) qui comporte les colonnes suivantes : time (temps en millisecondes), sym (désignation de l'entreprise en bourse - IBM, AAPL,…), prix (le prix auquel les actions ont été achetées), taille (taille de la transaction). L'intervalle de 25 millisecondes est arbitraire, ni trop petit ni trop long. Sa présence signifie que les données arrivent au service déjà mises en mémoire tampon. Il serait facile d'implémenter une mise en mémoire tampon côté service, y compris une mise en mémoire tampon dynamique en fonction de la charge actuelle, mais par souci de simplicité, nous nous concentrerons sur un intervalle fixe.

Le service doit compter chaque minute pour chaque symbole entrant de la colonne sym un ensemble de fonctions d'agrégation - prix maximum, prix moyen, taille de la somme, etc. informations utiles. Pour plus de simplicité, nous supposerons que toutes les fonctions peuvent être calculées de manière incrémentale, c'est-à-dire pour obtenir une nouvelle valeur, il suffit de connaître deux nombres : l'ancienne et la valeur entrante. Par exemple, les fonctions max, moyenne, somme ont cette propriété, mais pas la fonction médiane.

Nous supposerons également que le flux de données entrant est ordonné dans le temps. Cela nous donnera la possibilité de travailler uniquement à la dernière minute. En pratique, il suffit de pouvoir travailler avec les minutes en cours et précédentes au cas où certaines mises à jour seraient en retard. Par souci de simplicité, nous ne considérerons pas ce cas.

Fonctions d'agrégation

Les fonctions d'agrégation requises sont répertoriées ci-dessous. J'en ai pris le plus possible pour augmenter la charge du service :

  • élevé – prix maximum – prix maximum par minute.
  • bas – prix minimum – 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 en une minute.
  • numTrades – compte i – nombre de transactions par minute.
  • volume – taille de la somme – somme des tailles de transactions par minute.
  • pvolume – somme du prix – somme des prix par minute, requis pour avgPrice.
  • – somme du prix du chiffre d'affaires*taille – volume total des transactions par minute.
  • avgPrice – pvolume%numTrades – prix moyen par minute.
  • avgSize – volume%numTrades – taille moyenne des transactions par minute.
  • vwap – chiffre d’affaires % volume – prix moyen par minute pondéré par la taille de la transaction.
  • cumVolume – volume total – taille accumulée des transactions sur tout le temps.

Discutons immédiatement d'un point non évident : comment initialiser ces colonnes pour la première fois et pour chaque minute suivante. Certaines colonnes de type firstPrice doivent être initialisées à null à chaque fois ; leur valeur n'est pas définie. Les autres types de volumes doivent toujours être définis sur 0. Il existe également des colonnes qui nécessitent une approche combinée - par exemple, cumVolume doit être copié à partir de la minute précédente et pour la première définie sur 0. Définissons tous ces paramètres à l'aide des données du dictionnaire tapez (analogue à un enregistrement) :

// 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 au dictionnaire pour plus de commodité, maintenant initWith est une ligne prête à l'emploi de la table agrégée finale, où il reste à définir le sym et l'heure corrects. Vous pouvez l'utiliser pour ajouter de nouvelles lignes à un tableau.

Nous aurons besoin de aggCols lors de la création d’une fonction d’agrégation. La liste doit être inversée en raison de l'ordre dans lequel les expressions de Q sont évaluées (de droite à gauche). Le but est d'assurer le calcul dans le sens de high vers cumVolume, puisque certaines colonnes dépendent des précédentes.

Colonnes qui doivent être copiées dans une nouvelle minute à partir de la précédente, la colonne sym est ajoutée pour plus de commodité :

rollColumns:`sym`cumVolume;

Divisons maintenant les colonnes en groupes selon la manière dont elles doivent être mises à jour. Trois types peuvent être distingués :

  1. Accumulateurs (volume, chiffre d’affaires,..) – il faut ajouter la valeur entrante à la précédente.
  2. Avec un point spécial (haut, bas, ..) – la première valeur de la minute est extraite des données entrantes, le reste est calculé à l'aide de la fonction.
  3. Repos. Toujours calculé à l'aide d'une fonction.

Définissons les variables pour ces classes :

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

Ordre de calcul

Nous mettrons à jour le tableau agrégé en deux étapes. Pour plus d'efficacité, nous réduisons d'abord le tableau entrant afin qu'il n'y ait qu'une seule ligne pour chaque caractère et 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. Vous pouvez réduire la table en utilisant select :

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

Cette méthode présente un inconvénient : l'ensemble des colonnes calculées est prédéfini. Heureusement, dans Q, select est également implémenté en tant que fonction dans laquelle vous pouvez remplacer des arguments créés dynamiquement :

?[table;whereClause;byClause;selectClause]

Je ne décrirai pas en détail le format des arguments ; dans notre cas, seules les expressions by et select seront non triviales et doivent être des dictionnaires de la forme colonnes!expressions. Ainsi, la fonction de retrait 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 это функция map в Q для одного списка
preprocess:?[;();`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 qui peut être transmise à la fonction eval et qui est requise dans la fonction select. Notez é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 un prétraitement à une table, nous obtiendrons une table compressée.

La deuxième étape consiste à mettre à jour le tableau agrégé. Écrivons d'abord l'algorithme en pseudocode :

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

Dans Q, il est courant d'utiliser des fonctions map/reduce au lieu de boucles. Mais puisque Q est un langage vectoriel et que nous pouvons facilement appliquer toutes les opérations à tous les symboles à la fois, alors en première approximation, nous pouvons nous passer du tout de boucle, en effectuant des opérations sur tous les symboles à la fois :

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

Mais on peut aller plus loin, Q possède un opérateur unique et extrêmement puissant : l'opérateur d'affectation généralisée. Il vous permet de modifier un ensemble de valeurs dans une structure de données complexe à l'aide d'une liste d'indices, de fonctions et d'arguments. Dans notre cas, cela ressemble à ceci :

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

Malheureusement, pour attribuer à un tableau, vous avez besoin d'une liste de lignes, pas de colonnes, et vous devez transposer la matrice (liste de colonnes en liste de lignes) à l'aide de la fonction flip. Cela coûte cher pour une grande table, nous appliquons donc à la place une 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 fonctions. Notez également que dans Q, créer une liste est aussi une fonction et nous pouvons l'appeler en utilisant la fonction each(map) pour obtenir une liste de listes.

Pour nous assurer que l'ensemble des colonnes calculées n'est pas fixe, nous allons créer dynamiquement l'expression ci-dessus. Définissons d'abord les fonctions pour calculer 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 qu'il s'agit du premier grâce à la colonne row[`numTrades] - si elle contient 0, alors la valeur est la première. Q a 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 du 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 (une expression entre accolades). Il reçoit la valeur actuelle (le premier argument) et un argument supplémentaire, que je passe en 4ème paramètre.

Ajoutons les haut-parleurs à batterie séparément, puisque la fonction est la même pour eux :

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

Il s'agit d'une affectation normale selon les normes Q, mais j'attribue une liste de valeurs à la fois. 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 d'évaluation des colonnes est inversé car dans Q l'ordre d'évaluation est de droite à gauche.

Nous disposons désormais de deux fonctions principales nécessaires aux calculs, il suffit d'ajouter un peu d'infrastructure et le service est prêt.

Étapes finales

Nous avons des fonctions de prétraitement et de mise à jour qui font tout le travail. Mais encore faut-il assurer le bon passage des minutes et calculer les indices d'agrégation. Tout d'abord, définissons la fonction init :

init:{
  tradeAgg:: 0#enlist[initWith]; // создаем пустую типизированную таблицу, enlist превращает словарь в таблицу, а 0# означает взять 0 элементов из нее
  currTime::00:00; // начнем с 0, :: означает, что присваивание в глобальную переменную
  currSyms::`u#`symbol$(); // `u# - превращает список в дерево, для ускорения поиска элементов
  offset::0; // индекс в tradeAgg, где начинается текущая минута 
  rollCache:: `sym xkey update `u#sym from rollColumns#tradeAgg; // кэш для последних значений roll колонок, таблица с ключом sym
 }

Nous définirons également la fonction roll, qui changera la minute en cours :

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

Nous aurons besoin d'une fonction pour ajouter de nouveaux caractères :

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

Et enfin, la fonction upd (le 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 нам не нужно, но обычно сервис обрабатывает несколько таблиц 
  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]; // обновим агрегированную таблицу. Функция ? ищет индекс элементов списка справа в списке слева.
 };

C'est tout. Voici le code complet de notre service, comme promis, en quelques lignes seulement :

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 les performances du service. Pour ce faire, exécutons-le dans un processus séparé (mettez le code dans le fichier service.q) et appelons la fonction init :

q service.q –p 5566

q)init[]

Dans une autre console, démarrez le deuxième processus Q et connectez-vous au premier :

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

Tout d'abord, créons une liste de symboles - 10000 XNUMX pièces et ajoutons une fonction pour créer 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 faciliter leur recherche dans le tableau. La fonction rnd crée une table aléatoire avec n lignes, où le temps varie de t à t+25 millisecondes.

Vous pouvez maintenant essayer d'envoyer des données au service (ajoutez 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 bien é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|2919

Effectuons maintenant des tests de charge pour déterminer la quantité de données que le service peut traiter par minute. Permettez-moi de vous rappeler que nous avons fixé l'intervalle de mise à jour à 25 millisecondes. En conséquence, le service doit (en moyenne) tenir dans au moins 20 millisecondes par mise à jour pour donner aux utilisateurs le temps de demander des données. Saisissez 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 équivaut à deux minutes. Vous pouvez essayer d'exécuter d'abord 1000 25 lignes toutes les XNUMX millisecondes :

start 1000

Dans mon cas, le résultat est d'environ quelques millisecondes par mise à jour. Je vais donc immédiatement augmenter le nombre de lignes à 10.000 XNUMX :

start 10000

Résultat:

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

Encore une fois, rien de spécial, mais cela représente 24 millions de lignes par minute, soit 400 25 par seconde. Pendant plus de 5 millisecondes, la mise à jour n'a ralenti que 100.000 fois, apparemment lorsque les minutes changeaient. Passons à XNUMX :

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 vous pouvez le constater, le service peut à peine faire face, mais il parvient néanmoins à rester à flot. Un tel volume de données (240 millions de lignes par minute) est extrêmement important ; dans de tels cas, il est courant de lancer plusieurs clones (voire des dizaines de clones) du service, dont chacun ne traite qu'une partie des caractères. Pourtant, le résultat est impressionnant pour un langage interprété qui se concentre principalement sur le stockage de données.

La question peut se poser de savoir pourquoi le temps augmente de manière non linéaire avec la taille de chaque mise à jour. La raison en est que la fonction de réduction est en fait une fonction C, qui est beaucoup plus efficace que updateAgg. A partir d'une certaine taille de mise à jour (environ 10.000 30), updateAgg atteint son plafond et son temps d'exécution ne dépend alors pas de la taille de la mise à jour. C'est grâce à l'étape préliminaire Q que le service est capable de digérer de tels volumes de données. Cela souligne à quel point il est important de choisir le bon algorithme lorsque l’on travaille avec du Big Data. Un autre point est le stockage correct des données en mémoire. Si les données n'étaient pas stockées en colonnes ou n'étaient pas classées par temps, nous nous familiariserions alors avec un échec de cache TLB - l'absence d'adresse de page mémoire dans le cache d'adresses du processeur. La recherche d'une adresse prend environ XNUMX fois plus de temps en cas d'échec, et si les données sont dispersées, cela peut ralentir le service plusieurs fois.

Conclusion

Dans cet article, j'ai montré que les bases de données KDB+ et Q sont adaptées non seulement pour stocker des données volumineuses et y accéder facilement via select, mais également pour créer des services de traitement de données capables de digérer des centaines de millions de lignes/gigaoctets de données même dans un seul processus Q. Le langage Q lui-même permet une implémentation extrêmement concise et efficace d'algorithmes liés au traitement des données en raison de sa nature vectorielle, de son interpréteur de dialecte SQL intégré et d'un ensemble très performant de fonctions de bibliothèque.

Je noterai que ce qui précède n'est qu'une partie de ce que Q peut faire, il possède également d'autres fonctionnalités uniques. Par exemple, un protocole IPC extrêmement simple qui efface les frontières entre les processus Q individuels et vous permet de combiner des centaines de ces processus en un seul réseau, qui peut être situé sur des dizaines de serveurs dans différentes parties du monde.

Source: habr.com

Ajouter un commentaire