#Performances

0 Abonnés · 26 Publications

La balise Performance regroupe les publications concernant les problèmes de performance des logiciels et les meilleures pratiques pour résoudre et surveiller les problèmes de performance.

Article Iryna Mykhailova · Août 28, 2025 12m read

Nous présentons ici le processus d'utilisation de la célèbre solution Jaeger pour tracer les applications InterSystems IRIS. Jaeger est un produit open source permettant de suivre et d'identifier des problèmes, en particulier dans les environnements distribués et de microservices. Ce backend de traçage, apparu chez Uber en 2015, a été inspiré par Dapper de Google et OpenZipkin de Twitter. Il a ensuite rejoint la Fondation Cloud Native Computing (CNCF) en tant que projet en incubation en 2017, avant d'obtenir le statut gradué en 2019. Ce guide vous montrera comment utiliser la solution Jaeger conteneurisée intégrée à IRIS.

Caractéristiques du Jaeger

  1. Surveillance des flux de transactions exécutés par une ou plusieurs applications et composants (services) dans des environnements conventionnels ou distribués:
  2. Identification précise des goulots d'étranglement dans les flux métier, y compris ceux qui sont distribués:
  3. Étude et optimisation des dépendances entre services, composants, classes et méthodes:
  4. Identification des indicateurs de performance pour découvrir les possibilités d'amélioration:

Composants Jaeger



Extrait de la documentation Jaeger: https://www.jaegertracing.io/docs/1.23/architecture/

  1. Client: Solutions, applications ou technologies (telles que IRIS) qui envoient des données de surveillance à Jaeger.
  2. Agent: Démon réseau qui surveille les segments envoyés via UDP, les regroupe et les transmet au collecteur. Il est conçu pour être déployé sur tous les hôtes en tant que composant d'infrastructure, en abstraisant le routage et la découverte des collecteurs depuis le client.
  3. Collecteur: Récepteur des traces provenant des agents Jaeger, qui les traite via un pipeline de traitement , lequel valide les traces, les indexe, effectue les transformations nécessaires, puis les stocke.
  4. Agent d'ingestion (facultatif): Si une file d'attente de messages telle qu'Apache Kafka tamponne les données entre le collecteur et le stockage, l'agent d'ingestion Jaeger lit les données dans Kafka et les écrit dans le stockage.
  5. Requête: Service qui récupère les traces du stockage et héberge une interface utilisateur pour les afficher.
  6. Interface utilisateur: L'interface Web Jaeger pour analyser et tracer les flux de transactions.

Aperçu d'Open Telemetry (OTel)

Étant donné qu'OpenTelemetry est la technologie exploitée par IRIS pour envoyer des données de traçage à Jaeger, il est important de comprendre son fonctionnement.
OpenTelemetry (alias OTel) est un framework d'observabilité open source et fournisseur neutre permettant d'instrumenter, de générer, de collecter et d'exporter des données de télémétrie telles que des traces, des métriques et des logs. Dans cet article, nous nous concentrerons sur la fonctionnalité traces.
L'unité fondamentale de données dans OpenTelemetry est le “signal”. L'objectif d'OpenTelemetry est de collecter, traiter et exporter ces signaux, qui sont des sorties système décrivant l'activité sous-jacente du système d'exploitation et des applications s'exécutant sur une plateforme. Un signal peut être quelque chose que vous souhaitez mesurer à un moment précis (par exemple, la température, l'utilisation de la mémoire) ou un événement qui traverse les composants de votre système distribué que vous souhaitez tracer. Vous pouvez regrouper différents signaux afin d'observer le fonctionnement interne d'une même technologie sous plusieurs angles (source: https://opentelemetry.io/docs/concepts/signals/). Cet article explique la manière d'émettre des signaux associés à des traces (le cheminement d'une requête dans votre application) depuis IRIS vers les collecteurs OTel.
OpenTelemetry est un excellent choix pour surveiller et tracer votre environnement IRIS et votre code source, car il est pris en charge par plus de 40 fournisseurs d'observabilité. Il est également intégré à de nombreuses bibliothèques, services et applications, et adopté par de nombreux utilisateurs finaux (source: https://opentelemetry.io/docs/).

  • Les microservices développés en Java, .NET, Python, NodeJS, InterSystems ObjectScript et des dizaines d'autres langages peuvent envoyer des données de télémétrie à un collecteur OTel à l'aide des points de terminaison et des ports distants (dans notre exemple, nous utiliserons un port HTTP).
  • Les composants de l'infrastructure peuvent également envoyer des données, notamment des données sur les performances, l'utilisation des ressources (processeur, mémoire, etc.) et d'autres informations pertinentes pour la surveillance de ces composants. Pour InterSystems IRIS, les données sont collectées par Prometheus à partir du point de terminaison /monitor et peuvent être transférées vers un collecteur OTel.
  • Les API et les outils de base de données peuvent également envoyer des données de télémétrie. Certains produits de base de données sont capables de le faire automatiquement (instrumentalisation).
  • Le collecteur OTel reçoit les données OTel et les stocke dans une base de données compatible et/ou les transmet à des outils de surveillance (par exemple, Jaeger).


Comment InterSystems IRIS envoie des données de surveillance à Jaeger

À partir de la version 2025, InterSystems a lancé la prise en charge d'OpenTelemetry (OTel) dans son API de surveillance. Cette nouvelle fonctionnalité inclut l'émission de données de télémétrie pour le traçage, la journalisation et les métriques d'environnement. Cet article traite de l'envoi de données de traçage à Jaeger via OTel. Vous trouverez plus de détails ici: https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=AOTEL&ADJUST=1.
Bien que certains langages de programmation et certaines technologies prennent en charge la transmission automatique des données OTel (instrumentation automatique), pour IRIS, vous devez écrire des instructions de code spécifiques pour activer cette fonctionnalité. Téléchargez le code source de l'application exemple à partir de https://openexchange.intersystems.com/package/iris-telemetry-sample, ouvrez votre IDE et suivez les étapes ci-dessous:

1. Accédez à la classe dc.Sample.REST.TelemetryUtil. La méthode SetTracerProvider initialise un fournisseur de traceurs, ce qui vous permet de définir le nom et la version du service surveillé:

/// Définition du fournisseur de traceursClassMethod SetTracerProvider(ServiceName As%String, Version As%String) As%Status
{
    Set sc = $$$OKset attributes("service.name") = ServiceName
    set attributes("service.version") = Version
    Set tracerProv = ##class(%Trace.TracerProvider).%New(.attributes)
    Set sc = ##class(%Trace.Provider).SetTracerProvider(tracerProv)
    Quit sc
}

2. L'étape suivante consiste à récupérer l'instance du fournisseur de traceurs créée:
 

/// Obtention du fournisseur de traceursClassMethod GetTracerProvider() As%Trace.Provider
{
    Return##class(%Trace.Provider).GetTracerProvider()
}

3. Cet exemple va surveiller l'API PersonREST sur le point de terminaison /persons/all. Accédez à la classe dc.Sample.PersonREST (méthode de classe GetAllPersons):

/// Récupération de tous les enregistrements de dc.Sample.PersonClassMethod GetAllPersons() As%Status
{
<span class="hljs-keyword">#dim</span> tSC <span class="hljs-keyword">As</span> <span class="hljs-built_in">%Status</span> = <span class="hljs-built_in">$$$OK</span>
<span class="hljs-keyword">do</span> <span class="hljs-keyword">##class</span>(dc.Sample.REST.TelemetryUtil).SetTracerProvider(<span class="hljs-string">"Get.All.Persons"</span>, <span class="hljs-string">"1.0"</span>)
<span class="hljs-keyword">set</span> tracerProv = <span class="hljs-keyword">##class</span>(dc.Sample.REST.TelemetryUtil).GetTracerProvider()
<span class="hljs-keyword">set</span> tracer = tracerProv.GetTracer(<span class="hljs-string">"Get.All.Persons"</span>, <span class="hljs-string">"1.0"</span>)

4. Le fournisseur de traceurs vient de créer un service de surveillance appelé Get.All.Persons (version 1.0) et a obtenu l'instance de traceurs avec GetTracer.
5. L'exemple doit créer un racine Span comme suit:
 

set rootAttr("GetAllPersons") = 1set rootSpan = tracer.StartSpan("GetAllPersons", , "Server", .rootAttr)
    set rootScope = tracer.SetActiveSpan(rootSpan)

6. Les spans sont des éléments du flux de traçage. Chaque span doit être mappé à un élément du code source que vous souhaitez analyser.
7. SetActiveSpan est obligatoire pour définir le span actuel à surveiller.
8. À présent, l'exemple crée plusieurs spans descendants mappés à des éléments importants du flux:
 

set childSpan1 = tracer.StartSpan("Query.All.Persons")
    set child1Scope = tracer.SetActiveSpan(childSpan1)
    Try {
        Set rset = ##class(dc.Sample.Person).ExtentFunc()
        do childSpan1.SetStatus("Ok")
    } Catch Ex {
        do childSpan1.SetStatus("Error")
    }
    do childSpan1.End()
    kill childSpan1

9. Ce premier span des descendant surveille la requête pour toutes les personnes dans la base de données. Pour créer un span descendant, l'exemple utilise StartSpan avec un titre suggéré (Query.All.Persons). Le span actif doit ensuite être défini sur le span descendant actuel, ce que l'exemple réalise à l'aide de SetActiveSpan avec la référence childSpan1.
10. L'exemple exécute le code source métier (Set rset = ##class(dc.Sample.Person).ExtentFunc()). Si l'opération réussit, il définit le statut sur "Ok" ; sinon, il le définit sur "Error."
11. L'exemple termine la surveillance de ce morceau de code à l'aide de la méthode End et en supprimant la référence childSpan1.
12. Vous pouvez répéter cette procédure pour tous les autres segments de code que vous souhaitez examiner:

Pour surveiller la détection d'une personne par son ID (obtenir les détails de la personne):

set childSpan2 = tracer.StartSpan("Get.PersonByID")
set child2Scope = tracer.SetActiveSpan(childSpan2)
Set person = ##class(dc.Sample.Person).%OpenId(rset.ID)

       Pour observer le calcul de l'âge (classe dc.Sample.Person, méthode CalculateAge):
 

set tracerProv = ##class(dc.Sample.REST.TelemetryUtil).GetTracerProvider()
set tracer = tracerProv.GetTracer("Get.All.Persons", "1.0")
set childSpan1 = tracer.StartSpan("CalculateAge")
set child1Scope = tracer.SetActiveSpan(childSpan1)

Pour vérifier la définition du signe du zodiaque (classe dc.Sample.Person, méthode CalculateZodiacSign):
 

set tracerProv = ##class(dc.Sample.REST.TelemetryUtil).GetTracerProvider()
set tracer = tracerProv.GetTracer("Get.All.Persons", "1.0")
set childSpan1 = tracer.StartSpan("GetZodiacSign")
set child1Scope = tracer.SetActiveSpan(childSpan1)

 
Configuration des conteneurs IRIS et Jaeger

1. Créez le conteneur collecteur OTel (dans docker-composer.yml):
 

# --- 2. Collecteur OpenTelemetry ---  otel-collector:    image:otel/opentelemetry-collector-contrib:latest    command:["--config=/etc/otel-collector-config.yml"]    volumes:      -./otel-collector-config.yml:/etc/otel-collector-config.yml    ports:      -"4317:4317"# OTLP gRPC       -"4318:4318"# OTLP HTTP      -"9464:9464"# Métriques    depends_on:      -iris      -jaeger

2. Ajustez le fichier de configuration du collecteur OTel:

receivers:  otlp:    protocols:      grpc:        endpoint:"0.0.0.0:4317"      http:        endpoint:"0.0.0.0:4318"exporters:  otlp:    endpoint:jaeger:4317# Le nom du service « jaeger » de docker-compose pour le collecteur gRPC    tls:      insecure:true  prometheus:    endpoint:"0.0.0.0:9464"  debug:{}processors:  batch:# Processeur pour regrouper des traces en lots    send_batch_size:100    timeout:10sconnectors:  spanmetrics:# Connecteur SpanMetricsservice:  pipelines:    traces:      receivers:[otlp]      processors:[batch]# Les traces sont traitées pour générer des métriques      exporters:[otlp,spanmetrics]    metrics:      receivers:[otlp,spanmetrics]      exporters:[prometheus]    logs:      receivers:[otlp]      exporters:[debug]

3. Le collecteur OTel recevra les données de surveillance à l'adresse suivante:

http:        endpoint:"0.0.0.0:4318"

4. Le collecteur OTel enverra les exportateurs (Exporters) à Jaeger comme indiqué ci-dessous:

exporters:
  otlp:
    endpoint: jaeger:4317 # O nome do serviço 'jaeger' do docker-compose para o Collector gRPC
    tls:
      insecure: true

5. Le service OTel créera un pipeline pour recevoir et envoyer les données de surveillance:
 

service:  pipelines:    traces:      receivers:[otlp]      processors:[batch]      exporters:[otlp]

6. Dans le fichier docker-compose.yml, l'exemple va créer un conteneur IRIS, en définissant OTEL_EXPORTER_OTLP_ENDPOINT avec l'adresse du collecteur OTel:

iris:    build:      context:.      dockerfile:Dockerfile    restart:always    ports:      -51773:1972      -52773:52773      -53773    volumes:      -./:/home/irisowner/dev    environment:    -ISC_DATA_DIRECTORY=/home/irisowner/dev/durable    -OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318

 
7. Enfin, un conteneur Jaeger est créé pour recevoir les données du collecteur OTel et fournir une interface utilisateur permettant aux utilisateurs de surveiller et de tracer les données IRIS OTel:

jaeger:    image:jaegertracing/all-in-one:latest    ports:      -"16686:16686"# Jaeger UI      -"14269:14269"# Jaeger Metrics      -"14250:14250"# Jaeger Collector gRPC     environment:      -COLLECTOR_OTLP_ENABLED=true

 
Il existe des options de port supplémentaires pour Jaeger qui vous permettent de traiter plusieurs types de collecteurs. Vous trouverez ci-dessous la liste des ports exposés disponibles:

  • 6831/udp: Accepte les spans jaeger.thrift (Thrift compact)
  • 6832/udp: Accepte les spans jaeger.thrift (Thrift binaire)
  • 5778: Configuration Jaeger
  • 16686: Interface utilisateur Jaeger
  • 4317: Récepteur gRPC du protocole OpenTelemetry (OTLP)
  • 4318: Récepteur HTTP du protocole OpenTelemetry (OTLP)
  • 14250: Acceptation des spans model.proto via gRPC
  • 14268: Acceptation directe des spans jaeger.thrift via HTTP
  • 14269: Vérification de l'intégrité de Jaeger
  • 9411: Compatibilité Zipkin

Exécution de l'exemple

1. Copiez le code source de l'exemple: 

$ git clone https://github.com/yurimarx/iris-telemetry-sample.git

2. Ouvrez le terminal dans ce répertoire et exécutez le code ci-dessous:

$ docker-compose up -d --build

3. Créez quelques données de test simulées. Pour ce faire, ouvrez le terminal IRIS ou le terminal web sur /localhost:52773/terminal/ and call the following:

USER>do##class(dc.Sample.Person).AddTestData(10)

4. Ouvrez http://localhost:52773/swagger-ui/index.html et exécutez le point de terminaison /persons/all.

 

Analyse et traçage du code IRIS sur Jaeger

1. Accédez à Jaeger ici http://localhost:16686/search.

2. Sélectionnez Get.All.Persons dans le champ "Service", puis cliquez sur "Find Traces" (Rechercher les traces).


3. Cliquez sur la trace détectée pour afficher ses détails.


4. Observez la chronologie de la trace sélectionnée.


5. Dans le coin supérieur droit, sélectionnez "Trace Graph" :


6. Analysez les dépendances:


7. Cette analyse des dépendances est essentielle pour identifier les problèmes dans les systèmes/services distants au sein de votre environnement, en particulier si différents services distribués utilisent le même nom de service.
8. Maintenant, sélectionnez “Framegraph”:


9. Observez tous les composants du flux de transactions surveillé dans une table graphique:


10. Grâce à toutes ces ressources Jaeger, vous pouvez résoudre efficacement les problèmes de performances et identifier la source des erreurs.

 

Pour en savoir plus

Pour approfondir vos connaissances sur le traçage distribué, consultez les ressources externes suivantes:

  • Maîtrise du traçage distribué (Mastering Distributed Tracing) (2019) par Yuri Shkuro : article de blog rédigé par le créateur de Jaeger, qui explique l'histoire et les choix architecturaux à l'origine de Jaeger. Ce livre traite en détail de la conception et du fonctionnement de Jaeger, ainsi que du traçage distribué en général.
  • Suivez Jaeger pour un tour en HotROD: un tutoriel étape par étape qui montre comment utiliser Jaeger pour résoudre les problèmes de performances des applications.
  • Présentation de Jaeger : un (ancien) webinaire présentant Jaeger et ses capacités.
  • Tutoriel détaillé sur Jaeger: https://betterstack.com/community/guides/observability/jaeger-guide/ 
  • Évolution du traçage distribué au sein d'Uber.
  • Émettre des données de télémétrie vers un outil de surveillance compatible avec OpenTelemetry par la documentation InterSystems.
  • Observabilité moderne avec InterSystems IRIS & et OpenTelemetry: Une vidéo de démonstration sur la manière de travailler avec OpenTelemetry et IRIS: https://www.youtube.com/watch?v=NxA4nBe31nA
  • OpenTelemetry sur GitHub : Une collection d'API, de SDK et d'outils pour instrumenter, générer, collecter et exporter des données de télémétrie (métriques, journaux et traces) afin d'aider à analyser les performances et le comportement de logiciels: https://github.com/open-telemetry.
0
0 21
Article Benjamin De Boe · Juil 30, 2025 13m read

Cet article décrit une amélioration significative apportée dans la version 2025.2 à la manière dont InterSystems IRIS traite les statistiques de table, un élément crucial pour le traitement SQL IRIS. Nous commencerons par un bref rappel sur ce que sont les statistiques de table, comment elles sont utilisées et pourquoi cette amélioration était nécessaire. Nous nous intéresserons ensuite en détail à la nouvelle infrastructure de collecte et d'enregistrement des statistiques de table, puis nous examinerons ce que ce changement signifie en pratique pour vos applications. Nous terminerons par quelques remarques supplémentaires sur les modèles rendus possibles par le nouveau modèle et nous attendons avec impatience les étapes suivantes de cette première livraison.

Je n'utilise pas SQL, pourquoi suis-je ici? – C'est une question existentielle pertinente (wink) , mais cet article peut néanmoins contenir des informations précieuses pour vous. Certaines fonctionnalités avancées d'IRIS, notamment Interopérabilité, utilisent IRIS SQL en arrière-plan, et certains changements de comportement peuvent vous inciter à activer une nouvelle tâche de maintenance. Reportez-vous à la section "Collecte automatique des statistiques des tables" ci-dessous.

Statistiques de table 101

SQL signifie Structured Query Language (langage de requête structuré). SQL est un langage dans lequel vous exprimez ce que vous voulez (la requête), plutôt que comment vous le voulez (le code permettant d'obtenir le résultat de la requête). C'est au moteur de requête d'une base de données qu'il revient d'examiner toutes les options et de choisir le plan vraisemblablement optimal pour récupérer le résultat que vous demandez. Pour déterminer le meilleur plan, l'optimiseur de requêtes a essentiellement besoin de deux choses: des informations sur la structure de votre table, telles que l'emplacement de stockage de vos données et les index disponibles, et des informations sur les données de la table elle-même: les statistiques de table. Ces statistiques de table couvrent des informations telles que la taille estimée de la table et la sélectivité de chaque champ, qui exprime la proportion moyenne de la table correspondant à une valeur de champ particulière. Ces informations sont essentielles pour prendre les bonnes décisions concernant les plans de requête. Supposons que vous deviez trouver le nombre d'utilisateurs féminins dans un code postal particulier et que vous puissiez choisir entre partir d'un index sur le sexe ou sur le code postal. La sélectivité de l'index sur le sexe sera d'environ 50 %, tandis que celle sur le code postal pourra être aussi faible que 1 % ou moins, ce qui signifie que vous filtrerez beaucoup plus rapidement l'ensemble des lignes à rechercher en partant de ce dernier. 

Si la structure des tables fait naturellement partie de votre application et peut être intégrée au code de celle-ci, ce n'est pas nécessairement le cas des statistiques de table. Si l'exemple précédent est tiré d'une petite application librairie, il est probable que les valeurs de sélectivité des champs soient valables pour toutes les librairies dans lesquelles l'application est déployée, mais s'il s'agit d'un système de dossiers médicaux électroniques, la sélectivité du champ sexe sera différente entre une maternité et un cabinet médical généraliste, et le nombre de codes postaux (et donc leur sélectivité) dépendra de la taille de la zone couverte par l'hôpital. Il est évident que vous souhaitez que vos statistiques, et donc vos plans de requête, soient basés sur les données réelles de l'environnement dans lequel l'application est déployée.

Statistiques de table dans IRIS - avant 2025.2

Sur IRIS, les statistiques de table ont toujours été stockées dans la définition d'une classe. Cela présente plusieurs inconvénients

  • Comme décrit précédemment, les distributions de données peuvent varier considérablement entre les environnements dans lesquels vous déployez votre application. Ceci n'est pas seulement vrai pour la taille estimée des tables et la sélectivité, mais encore plus pour d'autres types de statistiques plus avancées telles que les informations sur les valeurs aberrantes et les histogrammes.
  • Lorsque vous déployez une version mise à jour de votre application, vous souhaitez généralement conserver toutes les statistiques existantes, en supposant qu'elles sont basées sur les données réelles de cet environnement. Cela s'avère plus délicat lorsqu'elles sont stockées en tant que partie intégrante du code, car vous ne souhaitez évidemment pas déployer accidentellement les statistiques de table de votre environnement de développement ou de test sur le système d'un utilisateur.
  • Lorsque le code d'application est déployé sous la forme d'une base de données en lecture seule ou lorsqu'il fait partie d'une bibliothèque système IRIS (comme les messages d'interopérabilité), il n'existe aucun moyen efficace de mettre à jour les statistiques de ces tables.

Consultez également cet article précédent qui fournit davantage de détails à ce sujet. Voici certaines raisons qui nous ont poussés à repenser la gestion des statistiques de table dans IRIS. La nouvelle infrastructure est désormais incluse dans InterSystems IRIS 2025.2.

Quel est le changement?

Statistiques collectées vs statistiques fixes

Comme décrit dans les exemples précédents, le stockage des statistiques dans le code de l'application n'a de sens que dans des environnements très spécifiques et contrôlés. Dans la plupart des cas, il est préférable de conserver les statistiques avec les données à partir desquelles elles ont été collectées, plutôt que de les fixer dans le code. C'est donc ce que nous faisons dans le nouveau modèle. Nous faisons la distinction entre les statistiques collectées qui sont toujours basées sur des données réelles et stockées dans l'index d'extension (une variable globale contenant d'autres détails au format registre sur les données de votre table, également appelées extensions), et les statistiques fixes qui sont codées en dur dans la définition de classe par le développeur de l'application et peuvent être basées sur des données réelles ou sur des hypothèses raisonnables. Comme vous pouvez le deviner, les statistiques fixes correspondent au modèle antérieur à la version 2025.2 pour la gestion des statistiques de table. 

Utilisez la nouvelle commande COLLECT STATISTICS FOR TABLE t   (qui est 100 % synonyme de la commande existante TUNE TABLE , (qui est 100 % synonyme de la commande existante TUNE TABLE, et qui sert simplement à normaliser la terminologie) pour remplir un nouvel ensemble de statistiques basé sur les données actuelles de votre table. Les statistiques de table sont très petites, nous n'écrasons donc pas les statistiques précédentes, mais stockons plutôt un nouvel ensemble et conservons les anciennes statistiques à des fins d'information jusqu'à ce qu'elles répondent aux critères de purge par défaut ou soient explicitement purgées. Si vous souhaitez passer au modèle fixe, vous pouvez enregistrer les statistiques collectées dans la définition de classe à l'aide de ALTER TABLE t FIX STATISTICS. Cela peut faire partie de la procédure de package de votre application, si vous êtes certain que ces statistiques correspondent à votre environnement cible et que vous préférez le modèle statique.

Par défaut, les statistiques fixes prévalent sur les statistiques collectées. Cela garantit la rétrocompatibilité et signifie qu'en tant que développeur, c'est toujours vous qui avez le dernier mot. En fait, si vous avez des statistiques fixes, même pour un seul champ, les statistiques collectées seront ignorées et nous reviendrons à des estimations sûres basées sur le type de données du champ pour tous les champs qui n'ont pas de statistiques fixes. Si vous souhaitez tester les statistiques collectées sans supprimer vos statistiques fixes, vous pouvez inclure l'indication %NOFIXEDSTATS hint dans la clause FROM de votre texte de requête (par table) afin d'obtenir le plan de requête basé uniquement sur les statistiques collectées, ou utiliser ALTER TABLE t SET RUNTIME IGNOREFIXEDSTATS = TRUE pour définir ce comportement au niveau de la table. 

Collecte automatique des statistiques de table

L'introduction des statistiques collectées résout déjà bon nombre des inconvénients liés au packagage et au déploiement mentionnés dans l'introduction. Cependant, elle ne résout pas vraiment l'un des problèmes les plus courants rencontrés par les utilisateurs: le manque de statistiques à jour . Comme décrit dans l'introduction, des statistiques précises sont essentielles pour obtenir les meilleurs plans de requête pour votre distribution de données spécifique. Si vous ne vous êtes jamais soucié de les collecter, l'optimiseur utilisera des valeurs par défaut approximatives basées sur le type de données de vos champs, mais celles-ci sont peu susceptibles de tenir compte correctement des spécificités de votre application dans tous les cas. À partir de la version 2021.2, IRIS collectera automatiquement les statistiques pour les tables qui n'en ont pas et qui sont éligibles à une technique d'échantillonnage rapide au moment de la requête, mais cela ne se produira qu'une seule fois, et peut-être à un moment où votre table n'aura pas encore été remplie avec des données représentatives. Mais même lorsque les statistiques ont été collectées à un moment opportun, les distributions de données évoluent avec le temps (notamment les histogrammes) et nous constatons très souvent que les utilisateurs obtiennent des plans de requête sous-optimaux simplement parce qu'ils sont basés sur des statistiques obsolètes.

Avec la version 2025.2, nous introduisons une nouvelle tâche système qui s'exécute pendant une fenêtre de maintenance de nuit et qui, pour chaque espace de noms, prend en compte toutes les tables qui n'ont aucune statistique, pour lesquelles les statistiques les plus récentes ont été invalidées (par exemple après avoir modifié la définition de stockage de la table et recompilé) ou pour lesquelles au moins 25 % des données de la table ont changé depuis la dernière collecte de statistiques (environ, ceci est mesuré sur la base du ROWCOUNT combiné pour les opérations DML sur cette table). Les statistiques sont ensuite collectées pour ces tables une par une, ou jusqu'à ce que la durée maximale de la tâche de maintenance (configurable) soit atteinte. D'autres options de configuration permettent de choisir de prendre en compte les tables avec des statistiques fixes (ON par défaut), les tables mappées vers un stockage distant (OFF par défaut) et les tables qui ne sont pas éligibles pour une technique d'échantillonnage rapide (OFF par défaut). Si nécessaire, vous pouvez marquer des tables individuelles à l'aide d'un paramètre de classe SKIPAUTOMATICSTATSCOLLECTION  (ou d'un indicateur d'exécution équivalent) afin qu'elles soient ignorées par cette tâche système (notez que ce nouvel indicateur affecte également le comportement AutoTune préexistant).

Dans la version 2025.2, cette tâche système est désactivée par défaut, car nous souhaitons évaluer avec les utilisateurs l'impact de cette fonctionnalité sur les modèles d'E/S des systèmes réels. Nous encourageons les utilisateurs à activer la tâche système et à nous communiquer leur expérience en matière de charge système afin que nous puissions procéder aux ajustements nécessaires avant de l'activer par défaut dans une version ultérieure. À ce moment-là, nous prévoyons de supprimer le fonctionnement précédent d' AutoTune qui pouvait se déclencher pendant le traitement des requêtes.

La collecte manuelle des statistiques de table est bien sûr toujours prise en charge, et l'exécution de COLLECT STATISTICS après des chargements ou des purges de données importants reste une bonne pratique.

Qu'est-ce que cela signifie pour votre application?

Nous avons pris soin de concevoir le nouveau modèle afin que les applications SQL mises à niveau vers IRIS 2025.2 ne subissent aucun changement. Les statistiques préexistantes seront désormais traitées comme des statistiques fixes, qui auront priorité sur toutes les statistiques collectées automatiquement en arrière-plan. Nous conseillons aux utilisateurs qui passent au nouveau modèle de tenir compte des trois recommandations suivantes:

Suppression des statistiques fixes après la mise à niveau

Si votre application ou, en fait, n'importe quelle table avait des statistiques avant la mise à niveau (ce qui serait le cas pour toutes les tables si vous avez suivi les meilleures pratiques), envisagez de les supprimer purement et simplement. Après la mise à niveau, elles seront considérées comme des statistiques fixes et auront priorité sur toutes les statistiques que vous collecterez explicitement (peut-être via des scripts personnalisés appelant TUNE TABLE) ou implicitement. Les statistiques fixes des tables peuvent être supprimées à l'aide d'une nouvelle commande ALTER TABLE t DROP FIXED STATISTICS ou de la commande équivalente ALTER SCHEMA s DROP FIXED STATISTICS. Vous pouvez utiliser l'indication %NOFIXEDSTATS pour vérifier à l'avance l'impact sur les requêtes individuelles, selon vos préférences.

On ne conservera les statistiques fixes que si l'on estime que les statistiques prédéfinies doivent rester inchangées pour maintenir les plans de requête actuels et qu'aucune modification de la distribution des données n'est prévue.

Prise en compte des statistiques pour les classes système

Il convient de noter que le nouveau modèle de statistiques collectées s'applique aux tables dont la définition réside dans une base de données en lecture seule, y compris les tables système IRIS telles que  Ens.MessageHeader. Pour ces tables, les statistiques de table étaient auparavant peu pratiques, voire impossibles à maintenir, mais elles deviendront désormais pertinentes et, lorsque vous aurez activé la tâche du système de collecte, elles seront maintenues automatiquement. Lorsque vous utilisez des productions d'interopérabilité ou d'autres éléments de l'infrastructure IRIS pouvant impliquer SQL en arrière-plan, nous vous recommandons de surveiller les performances des opérations de recherche et autres opérations similaires après la mise à niveau, et de collecter les statistiques manuellement ou d'activer la tâche système. 

Lors de l'utilisation de la recherche de messages, nous avons constaté des améliorations significatives des performances des requêtes après l'adoption du nouveau modèle, car celui-ci pouvait désormais utiliser une sélectivité et d'autres statistiques beaucoup plus réalistes, alors qu'auparavant, il s'appuyait sur des valeurs par défaut approximatives. Nous avons donc supprimé les statistiques fixes fournies avec ces tables système.

Soyez vigilant lors de l'importation et de l'exportation entre différentes versions

Lorsque vous exportez des définitions de table avec l'indicateur /exportselectivity=1 (par défaut), les statistiques sont incluses dans un nouveau format qui prend en charge à la fois les statistiques fixes et les statistiques les plus récentes collectées collected mais qui est incompatible avec les versions antérieures. Pour prendre en charge l'importation dans des instances IRIS exécutant des versions antérieures, utilisez /exportversion=2025.1 ou /exportselectivity=0 selon le cas. Veuillez noter que les statistiques collectées et exportées à l'aide de ce format seront toujours importées en tant que statistiques collectées et ne deviendront pas fixes de manière silencieuse parce qu'elles ont été incluses dans le fichier de définition de classe export . Compte tenu de ces nuances, vous pouvez revoir votre stratégie de contrôle de source afin de vous assurer que les informations correctes sont suivies pour votre modèle de déploiement. Notez également l'indicateur symétrique /importselectivity qui peut être utilisé lors de l'importation de définitions de classe. 

Les méthodes Import() et Export() de la classe $SYSTEM.SQL.Stats.Table ont également été étendues avec un argument de type supplémentaire afin de différencier correctement les deux types de statistiques. Pour plus d'informations, consultez la référence de classe .

Travaux futurs

Cette version comprend toute l'infrastructure nécessaire pour tirer parti du nouveau modèle. Outre quelques améliorations mineures visant à faciliter l'utilisation, nous prévoyons les deux mises à jour suivantes dans une prochaine version (probablement 2025.3)

Activation de la tâche de collecte automatique

Comme indiqué précédemment, nous avons introduit une nouvelle tâche système pour la collecte automatique des statistiques des tables pendant une fenêtre de traitement par lots de nuit, mais nous l'avons désactivée afin d'éviter toute charge E/S inattendue après la mise à niveau. Dans une prochaine version, nous l'activerons par défaut, si elle n'est pas déjà activée par l'utilisateur à ce moment-là.

Taille d'échantillon plus intelligente

Nous avons remarqué que notre algorithme d'échantillonnage rapide par blocs pouvait dans certains cas surestimer la sélectivité des champs hautement sélectifs dans les grandes tables, ce qui signifie que l'optimiseur pouvait ne pas utiliser les index correspondants alors que cela aurait été optimal. Nous mettons en place un algorithme qui affinera de manière dynamique la taille de l'échantillon si nous détectons trop de variabilité entre les échantillonnages. Ce changement n'a pratiquement aucun impact sur la modification globale de l'infrastructure décrite plus haut dans cet article.

Partagez votre expérience

Cela fait longtemps que nous attendions avec impatience le déploiement de cette modification, et nous sommes ravis qu'elle soit désormais incluse dans IRIS 2025.2. Nous avons pris soin de tester cette modification dans de nombreuses configurations différentes et nous sommes convaincus que la nouvelle infrastructure est robuste, mais nous sommes conscients que les modifications peuvent affecter des pratiques de déploiement hautement personnalisées, notamment le mappage des espaces de noms et le packagage du code. Nous vous invitons donc à nous faire part de vos commentaires et espérons que la nouvelle infrastructure vous facilitera la vie.

0
0 21
Question Corentin Blondeau · Juin 4, 2025

Bonjour,
Quels sont les avantages et les inconvénients d'avoir plusieurs namespaces par rapport à un seul?
J'aimerais bien savoir les aspects positifs et négatifs des deux cas de figures, par exemple 1 ou 4 espaces de nom pour un total de 20 flux.

Qu'en est t'il niveau sécurité, gestion serveur, gestion du code, autre?
En terme de performances, est-ce que trop d'items sur une seul production ralenti le serveur? Ou trop de namespaces augmente la consommation mémoire pour rien?
Comment évolue la consommation d'un namespace? Il y a un minimum?
Je vous remercie de vos réponses.
Corentin BLONDEAU

5
1 92
Article Lorenzo Scalese · Juin 10, 2025 11m read

Introduction

Les performances des bases de données sont devenues essentielles à la réussite des environnements applicatifs modernes. Il est donc indispensable d'identifier et d'optimiser les requêtes SQL les plus exigeantes en ressources afin de garantir une expérience utilisateur fluide et la stabilité des applications. 

Cet article présente une approche rapide pour analyser les statistiques d'exécution des requêtes SQL sur une instance InterSystems IRIS afin d'identifier les domaines à optimiser au sein d'une macro-application.

Au lieu de nous concentrer sur la surveillance en temps réel, nous allons mettre en place un système qui collecte et analyse les statistiques précalculées par IRIS une fois par heure.  Cette approche, bien qu'elle ne permette pas de surveillance instantanée, offre un excellent compromis entre la richesse des données disponibles et la simplicité de mise en œuvre. 

Nous utiliserons Grafana pour la visualisation et l'analyse des données, InfluxDB pour le stockage des séries chronologiques et Telegraf pour la collecte des métriques.  Ces outils, reconnus pour leur puissance et leur flexibilité, nous permettront d'obtenir un aperçu clair et exploitable.

Plus précisément, nous détaillerons la configuration de Telegraf pour récupérer les statistiques. Nous configurerons également l'intégration avec InfluxDB pour le stockage et l'analyse des données, et créerons des tableaux de bord personnalisés dans Grafana. Cela nous aidera à identifier rapidement les requêtes nécessitant une attention particulière.

Pour faciliter l'orchestration et le déploiement de ces différents composants, nous utiliserons Docker.

logos.png

Conditions préalables

Avant de commencer, assurez-vous d'avoir les éléments suivant:

  • Git: Git est nécessaire pour cloner le référentiel du projet contenant les fichiers de configuration et les scripts.
  • Docker ou Docker Desktop: Docker peut être utilisé pour conteneuriser les applications InfluxDB, Telegraf et Grafana, ce qui facilite leur déploiement et leur gestion.
  • Instance InterSystems IRIS:Au moins la version 2022.1, idéalement 2023.1 ou supérieure, avec le package sql-stats-api installé.  Ce package est essentiel pour exposer les statistiques SQL IRIS et permettre à Telegraf de les collecter. [Lien vers OpenExchange]

Bien que nos fichiers docker-compose incluent une instance IRIS, celle-ci ne contiendra aucune donnée statistique SQL, car elle vient d'être créée et démarrée. Elle ne constituera donc pas un choix pratique pour tester le système. C'est pourquoi nous vous recommandons vivement de disposer d'une autre instance IRIS "active" (celle qui contient l'historique des requêtes SQL) afin de pouvoir visualiser des données réelles et tester l'outil d'analyse.

À propos des statistiques

IRIS collecte des statistiques d'exécution des requêtes SQL à une granularité horaire et quotidienne. Les statistiques horaires identifient les variations de performances tout au long de la journée, tandis que les statistiques quotidiennes fournissent une vue d'ensemble de l'activité de la base de données.

Vous trouverez ci-dessous les données que nous collectons pour chaque requête SQL:

  • Nombre d'exécutions: indique le nombre de fois où la requête a été exécutée.
  • Durée totale d'exécution: mesure la durée totale d'exécution de la requête.
  • Variance des temps d'exécution: utilisée pour identifier les variations de performances et les problèmes ponctuels.
  • Nombre total de lignes renvoyées (RowCount): disponible pour IRIS 2023.1 et versions ultérieures, cette métrique indique le nombre total de lignes renvoyées par la requête. Elle peut vous aider à identifier les requêtes exigeantes en ressources.
  • Nombre total de commandes exécutées: également disponible pour IRIS 2023.1 et versions ultérieures, cette métrique facilite une analyse plus détaillée de l'activité de la base de données et identifie les requêtes qui pourraient être optimisées en réduisant le nombre d'opérations.

Ces informations sont accessibles via les tables suivantes:

  • INFORMATION_SCHEMA.STATEMENT_DAILY_STATS
  • INFORMATION_SCHEMA.STATEMENT_HOURLY_STATS

Ces tables sont disponibles depuis IRIS 2022.1. Vous trouverez ci-dessous un exemple de requête SQL permettant de récupérer des statistiques:

SELECT ds.* 
FROM INFORMATION_SCHEMA.STATEMENT_DAILY_STATS ds
INNERJOIN INFORMATION_SCHEMA.STATEMENTS st On ds.Statement = st.Hash


SELECTDATEADD('hh',"Hour",$PIECE(hs."Day",'||',2)) As DateTime, hs.*
FROM INFORMATION_SCHEMA.STATEMENT_HOURLY_STATS hs
INNERJOIN INFORMATION_SCHEMA.STATEMENTS st On $PIECE(hs."Day",'||',1) = st.Hash

Pour les versions plus anciennes que IRIS 2022.1, je recommande vivement l'article de David Loveluck, qui explique comment récupérer des statistiques similaires.

Architecture

Le projet repose sur l'interaction de quatre composants clés : IRIS, Grafana, InfluxDB et Telegraf. Le diagramme ci-dessous illustre l'architecture globale du système et le flux de données entre les différents composants:

archi.png

  • InterSystems IRIS: il s'agit de l'instance que nous utiliserons pour récupérer les statistiques.
  • Package sql-stats-api: ce package ObjectScript expose les données statistiques IRIS via l'API REST. Il offre deux formats de sortie : JSON pour une utilisation générale et Line Protocol, un format optimisé pour l'ingestion rapide de données de séries chronologiques dans InfluxDB.
  • Telegraf: il s'agit d'un agent de collecte de métriques qui fournit le lien entre IRIS et InfluxDB. Dans ce projet, nous utiliserons deux instances de Telegraf:
    • un agent interroge périodiquement l'API REST IRIS pour récupérer les statistiques SQL en temps réel.
    • Un autre agent fonctionne en mode "scan de répertoire". Il surveille un répertoire contenant les fichiers stockés et les transmet à InfluxDB, ce qui permet d'intégrer les données inaccessibles via l'API REST.
  • InfluxDB:  cette base de données de séries chronologiques stocke et gère les statistiques SQL collectées par Telegraf, car son architecture est optimisée pour ce type de données. InfluxDB offre également une intégration native avec Grafana, ce qui facilite la visualisation et l'analyse des données. Nous avons préféré InfluxDB à Prometheus, car ce dernier est davantage axé sur la surveillance en temps réel et n'est pas bien adapté au stockage de données agrégées, telles que les sommes ou les moyennes horaire ou journalière, qui sont essentielles pour notre analyse.
  • Grafana: il s'agit d'un outil de visualisation qui permet de créer des tableaux de bord personnalisés et interactifs pour analyser les performances SQL. Il récupère les données d'InfluxDB et offre une variété de graphiques et de widgets pour visualiser les statistiques de manière claire et exploitable.

Installation

Commencez par cloner le référentiel:

git clone https://github.com/lscalese/iris-sql-dashboard.git
cd irisiris-sql-dashboard

Configuration de l'environnement

Ce projet utilise Docker pour orchestrer Grafana, InfluxDB, Telegraf et IRIS. Pour des raisons de sécurité, les informations sensibles telles que les clés API et les mots de passe sont stockées dans un fichier .env.

Créez le fichier .env en utilisant l'exemple fourni ci-dessous:

cp .env.example .env

Éditez le fichier .env pour configurer les variables:

Configuration des variables

  • TZ: fuseau horaire. Il est recommandé de modifier cette variable en fonction de votre fuseau horaire afin de garantir l'exactitude de l'horodatage des données.
  • DOCKER_INFLUXDB_INIT_PASSWORD: mot de passe administrateur permettant d'accéder à InfluxDB.
  • IRIS_USER: il s'agit d'un utilisateur IRIS de l'instance Docker IRIS (_system par défaut).
  • IRIS_PASSWORD : Il s'agit du mot de passe de l'instance IRIS Docker (SYS par défaut).

Les clés API permettent les connexions suivantes:

  • GRAFANA_INFLUX_API_KEY : Grafana <-> InfluxDB.
  • TELEGRAF_INFLUX_API_KEY : Telegraf <-> InfluxDB.

Génération de clés API

Pour des raisons de sécurité, InfluxDB nécessite des clés API pour l'authentification et l'autorisation.  Ces clés sont utilisées pour identifier et autoriser divers composants (Telegraf, Grafana) à accéder à Influx DB.

Le script init-influxdb.sh, inclus dans le référentiel, facilite la génération de ces clés.  Il sera exécuté automatiquement lors du premier démarrage du conteneur infxludb2:

docker compose up -d influxdb2

Après quelques secondes, le fichier .env sera mis à jour avec vos clés API générées.

Remarque: cette étape ne doit être effectuée que lors du premier démarrage du conteneur.

Vérifiez si vous avez accès à l'interface d'administration InfluxDB via l'URL http://localhost:8086/

Connectez-vous avec le nom d'utilisateur "admin" et le mot de passe spécifiés dans la variable d'environnement "DOCKER_INFLUXDB_INIT_PASSWORD" dans le fichier ".env". Lorsque vous naviguez dans "Load Data >> Buckets”, vous devriez découvrir un bucket "IRIS_SQL_STATS" préconfiguré.   

influxdb-2.png
En parcourant "Load Data >> API Tokens", vous devriez trouver nos deux clés API : "Grafana_IRIS_SQL_STATS" et "Telegraf_IRIS_SQL_STATS":

influxdb-3.png

L'environnement est prêt maintenant, et nous pouvons passer à l'étape suivante!

Démarrage

L'environnement étant configuré et les clés API générées, vous pouvez enfin lancer l'ensemble de conteneurs. Pour ce faire, exécutez la commande suivante dans le répertoire racine du projet:

docker compose up -d

Cette commande lancera en arrière-plan tous les services définis dans le fichier docker-compose.yml : InfluxDB, Telegraf, Grafana et l'instance IRIS.

Tableau de bord Grafana

Grafana est désormais disponible à l'adresse http://localhost:3000.

Connexion à Grafana

Ouvrez votre navigateur Web et rendez-vous à l'adresse http://localhost:3000. Le nom d'utilisateur et le mot de passe par défaut sont admin/admin. Cependant, vous serez invité à modifier le mot de passe lors de votre première connexion.

grafana-login.png

Vérification de la source de données InfluxDB

La source de données InfluxDB est préconfigurée dans Grafana. Il vous suffit de vérifier son bon fonctionnement.

Accédez à “Connections > Data sources (sources de données)”.

grafana-ds.png

Vous devriez voir une source de données nommée “influxdb”.Cliquez dessus pour la modifier.

Cliquez ensuite sur “Save & Test”. Le message “Datasource is working. 1 bucket found” (La source de données fonctionne. 1 bucket trouvé) devrait maintenant s'afficher à l'écran.

grafana-datasources-influxdb.png

Exploration des tableaux de bord

À ce stade, vous avez vérifié que la communication entre Grafana et InfluxDB est établie, ce qui signifie que vous pouvez explorer les tableaux de bord prédéfinis.

Passez à “Dashboards” (tableaux de bord).

grafana-dashboard-list.png

Vous trouverez deux tableaux de bord prédéfinis:

  • InfluxDB - SQL Stats: ce tableau de bord affiche des statistiques générales sur l'exécution des requêtes SQL, par exemple le nombre d'exécutions, le temps total d'exécution et la variance du temps d'exécution.
  • InfluxDB - SQL Stats Details: ce tableau de bord fournit des informations plus détaillées sur chaque requête SQL, par exemple le nombre total de lignes renvoyées ou de commandes exécutées.

Pourquoi les tableaux de bord sont vides

Si vous ouvrez les tableaux de bord, vous verrez qu'ils sont vides.  Cela s'explique par le fait que notre agent Telegraf est actuellement connecté à l'instance IRIS fournie dans le référentiel Docker, dont les tables ne contiennent aucune donnée statistique. Les statistiques SQL ne sont collectées que si l'instance IRIS est active et conserve un historique des requêtes SQL.

Dans la section suivante, nous allons voir comment injecter des données dans l'instance IRIS afin d'afficher les statistiques dans Grafana.

 

Telegraf

Le système de surveillance utilise deux agents Telegraf ayant des rôles spécifiques:

  • telegraf-iris.conf:cet agent collecte des données en temps réel à partir d'une instance IRIS active. Il interroge l'API REST IRIS pour récupérer des statistiques SQL afin de les envoyer à InfluxDB.
  • telegraf-directory-scan.conf: cet agent intègre les données historiques stockées dans des fichiers. Il surveille le répertoire ./telegraf/in/, lit les fichiers contenant des statistiques SQL et les envoie à InfluxDB.

Pour collecter des données en temps réel, il faut connecter Telegraf à une instance IRIS active sur laquelle le package sql-stats-api est installé.  Ce package expose les statistiques SQL via une API REST, ce qui permet à Telegraf d'y accéder.

Configuration de telegraf-iris.conf

Pour connecter Telegraf à votre instance IRIS, vous devez modifier le fichier ./telegraf/config/telegraf-iris.conf. Vous trouverez ci-dessous un exemple de configuration:

[[inputs.http]]  ## Une ou plusieurs URL à partir desquelles lire les métriques formatées  urls = [    "http://iris:52773/csp/sqlstats/api/daily",    "http://iris:52773/csp/sqlstats/api/hourly"  ]  ## Méthode HTTP  method = "GET"
  ## En-têtes HTTP facultatifs  headers = {"Accept" = "text/plain"}  ## Informations d'identification pour l'authentification HTTP de base (facultatif)  username = "${IRIS_USER}"  password = "${IRIS_PASSWORD}"  data_format = "influx"

Assurez-vous que${IRIS_USER} et ${IRIS_PASSWORD} sont correctement définis dans votre dossier .env file.
Remarque: Vous pouvez copier le fichier et modifier les paramètres pour connecter Telegraf à plusieurs instances IRIS.

Redémarrage de Telegraf:

Après avoir modifié le fichier de configuration, il est nécessaire de redémarrer le conteneur Telegraf pour que les modifications prennent effet:

docker compose up -d telegraf --force-recreate

Récupération des données historiques

Pour récupérer les statistiques SQL historiques, utilisez la méthode ObjectScript CreateInfluxFile sur votre instance IRIS:

; Adaptez le chemin à vos besoinsSet sc = ##class(dc.sqlstats.services.SQLStats).CreateInfluxFile("/home/irisowner/dev/influxdb-lines.txt",,1)

Ce script enregistre l'historique des statistiques SQL dans des fichiers texte dont la longueur maximale est de 10 000 lignes par fichier. Vous pouvez ensuite placer ces fichiers dans le répertoire ./telegraf/in/ afin de les traiter et de les injecter dans InfluxDB.

Vérification de l'injection des données

Vous pouvez vérifier que les données ont été correctement injectées dans InfluxDB à l'aide de l'interface Web. Accédez à "Data Explorer" (Explorateur de données) et vérifiez:

influxdb-explorer.png

Visualisation des données dans Grafana

Une fois les données injectées, vous pouvez les afficher dans vos tableaux de bord Grafana fournis.

grafana-dashboard-daily-stats.png

grafana-dashboard-daily-details.png

Nous sommes arrivés à la fin de notre article. J'espère qu'il vous a été utile et qu'il vous avez appris à configurer facilement un système de surveillance et d'analyse des statistiques SQL sur vos instances IRIS.

Comme vous l'avez peut-être constaté, cet article a mis l'accent sur les aspects pratiques de la configuration et de l'utilisation de divers outils. Nous n'avons pas étudié en détail le fonctionnement interne d'InfluxDB, du format Line Protocol ou du langage Flux Query, ni examiné la multitude de plugins disponibles pour Telegraf.

Ces sujets, aussi fascinants soient-ils, nécessiteraient un article beaucoup plus long.  Je vous encourage vivement à consulter la documentation officielle d'InfluxDB Get started with InfluxDB (Débuter avec InfluxDB) et Telegraf Plugin Directory (Répertoire des plugins) pour approfondir vos connaissances et découvrir toutes les possibilités offertes par ces outils.

N'hésitez pas à partager vos expériences dans les commentaires.

Merci d'avoir lu cet article, et à bientôt!

0
0 52
Article Lorenzo Scalese · Mai 15, 2025 10m read

Introduction

MonLBL est un outil permettant d'analyser des performances d'exécution de code ObjectScript ligne par ligne. codemonitor.MonLBL est un wrapper reposant sur le package %Monitor.System.LineByLine d'InterSystems IRIS pour collecter des métriques précises sur l'exécution de routines, classes ou CSP.

Le wrapper et tous les exemples présentés dans cet article sont disponibles dans le repository GitHub suivant : iris-monlbl-example

Fonctionnalités

L'utilitaire permet de collecter plusieurs types de métriques :

  • RtnLine : Nombre d'exécutions de la ligne
  • GloRef : Nombre de références globales générées par la ligne
  • Time : Temps d'exécution de la ligne
  • TotalTime : Temps total d'exécution incluant les sous-routines appelées

Le tout exporté dans des fichiers CSV.

En plus des métriques par ligne, dc.codemonitor.MonLBL collecte des statistiques globales :

  • Temps d'exécution total
  • Nombre total de lignes exécutées
  • Nombre total de références globales
  • Temps CPU système et utilisateur :
    • Le temps CPU utilisateur correspond au temps passé par le processeur à exécuter le code de l'application
    • Le temps CPU système correspond au temps passé par le processeur à exécuter des opérations du système d'exploitation (appels système, gestion mémoire, I/O)
  • Temps de lecture disque

Prérequis

Pour pouvoir monitorer du code avec MonLBL :

  1. Disposez de la classe dc.codemonitor.MonLBL (disponible ici)
  2. Les routines ou classes à analyser doivent être compilées avec les flags "ck"

⚠️ Mise en garde importante

L'utilisation du monitoring ligne par ligne a un impact sur les performances du serveur. Il est important de respecter les recommandations suivantes :

  • N'utilisez cet outil que sur un ensemble limité de code et de processus (idéalement de l'exécution ponctuel dans un terminal)
  • Évitez son utilisation sur un serveur de production
  • Utilisez de préférence cet outil dans un environnement de développement ou de test

Ces précautions sont essentielles pour éviter des problèmes de performance qui pourraient affecter les utilisateurs ou les systèmes en production. Sachez que le code monitoré s'exécute environ 15-20% plus lentement que s'il ne l'est pas.

Utilisation

Exemple basique

// Création d'une instance de MonLBL
Set mon = ##class(dc.codemonitor.MonLBL).%New()

// Définition des routines à monitorer
Set mon.routines = $ListBuild("User.MaClasse.1")

// Démarrage du monitoring
Do mon.startMonitoring()

// Code à analyser...
// ...

// Arrêt du monitoring et génération des résultats
Do mon.stopMonitoring()

Note: Le monitoring démarré ici n'est valable que pour le processus courant.
Les autres processus qui exécuteraient le même code ne seront pas pris en compte dans les mesures.

Options de configuration

Le wrapper offre plusieurs options configurables :

  • directory : Répertoire où seront exportés les fichiers CSV (par défaut le répertoire Temp d'IRIS)
  • autoCompile : Recompile automatiquement les routines avec les flags "ck" si nécessaire
  • metrics : Liste personnalisable des métriques à collecter
  • decimalPointIsComma : Utilise la virgule comme séparateur décimal pour une meilleure compatibilité avec Excel
  • metricsEnabled : Active ou désactive la collecte des métriques ligne par ligne

Exemple d'utilisation avancée

Voici un exemple plus complet (disponible dans la classe dc.codemonitor.Example) :

ClassMethod MonitorGenerateNumber(parameters As %DynamicObject) As %Status
{
    Set sc = $$$OK
    Try {
        // Affichage des paramètres reçus
        Write "* Parameters :", !
        Set formatter = ##class(%JSON.Formatter).%New()
        Do formatter.Format(parameters)
        Write !
        
        // Création et configuration du moniteur
        Set monitor = ##class(dc.codemonitor.MonLBL).%New()
        
        // ATTENTION : en environnement de production, définissez autoCompile à $$$NO
        // et compilez manuellement le code à monitorer
        Set monitor.autoCompile = $$$YES
        Set monitor.metricsEnabled = $$$YES
        Set monitor.directory = ##class(%File).NormalizeDirectory(##class(%SYS.System).TempDirectory())
        Set monitor.decimalPointIsComma = $$$YES

        // Configuration de la routine à monitorer (forme "int" de la classe)
        // Pour trouver le nom exact de la routine, utilisez la commande :
        // Do $SYSTEM.OBJ.Compile("dc.codemonitor.DoSomething","ck")
        // La ligne "Compiling routine XXX" vous donnera le nom de la routine
        Set monitor.routines = $ListBuild("dc.codemonitor.DoSomething.1")

        // Démarrage du monitoring
        $$$TOE(sc, monitor.startMonitoring())
        
        // Exécution du code à monitorer avec gestion des erreurs
        Try {
            Do ##class(dc.codemonitor.DoSomething).GenerateNumber(parameters.Number)

            // Important : toujours arrêter le monitoring
            Do monitor.stopMonitoring()
        }
        Catch ex {
            // Arrêt du monitoring même en cas d'erreur
            Do monitor.stopMonitoring()
            Throw ex
        }
    }
    Catch ex {
        Set sc = ex.AsStatus()
        Do $SYSTEM.Status.DisplayError(sc)
    }

    Return sc
}

Cet exemple montre plusieurs bonnes pratiques importantes:

  • Utilisation d'un bloc Try/Catch pour gérer les erreurs
  • Arrêt systématique du monitoring, même en cas d'erreur
  • Documentation sur la façon de trouver le nom exact de la routine correspondant à une classe à monitorer
  • Paramétrage complet du moniteur

Exemple d'utilisation avec des pages CSP

MonLBL permet également de monitorer des pages CSP. Voici un exemple (également disponible dans la classe dc.codemonitor.ExampleCsp) :

ClassMethod MonitorCSP(parameters As %DynamicObject = {{}}) As %Status
{
    Set sc = $$$OK
    Try {
        // Affichage des paramètres reçus
        Write "* Parameters :", !
        Set formatter = ##class(%JSON.Formatter).%New()
        Do formatter.Format(parameters)
        Write !
        
        // Création et configuration du moniteur
        Set monitor = ##class(dc.codemonitor.MonLBL).%New()
        Set monitor.autoCompile = $$$YES
        Set monitor.metricsEnabled = $$$YES
        Set monitor.directory = ##class(%File).NormalizeDirectory(##class(%SYS.System).TempDirectory())
        Set monitor.decimalPointIsComma = $$$YES

        // Pour monitorer une page CSP, on utilise la routine générée
        // Exemple: /csp/user/menu.csp --> classe: csp.menu --> routine: csp.menu.1
        Set monitor.routines = $ListBuild("csp.menu.1")

        // Les pages CSP nécessitent les objets %session, %request et %response
        // On crée ces objets avec les paramètres nécessaires
        Set %request = ##class(%CSP.Request).%New()
        // Configurer les paramètres de requête si nécessaire
        // Set %request.Data("<param_name>", 1) = <value>
        Set %request.CgiEnvs("SERVER_NAME") = "localhost"
        Set %request.URL = "/csp/user/menu.csp"

        Set %session = ##class(%CSP.Session).%New(1234)
        // Configurer les données de session si nécessaire
        // Set %session.Data("<data_name>", 1) = <value>

        Set %response = ##class(%CSP.Response).%New()
            
        // Démarrage du monitoring
        $$$TOE(sc, monitor.startMonitoring())
        
        Try {
            // Pour éviter d'afficher le contenu de la page CSP dans le terminal,
            // on peut utiliser la classe IORedirect pour rediriger la sortie vers null
            // (nécessite l'installation via zpm "install io-redirect")
            Do ##class(IORedirect.Redirect).ToNull() 
            
            // Appel de la page CSP via sa méthode OnPage
            Do ##class(csp.menu).OnPage()
            
            // Restauration de la sortie standard
            Do ##class(IORedirect.Redirect).RestoreIO()

            // Arrêt du monitoring
            Do monitor.stopMonitoring()
        }
        Catch ex {
            // Toujours restaurer la sortie et arrêter le monitoring en cas d'erreur
            Do ##class(IORedirect.Redirect).RestoreIO()
            Do monitor.stopMonitoring()
           
            Throw ex
        }
    }
    Catch ex {
        Set sc = ex.AsStatus()
        Do $SYSTEM.Status.DisplayError(sc)
    }

    Return sc
}

Points importants pour le monitoring des pages CSP :

  1. Identification de la routine : Une page CSP est compilée en une classe et une routine. Par exemple, /csp/user/menu.csp génère la classe csp.menu et la routine csp.menu.1.

  2. Environnement CSP : Il est nécessaire de créer les objets de contexte CSP (%request, %session, %response) pour que la page s'exécute correctement.

  3. Redirection de sortie : Pour éviter que le contenu HTML ne s'affiche dans le terminal, on peut utiliser l'utilitaire IORedirect (disponible sur OpenExchange via zpm "install io-redirect").

  4. Appel de la page : L'exécution se fait via la méthode OnPage() de la classe générée.

Exemple de sortie

Voici un exemple de sortie obtenue lors de l'exécution de la méthode MonitorGenerateNumber :

USER>d ##class(dc.codemonitor.Example).MonitorGenerateNumber({"number":"100"})
* Parameters :
{
  "number":"100"
}

* Metrics are exported to /usr/irissys/mgr/Temp/dc.codemonitor.DoSomething.1.csv
* Perf results :
{
  "startDateTime":"2025-05-07 18:45:42",
  "systemCPUTime":0,
  "userCPUTime":0,
  "timing":0.000205,
  "lines":19,
  "gloRefs":14,
  "diskReadInMs":"0"
}

On peut observer dans cette sortie :

  1. L'affichage des paramètres d'entrée
  2. La confirmation que les métriques ont été exportées dans un fichier CSV
  3. Un résumé des performances globales au format JSON, incluant :
    • La date et l'heure de début
    • Le temps CPU système et utilisateur
    • Le temps d'exécution total
    • Le nombre de lignes exécutées
    • Le nombre de références globales
    • Le temps de lecture disque

Interprétation des résultats CSV

Après l'exécution, des fichiers CSV (1 par routine dans le $ListBuild routines) sont générés dans le répertoire configuré. Ces fichiers contiennent :

  • Le numéro de ligne
  • Les métriques collectées pour chaque ligne
  • Le code source de la ligne (attention si vous n'avez pas compilé les classes avec le flag "k", le code source ne sera pas disponible dans le fichier csv)

Voici un exemple du contenu d'un fichier CSV exporté (dc.codemonitor.DoSomething.1.csv) :

LigneRtnLineGloRefTimeTotalTimeCode
10000 ;dc.codemonitor.DoSomething.1
20000 ;Generated for class dc.codemonitor.DoSomething. Do NOT edit. 05/07/2025 10:16:07AM
30000 ;;59595738;dc.codemonitor.DoSomething
40000 ;
50000GenerateNumber(n=1000000) methodimpl {
6100,0000050,000005 For i=1:1:n {
710000,0000190,000019 Set number = $Random(100000)
810000,0000150,000015 Set isOdd = number # 2
910000,0000130,000013 }
10100,0000030,000003 Return }

Dans ce tableau, nous pouvons analyser :

  • RtnLine : Indique combien de fois chaque ligne a été exécutée (ici, les lignes 6 et 10 ont été exécutées une fois)
  • GloRef : Montre les références globales générées par chaque ligne
  • Time : Présente le temps d'exécution propre à chaque ligne
  • TotalTime : Affiche le temps total incluant les appels à d'autres routines

Ces données peuvent être facilement importées dans un tableur pour analyse approfondie. Les lignes les plus coûteuses en termes de temps ou d'accès aux données peuvent ainsi être facilement identifiées.

Remarque sur l'efficacité du cache

L'efficacité du cache de base de données (global buffer) peut masquer des problèmes de performance réels. Lors de l'analyse, les accès aux données peuvent paraître rapides grâce à ce cache, mais pourraient être bien plus lents dans certaines conditions réelles d'utilisation.

Sur les systèmes de développement, vous pouvez vider le cache entre vos mesures avec la commande suivante :

Do ClearBuffers^|"%SYS"|GLOBUFF()

⚠️ ATTENTION : Soyez prudent avec cette commande, car elle s'applique à l'ensemble du système. Ne l'utilisez jamais sur un environnement de production, car cela pourrait impacter les performances de toutes les applications en cours d'exécution.

Conclusion

Le monitoring ligne par ligne est un outil précieux pour l'analyse de performance de code ObjectScript. En identifiant précisément les lignes de code qui consomment le plus de ressources, il permet aux développeurs de gagner beaucoup de temps dans l'analyse de problèmes de lenteurs.

0
0 45
Article Sylvain Guilbaud · Mars 28, 2025 54m read

Depuis l'introduction d'Embedded Python, il y a toujours eu un doute sur ses performances par rapport à ObjectScript et J'en ai discuté à plusieurs reprises avec @Guillaume Rongier , eh bien, profitant du fait que je faisais une petite application pour capturer les données des concours publics en Espagne et pouvoir effectuer des recherches en utilisant les capacités de VectorSearch, j'ai vu l'opportunité de réaliser un petit test.

Données pour le test

Les informations relatives aux concours publics sont fournies mensuellement dans des fichiers XML à partir de cette URL  et le format typique des informations d'un concours est le suivant:

 
Spoiler
<entry>
        <id>https://contrataciondelestado.es/sindicacion/licitacionesPerfilContratante/15070121</id>
        <link href="https://contrataciondelestado.es/wps/poc?uri=deeplink:detalle_licitacion&amp;idEvl=zVQ4gDdr3Y436J9Lctlsuw%3D%3D"/>
        <summary type="text">Id licitación: C54243062800; Órgano de Contratación: Dirección General de Carreteras; Importe: 3088367.31 EUR; Estado: RES</summary>
        <title>39-MU-6280; 54.408/24. Actuaciones para el desarrollo del plan de acción contra el ruido de la Fase II en la Región de Murcia. Plan de Recuperación, Transformación y Resiliencia, Next Generation EU.</title>
        <updated>2024-12-19T07:58:39.502+01:00</updated>
        <cac-place-ext:ContractFolderStatus>
            <cbc:ContractFolderID>C54243062800</cbc:ContractFolderID>
            <cbc-place-ext:ContractFolderStatusCode listURI="https://contrataciondelestado.es/codice/cl/2.04/SyndicationContractFolderStatusCode-2.04.gc" languageID="es">RES</cbc-place-ext:ContractFolderStatusCode>
            <cac-place-ext:LocatedContractingParty>
                <cbc:ContractingPartyTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.10/ContractingAuthorityCode-2.10.gc">1</cbc:ContractingPartyTypeCode>
                <cbc:ActivityCode listURI="http://contrataciondelestado.es/codice/cl/2.10/ContractingAuthorityActivityCode-2.10.gc">1</cbc:ActivityCode>
                <cbc:BuyerProfileURIID>https://contrataciondelestado.es/wps/poc?uri=deeplink:perfilContratante&amp;idBp=bbqeQ9uN6YE%3D</cbc:BuyerProfileURIID>
                <cac:Party>
                    <cbc:WebsiteURI>http://www.fomento.es</cbc:WebsiteURI>
                    <cac:PartyIdentification>
                        <cbc:ID schemeName="DIR3">E00124905</cbc:ID>
                    </cac:PartyIdentification>
                    <cac:PartyIdentification>
                        <cbc:ID schemeName="NIF">S2817015G</cbc:ID>
                    </cac:PartyIdentification>
                    <cac:PartyIdentification>
                        <cbc:ID schemeName="ID_PLATAFORMA">10000170000108</cbc:ID>
                    </cac:PartyIdentification>
                    <cac:PartyName>
                        <cbc:Name>Dirección General de Carreteras</cbc:Name>
                    </cac:PartyName>
                    <cac:PostalAddress>
                        <cbc:CityName>Madrid</cbc:CityName>
                        <cbc:PostalZone>28071</cbc:PostalZone>
                        <cac:AddressLine>
                            <cbc:Line>Paseo de la Castellana 67 - Despacho B750</cbc:Line>
                        </cac:AddressLine>
                        <cac:Country>
                            <cbc:IdentificationCode listURI="http://contrataciondelestado.es/codice/cl/2.08/CountryIdentificationCode-2.08.gc">ES</cbc:IdentificationCode>
                            <cbc:Name>España</cbc:Name>
                        </cac:Country>
                    </cac:PostalAddress>
                    <cac:Contact>
                        <cbc:Name>Dirección General de Carreteras</cbc:Name>
                        <cbc:Telephone>915978341</cbc:Telephone>
                        <cbc:Telefax>915978547</cbc:Telefax>
                        <cbc:ElectronicMail>dgc.licitaciones@fomento.es</cbc:ElectronicMail>
                    </cac:Contact>
                </cac:Party>
                <cac-place-ext:ParentLocatedParty>
                    <cac:PartyName>
                        <cbc:Name>Dirección General de Carreteras</cbc:Name>
                    </cac:PartyName>
                    <cac-place-ext:ParentLocatedParty>
                        <cac:PartyName>
                            <cbc:Name>Secretaría General de Transporte Terrestre</cbc:Name>
                        </cac:PartyName>
                        <cac-place-ext:ParentLocatedParty>
                            <cac:PartyName>
<cbc:Name>Secretaría de Estado de Transportes y Movilidad Sostenible</cbc:Name>
                            </cac:PartyName>
                            <cac-place-ext:ParentLocatedParty>
<cac:PartyIdentification>
    <cbc:ID schemeName="DIR3">E05229701</cbc:ID>
</cac:PartyIdentification>
<cac:PartyName>
    <cbc:Name>Ministerio de Transportes y Movilidad Sostenible</cbc:Name>
</cac:PartyName>
<cac-place-ext:ParentLocatedParty>
    <cac:PartyIdentification>
        <cbc:ID schemeName="DIR3">EA9999999</cbc:ID>
    </cac:PartyIdentification>
    <cac:PartyName>
        <cbc:Name>ADMINISTRACIÓN GENERAL DEL ESTADO</cbc:Name>
    </cac:PartyName>
    <cac-place-ext:ParentLocatedParty>
        <cac:PartyName>
            <cbc:Name>Sector Público</cbc:Name>
        </cac:PartyName>
    </cac-place-ext:ParentLocatedParty>
</cac-place-ext:ParentLocatedParty>
                            </cac-place-ext:ParentLocatedParty>
                        </cac-place-ext:ParentLocatedParty>
                    </cac-place-ext:ParentLocatedParty>
                </cac-place-ext:ParentLocatedParty>
            </cac-place-ext:LocatedContractingParty>
            <cac:ProcurementProject>
                <cbc:Name>39-MU-6280; 54.408/24. Actuaciones para el desarrollo del plan de acción contra el ruido de la Fase II en la Región de Murcia. Plan de Recuperación, Transformación y Resiliencia, Next Generation EU.</cbc:Name>
                <cbc:TypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/ContractCode-2.08.gc">3</cbc:TypeCode>
                <cbc:SubTypeCode listURI="http://contrataciondelestado.es/codice/cl/1.04/WorksContractCode-1.04.gc">4500</cbc:SubTypeCode>
                <cbc:MixContractIndicator>false</cbc:MixContractIndicator>
                <cac:BudgetAmount>
                    <cbc:EstimatedOverallContractAmount currencyID="EUR">3706040.77</cbc:EstimatedOverallContractAmount>
                    <cbc:TotalAmount currencyID="EUR">3736924.45</cbc:TotalAmount>
                    <cbc:TaxExclusiveAmount currencyID="EUR">3088367.31</cbc:TaxExclusiveAmount>
                </cac:BudgetAmount>
                <cac:RequiredCommodityClassification>
                    <cbc:ItemClassificationCode listURI="http://contrataciondelestado.es/codice/cl/2.04/CPV2008-2.04.gc">45233000</cbc:ItemClassificationCode>
                </cac:RequiredCommodityClassification>
                <cac:RealizedLocation>
                    <cbc:CountrySubentity>Región de Murcia</cbc:CountrySubentity>
                    <cbc:CountrySubentityCode listURI="http://contrataciondelestado.es/codice/cl/2.08/NUTS-2021.gc">ES62</cbc:CountrySubentityCode>
                    <cac:Address>
                        <cac:Country>
                            <cbc:IdentificationCode listURI="http://contrataciondelestado.es/codice/cl/2.08/CountryIdentificationCode-2.08.gc">ES</cbc:IdentificationCode>
                            <cbc:Name>España</cbc:Name>
                        </cac:Country>
                    </cac:Address>
                </cac:RealizedLocation>
                <cac:PlannedPeriod>
                    <cbc:DurationMeasure unitCode="MON">6</cbc:DurationMeasure>
                </cac:PlannedPeriod>
            </cac:ProcurementProject>
            <cac:TenderResult>
                <cbc:ResultCode listURI="http://contrataciondelestado.es/codice/cl/2.09/TenderResultCode-2.09.gc">9</cbc:ResultCode>
                <cbc:Description>Por ser la ofertas más ventajosa</cbc:Description>
                <cbc:AwardDate>2024-11-26</cbc:AwardDate>
                <cbc:ReceivedTenderQuantity>20</cbc:ReceivedTenderQuantity>
                <cbc:LowerTenderAmount currencyID="EUR">2388215.74</cbc:LowerTenderAmount>
                <cbc:HigherTenderAmount currencyID="EUR">2764090</cbc:HigherTenderAmount>
                <cbc:SMEsReceivedTenderQuantity>6</cbc:SMEsReceivedTenderQuantity>
                <cbc:SMEAwardedIndicator>false</cbc:SMEAwardedIndicator>
                <cac:Contract>
                    <cbc:IssueDate>2024-12-18</cbc:IssueDate>
                </cac:Contract>
                <cac:WinningParty>
                    <cac:PartyIdentification>
                        <cbc:ID schemeName="NIF">A17013863</cbc:ID>
                    </cac:PartyIdentification>
                    <cac:PartyName>
                        <cbc:Name>CONSTRUCCIONES RUBAU, S.A.</cbc:Name>
                    </cac:PartyName>
                    <cac:PhysicalLocation>
                        <cac:Address>
                            <cbc:CityName>Madrid</cbc:CityName>
                            <cbc:PostalZone>28046</cbc:PostalZone>
                            <cac:Country>
<cbc:IdentificationCode listURI="http://contrataciondelestado.es/codice/cl/2.08/CountryIdentificationCode-2.08.gc" name="España">ES</cbc:IdentificationCode>
                            </cac:Country>
                        </cac:Address>
                    </cac:PhysicalLocation>
                </cac:WinningParty>
                <cac:AwardedTenderedProject>
                    <cac:LegalMonetaryTotal>
                        <cbc:TaxExclusiveAmount currencyID="EUR">2469149.66</cbc:TaxExclusiveAmount>
                        <cbc:PayableAmount currencyID="EUR">2987671.09</cbc:PayableAmount>
                    </cac:LegalMonetaryTotal>
                </cac:AwardedTenderedProject>
            </cac:TenderResult>
            <cac:TenderingTerms>
                <cbc:RequiredCurriculaIndicator>false</cbc:RequiredCurriculaIndicator>
                <cbc:VariantConstraintIndicator>false</cbc:VariantConstraintIndicator>
                <cbc:FundingProgramCode listURI="http://contrataciondelestado.es/codice/cl/2.08/FundingProgramCode-2.08.gc" name="Financiación con fondos de la UE">EU</cbc:FundingProgramCode>
                <cbc:FundingProgramCode listURI="http://contrataciondelestado.es/codice/cl/2.08/FundingProgramCode-2.08.gc" name="Asociado al Plan de Recuperación, Transformación y Resiliencia">PRTR</cbc:FundingProgramCode>
                <cbc:FundingProgram>NEXT GENERATION EU</cbc:FundingProgram>
                <cbc:ProcurementNationalLegislationCode listURI="https://contrataciondelestado.es/codice/cl/2.08/ProcurementNationalLegislationCode-2.08.gc">3</cbc:ProcurementNationalLegislationCode>
                <cbc:ReceivedAppealQuantity>0</cbc:ReceivedAppealQuantity>
                <cac:RequiredFinancialGuarantee>
                    <cbc:GuaranteeTypeCode listURI="http://contrataciondelestado.es/codice/cl/1.04/GuaranteeTypeCode-1.04.gc">3</cbc:GuaranteeTypeCode>
                    <cbc:AmountRate>5</cbc:AmountRate>
                </cac:RequiredFinancialGuarantee>
                <cac:RequiredFinancialGuarantee>
                    <cbc:GuaranteeTypeCode listURI="http://contrataciondelestado.es/codice/cl/1.04/GuaranteeTypeCode-1.04.gc">2</cbc:GuaranteeTypeCode>
                    <cbc:AmountRate>5</cbc:AmountRate>
                </cac:RequiredFinancialGuarantee>
                <cac:ProcurementLegislationDocumentReference>
                    <cbc:ID>N/A</cbc:ID>
                </cac:ProcurementLegislationDocumentReference>
                <cac:TendererQualificationRequest>
                    <cac:RequiredBusinessClassificationScheme>
                        <cbc:ID>RequiredBusinessProfileCode</cbc:ID>
                        <cac:ClassificationCategory>
                            <cbc:CodeValue>G6-5</cbc:CodeValue>
                        </cac:ClassificationCategory>
                    </cac:RequiredBusinessClassificationScheme>
                    <cac:FinancialEvaluationCriteria>
                        <cbc:EvaluationCriteriaTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.0/FinancialCapabilityTypeCode-2.0.gc">ZZZ</cbc:EvaluationCriteriaTypeCode>
                        <cbc:Description>Se acreditará con la clasificación</cbc:Description>
                    </cac:FinancialEvaluationCriteria>
                    <cac:SpecificTendererRequirement>
                        <cbc:RequirementTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/DeclarationTypeCode-2.08.gc">1</cbc:RequirementTypeCode>
                        <cbc:Description>Capacidad de obrar</cbc:Description>
                    </cac:SpecificTendererRequirement>
                </cac:TendererQualificationRequest>
                <cac:AwardingTerms>
                    <cac:AwardingCriteria>
                        <cbc:AwardingCriteriaTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.0/AwardingCriteriaCode-2.0.gc">SUBJ</cbc:AwardingCriteriaTypeCode>
                        <cbc:Description>Juicio de valor</cbc:Description>
                        <cbc:Note>Criterios evaluables mediante juicio de valor.</cbc:Note>
                        <cbc:AwardingCriteriaSubTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.09/AwardingCriteriaNotAutomaticallyEvaluatedSubTypeCode-2.09.gc">99</cbc:AwardingCriteriaSubTypeCode>
                        <cbc:WeightNumeric>49</cbc:WeightNumeric>
                    </cac:AwardingCriteria>
                    <cac:AwardingCriteria>
                        <cbc:AwardingCriteriaTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.0/AwardingCriteriaCode-2.0.gc">OBJ</cbc:AwardingCriteriaTypeCode>
                        <cbc:Description>Precio</cbc:Description>
                        <cbc:AwardingCriteriaSubTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.09/AwardingCriteriaAutomaticallyEvaluatedSubTypeCode-2.09.gc">1</cbc:AwardingCriteriaSubTypeCode>
                        <cbc:WeightNumeric>45.9</cbc:WeightNumeric>
                    </cac:AwardingCriteria>
                    <cac:AwardingCriteria>
                        <cbc:AwardingCriteriaTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.0/AwardingCriteriaCode-2.0.gc">OBJ</cbc:AwardingCriteriaTypeCode>
                        <cbc:Description>Incremento de gastos de ensayos</cbc:Description>
                        <cbc:Note>El licitador deberá consignar el porcentaje total de gastos de ensayo al que se compromete</cbc:Note>
                        <cbc:AwardingCriteriaSubTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.09/AwardingCriteriaAutomaticallyEvaluatedSubTypeCode-2.09.gc">2</cbc:AwardingCriteriaSubTypeCode>
                        <cbc:WeightNumeric>5.1</cbc:WeightNumeric>
                    </cac:AwardingCriteria>
                </cac:AwardingTerms>
                <cac:TenderRecipientParty>
                    <cbc:EndpointID>https://contrataciondelestado.es</cbc:EndpointID>
                </cac:TenderRecipientParty>
                <cac:Language>
                    <cbc:ID>es</cbc:ID>
                </cac:Language>
            </cac:TenderingTerms>
            <cac:TenderingProcess>
                <cbc:ProcedureCode listURI="https://contrataciondelestado.es/codice/cl/2.07/SyndicationTenderingProcessCode-2.07.gc">1</cbc:ProcedureCode>
                <cbc:UrgencyCode listURI="http://contrataciondelestado.es/codice/cl/1.04/DiligenceTypeCode-1.04.gc">1</cbc:UrgencyCode>
                <cbc:ContractingSystemCode listURI="http://contrataciondelestado.es/codice/cl/2.08/ContractingSystemTypeCode-2.08.gc">0</cbc:ContractingSystemCode>
                <cbc:SubmissionMethodCode listURI="http://contrataciondelestado.es/codice/cl/1.04/TenderDeliveryCode-1.04.gc">1</cbc:SubmissionMethodCode>
                <cbc:OverThresholdIndicator>false</cbc:OverThresholdIndicator>
                <cac:DocumentAvailabilityPeriod>
                    <cbc:EndDate>2024-07-11</cbc:EndDate>
                    <cbc:EndTime>19:00:00</cbc:EndTime>
                </cac:DocumentAvailabilityPeriod>
                <cac:TenderSubmissionDeadlinePeriod>
                    <cbc:EndDate>2024-07-11</cbc:EndDate>
                    <cbc:EndTime>19:00:00</cbc:EndTime>
                </cac:TenderSubmissionDeadlinePeriod>
                <cac:AuctionTerms>
                    <cbc:AuctionConstraintIndicator>false</cbc:AuctionConstraintIndicator>
                </cac:AuctionTerms>
            </cac:TenderingProcess>
            <cac:LegalDocumentReference>
                <cbc:ID>10 39-MU-6280_PCAP1 COMPLETO.pdf</cbc:ID>
                <cac:Attachment>
                    <cac:ExternalReference>
                        <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=e5192f55-437c-4336-bcae-fa5ed89ed137</cbc:URI>
                        <cbc:DocumentHash>TaNyzb5w+ClWd4GazjY4A6j0n90=</cbc:DocumentHash>
                    </cac:ExternalReference>
                </cac:Attachment>
            </cac:LegalDocumentReference>
            <cac:TechnicalDocumentReference>
                <cbc:ID>13 39-MU-6280 ENLACES PROYECTO.pdf</cbc:ID>
                <cac:Attachment>
                    <cac:ExternalReference>
                        <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=5f34283e-8f1f-4111-83bc-6ee7c627f85a</cbc:URI>
                        <cbc:DocumentHash>C+IvGwHro7JPdvGq0G0Zw5qd4jQ=</cbc:DocumentHash>
                    </cac:ExternalReference>
                </cac:Attachment>
            </cac:TechnicalDocumentReference>
            <cac:AdditionalDocumentReference>
                <cbc:ID>39-MU-6280.xml</cbc:ID>
                <cac:Attachment>
                    <cac:ExternalReference>
                        <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=f0de9e5c-3148-464a-9d1d-6755d7ce3e97</cbc:URI>
                        <cbc:DocumentHash>Mo8VWVuBQYTANqJQ8I2ydW6lmkM=</cbc:DocumentHash>
                    </cac:ExternalReference>
                </cac:Attachment>
            </cac:AdditionalDocumentReference>
            <cac:AdditionalDocumentReference>
                <cbc:ID>Instrucciones DEUC.pdf</cbc:ID>
                <cac:Attachment>
                    <cac:ExternalReference>
                        <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=f8ffa4b2-7c67-41f4-ad8c-490a1cd924e4</cbc:URI>
                        <cbc:DocumentHash>2XK7llhvbjvUXAN8KlDbZhtleg8=</cbc:DocumentHash>
                    </cac:ExternalReference>
                </cac:Attachment>
            </cac:AdditionalDocumentReference>
            <cac:AdditionalDocumentReference>
                <cbc:ID>39-MU-6280.pdf</cbc:ID>
                <cac:Attachment>
                    <cac:ExternalReference>
                        <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=d52c9815-e054-4aea-aed0-b0a43a9c77d5</cbc:URI>
                        <cbc:DocumentHash>B7+m/6Aq3+UHDBARLcj9tZQJivo=</cbc:DocumentHash>
                    </cac:ExternalReference>
                </cac:Attachment>
            </cac:AdditionalDocumentReference>
            <cac-place-ext:ValidNoticeInfo>
                <cbc-place-ext:NoticeTypeCode listURI="https://contrataciondelestado.es/codice/cl/2.11/TenderingNoticeTypeCode-2.11.gc">DOC_CN</cbc-place-ext:NoticeTypeCode>
                <cac-place-ext:AdditionalPublicationStatus>
                    <cbc-place-ext:PublicationMediaName>BOE</cbc-place-ext:PublicationMediaName>
                    <cac-place-ext:AdditionalPublicationDocumentReference>
                        <cbc:IssueDate>2024-06-13</cbc:IssueDate>
                    </cac-place-ext:AdditionalPublicationDocumentReference>
                </cac-place-ext:AdditionalPublicationStatus>
            </cac-place-ext:ValidNoticeInfo>
            <cac-place-ext:ValidNoticeInfo>
                <cbc-place-ext:NoticeTypeCode listURI="https://contrataciondelestado.es/codice/cl/2.11/TenderingNoticeTypeCode-2.11.gc">DOC_CAN_ADJ</cbc-place-ext:NoticeTypeCode>
                <cac-place-ext:AdditionalPublicationStatus>
                    <cbc-place-ext:PublicationMediaName>Perfil del contratante</cbc-place-ext:PublicationMediaName>
                    <cac-place-ext:AdditionalPublicationDocumentReference>
                        <cbc:IssueDate>2024-11-27</cbc:IssueDate>
                    </cac-place-ext:AdditionalPublicationDocumentReference>
                </cac-place-ext:AdditionalPublicationStatus>
            </cac-place-ext:ValidNoticeInfo>
            <cac-place-ext:ValidNoticeInfo>
                <cbc-place-ext:NoticeTypeCode listURI="https://contrataciondelestado.es/codice/cl/2.11/TenderingNoticeTypeCode-2.11.gc">DOC_CD</cbc-place-ext:NoticeTypeCode>
                <cac-place-ext:AdditionalPublicationStatus>
                    <cbc-place-ext:PublicationMediaName>Perfil del contratante</cbc-place-ext:PublicationMediaName>
                    <cac-place-ext:AdditionalPublicationDocumentReference>
                        <cbc:IssueDate>2024-06-06</cbc:IssueDate>
                    </cac-place-ext:AdditionalPublicationDocumentReference>
                </cac-place-ext:AdditionalPublicationStatus>
            </cac-place-ext:ValidNoticeInfo>
            <cac-place-ext:ValidNoticeInfo>
                <cbc-place-ext:NoticeTypeCode listURI="https://contrataciondelestado.es/codice/cl/2.11/TenderingNoticeTypeCode-2.11.gc">DOC_CN</cbc-place-ext:NoticeTypeCode>
                <cac-place-ext:AdditionalPublicationStatus>
                    <cbc-place-ext:PublicationMediaName>Perfil del contratante</cbc-place-ext:PublicationMediaName>
                    <cac-place-ext:AdditionalPublicationDocumentReference>
                        <cbc:IssueDate>2024-06-06</cbc:IssueDate>
                    </cac-place-ext:AdditionalPublicationDocumentReference>
                </cac-place-ext:AdditionalPublicationStatus>
            </cac-place-ext:ValidNoticeInfo>
            <cac-place-ext:ValidNoticeInfo>
                <cbc-place-ext:NoticeTypeCode listURI="https://contrataciondelestado.es/codice/cl/2.11/TenderingNoticeTypeCode-2.11.gc">DOC_FORM</cbc-place-ext:NoticeTypeCode>
                <cac-place-ext:AdditionalPublicationStatus>
                    <cbc-place-ext:PublicationMediaName>Perfil del contratante</cbc-place-ext:PublicationMediaName>
                    <cac-place-ext:AdditionalPublicationDocumentReference>
                        <cbc:IssueDate>2024-12-19</cbc:IssueDate>
                    </cac-place-ext:AdditionalPublicationDocumentReference>
                </cac-place-ext:AdditionalPublicationStatus>
            </cac-place-ext:ValidNoticeInfo>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-808fc79b-06f9-4f36-9949-667977a5f86d</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">14</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-808fc79b-06f9-4f36-9949-667977a5f86d</cbc:URI>
                            <cbc:FileName>Informe sobre las ofertas incursas en presunción de anormalidad</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-715e4b11-0c10-44d2-9127-81a56418ae13</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">12</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-715e4b11-0c10-44d2-9127-81a56418ae13</cbc:URI>
                            <cbc:FileName>Acta del órgano de asistencia</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-f761442b-7a0c-44b8-9bd5-7c09187c8ebe</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">1</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-f761442b-7a0c-44b8-9bd5-7c09187c8ebe</cbc:URI>
                            <cbc:FileName>Actos públicos informativos o de aperturas de ofertas</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-526c90fa-93d8-4db8-8feb-dc96d6a5bfce</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">12</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-526c90fa-93d8-4db8-8feb-dc96d6a5bfce</cbc:URI>
                            <cbc:FileName>Acta del órgano de asistencia</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-1a70d65c-0e41-4302-a9be-4a39a072903e</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">12</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-1a70d65c-0e41-4302-a9be-4a39a072903e</cbc:URI>
                            <cbc:FileName>Acta del órgano de asistencia</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-84248c0b-2d31-4307-9e10-0642ab0ac3aa</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">12</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-84248c0b-2d31-4307-9e10-0642ab0ac3aa</cbc:URI>
                            <cbc:FileName>Acta del órgano de asistencia</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-476ebe20-72eb-479e-9815-05fc29efd728</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">12</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-476ebe20-72eb-479e-9815-05fc29efd728</cbc:URI>
                            <cbc:FileName>Acta del órgano de asistencia</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-96d6f24e-8931-47c4-9feb-a87ed7bddcbe</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">13</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-96d6f24e-8931-47c4-9feb-a87ed7bddcbe</cbc:URI>
                            <cbc:FileName>Informe de valoración de los criterios de adjudicación cuantificables mediante juicio de valor</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>2024-3f052589-4887-46d8-9a0a-f821ced98d8a</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">1</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=2024-3f052589-4887-46d8-9a0a-f821ced98d8a</cbc:URI>
                            <cbc:FileName>Actos públicos informativos o de aperturas de ofertas</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>f8896783-0abc-48ab-87c1-cf1f5cd7e367</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">11</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=f8896783-0abc-48ab-87c1-cf1f5cd7e367</cbc:URI>
                            <cbc:FileName>Documento de aprobación del expediente</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>d93158fa-158c-4ddf-a037-ea8ae1372ddb</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">8</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=d93158fa-158c-4ddf-a037-ea8ae1372ddb</cbc:URI>
                            <cbc:FileName>Acuerdo de iniciación del expediente</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
            <cac-place-ext:GeneralDocument>
                <cac-place-ext:GeneralDocumentDocumentReference>
                    <cbc:ID>daffb941-c1b3-415a-950a-bcf2c51f13fa</cbc:ID>
                    <cbc:DocumentTypeCode listURI="http://contrataciondelestado.es/codice/cl/2.08/GeneralContractDocuments-2.08.gc">ZZZ</cbc:DocumentTypeCode>
                    <cac:Attachment>
                        <cac:ExternalReference>
                            <cbc:URI>https://contrataciondelestado.es/wps/wcm/connect/PLACE_es/Site/area/docAccCmpnt?srv=cmpnt&amp;cmpntname=GetDocumentsById&amp;source=library&amp;DocumentIdParam=daffb941-c1b3-415a-950a-bcf2c51f13fa</cbc:URI>
                            <cbc:FileName>Enlace nuevo a proyecto</cbc:FileName>
                        </cac:ExternalReference>
                    </cac:Attachment>
                </cac-place-ext:GeneralDocumentDocumentReference>
            </cac-place-ext:GeneralDocument>
        </cac-place-ext:ContractFolderStatus>
    </entry>
 

Comme vous le voyez, chaque concours a des dimensions considérables et dans chaque fichier, nous pouvons trouver environ 450 concours. Cette dimension ne permet pas d'utiliser une classe d'ObjectScript pour le mappage (on pourrait... mais je ne suis pas prêt à le faire).  

Codes pour les tests

Mon idée est de capturer uniquement les champs pertinents pour les recherches ultérieures. Pour ce faire, j'ai créé la classe suivante qui nous servira à stocker les informations capturées:

Class Inquisidor.Object.Licitacion Extends (%Persistent, %XML.Adaptor) [ DdlAllowed ]
{

Property IdLicitacion As%String(MAXLEN = 200);Property Titulo As%String(MAXLEN = 2000);Property URL As%String(MAXLEN = 1000);Property Resumen As%String(MAXLEN = 2000);Property TituloVectorizado As%Vector(DATATYPE = "DECIMAL", LEN = 384);Property Contratante As%String(MAXLEN = 2000);Property URLContratante As%String(MAXLEN = 2000);Property ValorEstimado As%Numeric(STORAGEDEFAULT = "columnar");Property ImporteTotal As%Numeric(STORAGEDEFAULT = "columnar");Property ImporteTotalSinImpuestos As%Numeric(STORAGEDEFAULT = "columnar");Property FechaAdjudicacion As%Date;Property Estado As%String;Property Ganador As%String(MAXLEN = 200);Property ImporteGanador As%Numeric(STORAGEDEFAULT = "columnar");Property ImporteGanadorSinImpuestos As%Numeric(STORAGEDEFAULT = "columnar");Property Clasificacion As%String(MAXLEN = 10);Property Localizacion As%String(MAXLEN = 200); Index IndexContratante On Contratante; Index IndexGanador On Ganador; Index IndexClasificacion On Clasificacion; Index IndexLocalizacion On Localizacion; Index IndexIdLicitation On IdLicitacion [ PrimaryKey ]; }

Pour capturer les données à l'aide d'Embedded Python, j'ai utilisé la bibliothèque  xml.etree.ElementTree  qui nous permet d'extraire les valeurs nœud par nœud. Voici la méthode Python que j'ai utilisée pour le mappage du XML:

Method ReadXML(xmlPath As %String) As %String [ Language = python ]
{
    import xml.etree.ElementTree as ET
    import iris
    import pandas as pd
<span class="hljs-keyword">try</span> :
    tree = ET.parse(xmlPath)
    root = tree.getroot()
    <span class="hljs-keyword">for</span> entry <span class="hljs-keyword">in</span> root.iter(<span class="hljs-string">"{http://www.w3.org/2005/Atom}entry"</span>):
        licitacion = {<span class="hljs-string">"titulo"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"resumen"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"idlicitacion"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"url"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"contratante"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"urlcontratante"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"estado"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"valorestimado"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"importetotal"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"importetotalsinimpuestos"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"clasificacion"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"localizacion"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"fechaadjudicacion"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"ganador"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"importeganadorsinimpuestos"</span>: <span class="hljs-string">""</span>, <span class="hljs-string">"importeganador"</span>: <span class="hljs-string">""</span>}
        <span class="hljs-keyword">for</span> tags <span class="hljs-keyword">in</span> entry:
            <span class="hljs-keyword">if</span> tags.tag == <span class="hljs-string">"{http://www.w3.org/2005/Atom}title"</span>:
                licitacion[<span class="hljs-string">"titulo"</span>] = tags.text
            <span class="hljs-keyword">if</span> tags.tag == <span class="hljs-string">"{http://www.w3.org/2005/Atom}summary"</span>:
                licitacion[<span class="hljs-string">"resumen"</span>] = tags.text
            <span class="hljs-keyword">if</span> tags.tag == <span class="hljs-string">"{http://www.w3.org/2005/Atom}id"</span>:
                licitacion[<span class="hljs-string">"idlicitacion"</span>] = tags.text
            <span class="hljs-keyword">if</span> tags.tag == <span class="hljs-string">"{http://www.w3.org/2005/Atom}link"</span>:
                licitacion[<span class="hljs-string">"url"</span>] = tags.attrib[<span class="hljs-string">"href"</span>]
            <span class="hljs-keyword">if</span> tags.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice-place-ext:schema:xsd:CommonAggregateComponents-2}ContractFolderStatus"</span>:
                <span class="hljs-keyword">for</span> detailTags <span class="hljs-keyword">in</span> tags:
                    <span class="hljs-keyword">if</span> detailTags.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice-place-ext:schema:xsd:CommonAggregateComponents-2}LocatedContractingParty"</span>:
                        <span class="hljs-keyword">for</span> infoContractor <span class="hljs-keyword">in</span> detailTags:
                            <span class="hljs-keyword">if</span> infoContractor.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}Party"</span>:
                                <span class="hljs-keyword">for</span> contractorDetails <span class="hljs-keyword">in</span> infoContractor:
                                    <span class="hljs-keyword">if</span> contractorDetails.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}PartyName"</span> :
                                        <span class="hljs-keyword">for</span> name <span class="hljs-keyword">in</span> contractorDetails:
                                            licitacion[<span class="hljs-string">"contratante"</span>] = name.text
                                    <span class="hljs-keyword">elif</span> contractorDetails.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}WebsiteURI"</span>:
                                        licitacion[<span class="hljs-string">"urlcontratante"</span>] = contractorDetails.text
                    <span class="hljs-keyword">elif</span> detailTags.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice-place-ext:schema:xsd:CommonAggregateComponents-2}ContractFolderStatusCode"</span>:
                        licitacion[<span class="hljs-string">"estado"</span>] = detailTags.text
                    <span class="hljs-keyword">elif</span> detailTags.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}ProcurementProject"</span>:
                        <span class="hljs-keyword">for</span> infoProcurement <span class="hljs-keyword">in</span> detailTags:
                            <span class="hljs-keyword">if</span> infoProcurement.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}BudgetAmount"</span>:
                                <span class="hljs-keyword">for</span> detailBudget <span class="hljs-keyword">in</span> infoProcurement:
                                    <span class="hljs-keyword">if</span> detailBudget.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}EstimatedOverallContractAmount"</span>:
                                        licitacion[<span class="hljs-string">"valorestimado"</span>] = detailBudget.text
                                    <span class="hljs-keyword">elif</span> detailBudget.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}TotalAmount"</span>:
                                        licitacion[<span class="hljs-string">"importetotal"</span>] = detailBudget.text
                                    <span class="hljs-keyword">elif</span> detailBudget.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}TaxExclusiveAmount"</span>:
                                        licitacion[<span class="hljs-string">"importetotalsinimpuestos"</span>] = detailBudget.text
                            <span class="hljs-keyword">elif</span> infoProcurement.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}RequiredCommodityClassification"</span>:
                                <span class="hljs-keyword">for</span> detailClassification <span class="hljs-keyword">in</span> infoProcurement:
                                    <span class="hljs-keyword">if</span> detailClassification.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}ItemClassificationCode"</span>:
                                        licitacion[<span class="hljs-string">"clasificacion"</span>] = detailClassification.text
                            <span class="hljs-keyword">elif</span> infoProcurement.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}RealizedLocation"</span>:
                                <span class="hljs-keyword">for</span> detailLocalization <span class="hljs-keyword">in</span> infoProcurement:
                                    <span class="hljs-keyword">if</span> detailLocalization.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}CountrySubentity"</span>:
                                        licitacion[<span class="hljs-string">"localizacion"</span>] = detailLocalization.text
                    <span class="hljs-keyword">elif</span> detailTags.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}TenderResult"</span>:
                        <span class="hljs-keyword">for</span> infoResult <span class="hljs-keyword">in</span> detailTags:
                            <span class="hljs-keyword">if</span> infoResult.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}AwardDate"</span>:
                                licitacion[<span class="hljs-string">"fechaadjudicacion"</span>] = infoResult.text
                            <span class="hljs-keyword">elif</span> infoResult.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}WinningParty"</span>:
                                <span class="hljs-keyword">for</span> detailWinner <span class="hljs-keyword">in</span> infoResult:
                                    <span class="hljs-keyword">if</span> detailWinner.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}PartyName"</span>:
                                        <span class="hljs-keyword">for</span> detailName <span class="hljs-keyword">in</span> detailWinner:
                                            <span class="hljs-keyword">if</span> detailName.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}Name"</span>:
                                                licitacion[<span class="hljs-string">"ganador"</span>] = detailName.text
                            <span class="hljs-keyword">elif</span> infoResult.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}AwardedTenderedProject"</span>:
                                <span class="hljs-keyword">for</span> detailTender <span class="hljs-keyword">in</span> infoResult:
                                    <span class="hljs-keyword">if</span> detailTender.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonAggregateComponents-2}LegalMonetaryTotal"</span>:
                                        <span class="hljs-keyword">for</span> detailWinnerAmount <span class="hljs-keyword">in</span> detailTender:
                                            <span class="hljs-keyword">if</span> detailWinnerAmount.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}TaxExclusiveAmount"</span>:
                                                licitacion[<span class="hljs-string">"importeganadorsinimpuestos"</span>] = detailWinnerAmount.text
                                            <span class="hljs-keyword">elif</span> detailWinnerAmount.tag == <span class="hljs-string">"{urn:dgpe:names:draft:codice:schema:xsd:CommonBasicComponents-2}PayableAmount"</span>:
                                                licitacion[<span class="hljs-string">"importeganador"</span>] = detailWinnerAmount.text
        iris.cls(<span class="hljs-string">"Ens.Util.Log"</span>).LogInfo(<span class="hljs-string">"Inquisidor.BP.XMLToLicitacion"</span>, <span class="hljs-string">"VectorizePatient"</span>, <span class="hljs-string">"Terminado mapeo "</span>+licitacion[<span class="hljs-string">"titulo"</span>])
        <span class="hljs-keyword">if</span> licitacion.get(<span class="hljs-string">"importeganador"</span>) <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">None</span> <span class="hljs-keyword">and</span> licitacion.get(<span class="hljs-string">"importeganador"</span>) <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-string">""</span>:
            iris.cls(<span class="hljs-string">"Ens.Util.Log"</span>).LogInfo(<span class="hljs-string">"Inquisidor.BP.XMLToLicitacion"</span>, <span class="hljs-string">"VectorizePatient"</span>, <span class="hljs-string">"Lanzando insert "</span>+licitacion[<span class="hljs-string">"titulo"</span>])
            stmt = iris.sql.prepare(<span class="hljs-string">"INSERT INTO INQUISIDOR_Object.Licitacion (Titulo, Resumen, IdLicitacion, URL, Contratante, URLContratante, Estado, ValorEstimado, ImporteTotal, ImporteTotalSinImpuestos, Clasificacion, Localizacion, FechaAdjudicacion, Ganador, ImporteGanadorSinImpuestos, ImporteGanador) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,TO_DATE(?,'YYYY-MM-DD'),?,?,?)"</span>)
            <span class="hljs-keyword">try</span>:
                rs = stmt.execute(licitacion[<span class="hljs-string">"titulo"</span>], licitacion[<span class="hljs-string">"resumen"</span>], licitacion[<span class="hljs-string">"idlicitacion"</span>], licitacion[<span class="hljs-string">"url"</span>], licitacion[<span class="hljs-string">"contratante"</span>], licitacion[<span class="hljs-string">"urlcontratante"</span>], licitacion[<span class="hljs-string">"estado"</span>], licitacion[<span class="hljs-string">"valorestimado"</span>], licitacion[<span class="hljs-string">"importetotal"</span>], licitacion[<span class="hljs-string">"importetotalsinimpuestos"</span>], licitacion[<span class="hljs-string">"clasificacion"</span>], licitacion[<span class="hljs-string">"localizacion"</span>], licitacion[<span class="hljs-string">"fechaadjudicacion"</span>], licitacion[<span class="hljs-string">"ganador"</span>], licitacion[<span class="hljs-string">"importeganadorsinimpuestos"</span>], licitacion[<span class="hljs-string">"importeganador"</span>])
            <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> err:
                iris.cls(<span class="hljs-string">"Ens.Util.Log"</span>).LogInfo(<span class="hljs-string">"Inquisidor.BP.XMLToLicitacion"</span>, <span class="hljs-string">"VectorizePatient"</span>, repr(err))
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Success"</span>
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> err:
    iris.cls(<span class="hljs-string">"Ens.Util.Log"</span>).LogInfo(<span class="hljs-string">"Inquisidor.BP.XMLToLicitacion"</span>, <span class="hljs-string">"VectorizePatient"</span>, repr(err))
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Error"</span>

}

Une fois le mappage terminé, nous procédons à un simple insert avec l'enregistrement.

Pour le mappage en utilisant ObjectScript, j'ai utilisé la fonctionnalité %XML.TextReader , voyons la méthode:

Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As%Status
{
    set filename = pRequest.OriginalFilename

    set status=##class(%XML.TextReader).ParseFile(filename,.textreader)
    //check statusif$$$ISERR(status) {do$System.Status.DisplayError(status) quit}
    set tStatement = ##class(%SQL.Statement).%New()
    //iterate through document, node by nodewhile textreader.Read()
    {        

        if ((textreader.NodeType = "element") && (textreader.Depth = 2) && (textreader.Path = "/feed/entry")) {
            if ($DATA(licitacion))
            {                
                if (licitacion.ImporteGanador '= ""){
                    //set sc = licitacion.%Save()set myquery = "INSERT INTO INQUISIDOR_Object.LicitacionOS (Titulo, Resumen, IdLicitacion, URL, Contratante, URLContratante, Estado, ValorEstimado, ImporteTotal, ImporteTotalSinImpuestos, Clasificacion, Localizacion, FechaAdjudicacion, Ganador, ImporteGanadorSinImpuestos, ImporteGanador) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"set qStatus = tStatement.%Prepare(myquery)
                    if qStatus '= 1 {
                        write"%Prepare failed:"do$System.Status.DisplayError(qStatus)
                        quit
                    }
                    set rset = tStatement.%Execute(licitacion.Titulo, licitacion.Resumen, licitacion.IdLicitacion, licitacion.URL, licitacion.Contratante, licitacion.URLContratante, licitacion.Estado, licitacion.ValorEstimado, licitacion.ImporteTotal, licitacion.ImporteTotalSinImpuestos, licitacion.Clasificacion, licitacion.Localizacion, licitacion.FechaAdjudicacion, licitacion.Ganador, licitacion.ImporteGanadorSinImpuestos, licitacion.ImporteGanador)
                }                
            }
            set licitacion = ##class(Inquisidor.Object.LicitacionOS).%New()
        }        

        if (textreader.Path = "/feed/entry/title"){
            if (textreader.Value '= ""){
                set licitacion.Titulo = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/summary"){
            if (textreader.Value '= ""){
                set licitacion.Resumen = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/id"){
            if (textreader.Value '= ""){
                set licitacion.IdLicitacion = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/link"){            
            if (textreader.MoveToAttributeName("href")) {
                set licitacion.URL = textreader.Value                
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cbc-place-ext:ContractFolderStatusCode"){
            if (textreader.Value '= ""){
                set licitacion.Estado = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac-place-ext:LocatedContractingParty/cac:Party/cac:PartyName"){
            if (textreader.Value '= ""){
                set licitacion.Contratante = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac-place-ext:LocatedContractingParty/cac:Party/cbc:WebsiteURI"){
            if (textreader.Value '= ""){
                set licitacion.URLContratante = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:BudgetAmount/cbc:EstimatedOverallContractAmount"){
            if (textreader.Value '= ""){
                set licitacion.ValorEstimado = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:BudgetAmount/cbc:TotalAmount"){
            if (textreader.Value '= ""){
                set licitacion.ImporteTotal = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:BudgetAmount/cbc:TaxExclusiveAmount"){
            if (textreader.Value '= ""){
                set licitacion.ImporteTotalSinImpuestos = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:RequiredCommodityClassification/cbc:ItemClassificationCode"){
            if (textreader.Value '= ""){
                set licitacion.Clasificacion = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:ProcurementProject/cac:RealizedLocation/cbc:CountrySubentity"){
            if (textreader.Value '= ""){
                set licitacion.Localizacion = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cbc:AwardDate"){
            if (textreader.Value '= ""){
                set licitacion.FechaAdjudicacion = $System.SQL.Functions.TODATE(textreader.Value,"YYYY-MM-DD")
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cac:WinningParty/cac:PartyName/cbc:Name"){
            if (textreader.Value '= ""){
                set licitacion.Ganador = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cac:AwardedTenderedProject/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount"){
            if (textreader.Value '= ""){
                set licitacion.ImporteGanadorSinImpuestos = textreader.Value
            }
        }
        if (textreader.Path = "/feed/entry/cac-place-ext:ContractFolderStatus/cac:TenderResult/cac:AwardedTenderedProject/cac:LegalMonetaryTotal/cbc:PayableAmount"){
            if (textreader.Value '= ""){
                set licitacion.ImporteGanador = textreader.Value
            }
        }       
    }    
    // set resultEmbeddings = ..GenerateEmbeddings()Quit$$$OK
}

Les deux codes n'enregistreront dans la base de données que les concours qui ont déjà été résolus (pour lesquels le montant total gagnant a été communiqué).

Configuration de la production

Avec nos méthodes mises en œuvre dans les processus métier correspondants, il ne nous reste plus qu'à configurer la production pour notre test, ce qui nous permettra d'alimenter les deux méthodes. Il suffit d'ajouter deux services métier qui se limiteront à capturer les fichiers avec les informations XML et à les transmettre aux processus métier.

Pour éviter toute interférence lors de la capture et de la transmission des informations aux processus métier, nous allons créer deux services métier. La production se présentera comme suit:

Pour le test, nous allons introduire les concours publics du mois de février, soit un total de 91 fichiers avec 1,30 Go de données. Voyons comment se comportent les deux codes.

À vos marques...

Prêts...

C'est parti!

Résultats du mappage XML en ObjectScript

Commençons par le temps nécessaire au code ObjectScript pour mapper les 91 fichiers:

Le premier fichier a été démarré à 21:11:15, voyons quand le dernier fichier a été mappé:

Si nous regardons les détails du dernier message, nous pouvons voir la date finale du traitement:

L'heure finale est 21:17:43, ce qui donne un temps de traitement de 6 minutes et 28 secondes.

Résultats du mappage XML en Embedded Python

Répétez la même opération avec le processus qui utilise Python:

Début à 21:11:15 comme dans le cas précédent, voyons quand l'opération s'est terminée:

Voyons le message en détail pour connaître la fin exacte:

L'heure finale était 21:12:03, ce qui nous amène à un total de 48 secondes.

Eh bien, le gagnant est connu ! Dans ce round, Embedded Python a battu ObjectScript, du moins en ce qui concerne l'analyse XML. Si vous avez des suggestions ou des améliorations à apporter au code des deux méthodes, je vous encourage à les mettre dans les commentaires et je répéterai les tests pour vérifier les améliorations possibles.

Ce que nous pouvons dire, c'est qu'en ce qui concerne la supériorité évidente de la performance d'ObjectScript par rapport à Python... le mythe est brisé!

myth-busted – Mike Raffety, DTM, PID

0
0 35
Article Sylvain Guilbaud · Fév 21, 2025 6m read

InterSystems est à l'avant-garde de la technologie des bases de données depuis sa création, en étant à l'origine d'innovations qui surpassent régulièrement ses concurrents comme Oracle, IBM et Microsoft. En se concentrant sur une conception de noyau efficace et en adoptant une approche sans compromis des performances des données, InterSystems s'est taillé une place de choix dans les applications critiques, garantissant fiabilité, rapidité et évolutivité.

Une histoire d'excellence technique

0
0 66
Article Guillaume Rongier · Fév 12, 2025 5m read

Cela fait longtemps que je n'ai pas écrit de post de mise à jour sur l'IoP.

image

Quelles sont les nouveautés depuis la publication de l'interface en ligne de commande de l'IoP?

Deux nouvelles fonctionnalités importantes ont été ajoutées à l'IoP:

  • Rebranding: le module grongier.pex a été renommé en iop pour refléter le nouveau nom du projet.
  • Support des opérations asynchrones: L'interface de programmation prend désormais en charge les fonctions asynchrones et les coroutines.

Rebranding

Le module grongier.pex a été renommé en iop pour refléter le nouveau nom du projet.

Le module grongier.pex est encore disponible pour des raisons de rétrocompatibilité, mais il sera supprimé à l'avenir.

Support des opérations asynchrones

L'IoP supporte les appels asynchrones depuis longtemps, mais il n'était pas possible d'utiliser les fonctions asynchrones et les coroutines directement dans l'IoP..

Avant d'aborder cette nouvelle fonctionnalité, je vais expliquer le fonctionnement des appels asynchrones dans InterSystems IRIS et présenter deux exemples d'utilisation des appels asynchrones dans l'IoP.

Appels asynchrones hérités

Examinons le fonctionnement des appels asynchrones hérités du passé:

from iop import BusinessProcess
from msg import MyMessage


class MyBP(BusinessProcess):

    def on_message(self, request):
        msg_one = MyMessage(message="Message1")
        msg_two = MyMessage(message="Message2")

        self.send_request_async("Python.MyBO", msg_one,completion_key="1")
        self.send_request_async("Python.MyBO", msg_two,completion_key="2")

    def on_response(self, request, response, call_request, call_response, completion_key):
        if completion_key == "1":
            self.response_one = call_response
        elif completion_key == "2":
            self.response_two = call_response

    def on_complete(self, request, response):
        self.log_info(f"Received response one: {self.response_one.message}")
        self.log_info(f"Received response two: {self.response_two.message}")

En fait, ils fonctionnent de la même manière que les appels asynchrones dans IRIS. La méthode send_request_async envoie une requête à une opération métier (Business Operation) et la méthode on_response est appelée lorsque la réponse est reçue.

Vous pouvez distinguer les réponses par le paramètre completion_key.

Envoi de plusieurs requête de synchronisation

Il ne s'agit pas vraiment d'une nouvelle fonctionnalité, mais il convient de mentionner qu'il est possible d'envoyer plusieurs requêtes de synchronisation en parallèle:

from iop import BusinessProcess
from msg import MyMessage


class MyMultiBP(BusinessProcess):

    def on_message(self, request):
        msg_one = MyMessage(message="Message1")
        msg_two = MyMessage(message="Message2")

        tuple_responses = self.send_multi_request_sync([("Python.MyMultiBO", msg_one),
                                                        ("Python.MyMultiBO", msg_two)])

        self.log_info("All requests have been processed")
        for target,request,response,status in tuple_responses:
            self.log_info(f"Received response: {response.message}")

Ici, nous envoyons deux requêtes à la même opération métier (Business Operation) en parallèle.

La réponse est un tuple contenant la cible, la requête, la réponse et le statut de chaque appel.

C'est très utile lorsque vous devez envoyer plusieurs requêtes et que vous ne vous souciez pas de l'ordre des réponses.

Fonctions asynchrones et coroutines

Voyons maintenant comment utiliser les fonctions asynchrones et les coroutines dans l'IoP:

import asyncio

from iop import BusinessProcess
from msg import MyMessage


class MyAsyncNGBP(BusinessProcess):

    def on_message(self, request):

        results = asyncio.run(self.await_response(request))

        for result in results:
            print(f"Received response: {result.message}")

    async def await_response(self, request):
        msg_one = MyMessage(message="Message1")
        msg_two = MyMessage(message="Message2")

        # utilisation d'asyncio.gather pour envoyer plusieurs requêtes de manière asynchrone
        # utilisation de la méthode send_request_async_ng
        tasks = [self.send_request_async_ng("Python.MyAsyncNGBO", msg_one),
                 self.send_request_async_ng("Python.MyAsyncNGBO", msg_two)]

        return await asyncio.gather(*tasks)

Dans cet exemple, nous envoyons plusieurs demandes à la même opération métier (Business Operation) en parallèle à l'aide de la méthode send_request_async_ng.

Si vous avez lu attentivement ce post jusqu'à ce point, veuillez ajouter le commentaire "Boomerang". C'est peut-être peu de choses pour vous, mais pour moi c'est très important. Je vous remercie!

La méthode await_response est une coroutine qui envoie plusieurs requêtes et attend que toutes les réponses soient reçues. Grâce à la fonction asyncio.gather, nous pouvons attendre que toutes les réponses soient reçues en parallèle.

Les avantages de l'utilisation des fonctions asynchrones et des coroutines sont les suivants :

  • Meilleures performances: vous pouvez envoyer plusieurs requêtes en parallèle.
  • Simplicité de lecture et de maintenance: vous pouvez utiliser le mot-clé await pour attendre les réponses.
  • Plus de flexibilité: vous pouvez utiliser le module asyncio pour créer des flux de travail complexes.
  • Plus de contrôle: vous pouvez utiliser le module asyncio pour gérer les exceptions et les timeouts.

Conclusion

Quelle est la différence entre send_request_async, send_multi_request_sync et send_request_async_ng?

  • send_request_async: envoie une requête à une opération métier (Business Operation) et attend la réponse si la méthode on_response est implémentée et le paramètre completion_key est utilisé.
    • avantage: vous pouvez utiliser les appels asynchrones comme vous en avez l'habitude.
    • inconvénient: il peut être difficile à maintenir si vous avez besoin d'envoyer plusieurs requêtes en parallèle.
  • send_multi_request_sync : envoie plusieurs requêtes à la même opération métier (Business Operation) en parallèle et attend que toutes les réponses soient reçues.
    • avantage: facile à utiliser.
    • inconvénient: vous ne pouvez pas contrôler l'ordre des réponses (c'est-à-dire que la liste des réponses n'est pas ordonnée).
  • send_request_async_ng: envoie plusieurs requêtes à la même opération métier (Business Operation) en parallèle et attend que toutes les réponses soient reçues.
    • avantage: vous pouvez contrôler l'ordre des réponse.
    • inconvénient : vous devez utiliser des fonctions asynchrones et des coroutines.

Joyeux multitraitement!

0
0 46
Article Guillaume Rongier · Fév 7, 2025 9m read

L'essor des projets Big Data, des analyses en libre-service en temps réel, des services de recherche en ligne et des réseaux sociaux, entre autres, a donné naissance à des scénarios de requête de données massives et très performantes. En réponse à ce défi, la technologie MPP (base de données de traitement hautement parallèle) a été créée et s'est rapidement imposée. Parmi les options MPP open-source, Presto (https://prestodb.io/) est la plus connue. Cette solution a vu le jour au sein de Facebook et a été utilisée pour l'analyse de données, avant d'être mise à disposition en libre accès. Cependant, depuis que Teradata a rejoint la communauté Presto, elle offre désormais un support.

Presto se connecte aux sources de données transactionnelles (Oracle, DB2, MySQL, PostgreSQL, MongoDB et autres bases de données SQL et NoSQL) et fournit un traitement de données SQL distribué et en mémoire, combiné à des optimisations automatiques des plans d'exécution. Son objectif est avant tout d'exécuter des requêtes rapides, que vous traitiez des gigaoctets ou des téraoctets de données, en mettant à l'échelle et en parallélisant les charges de travail.

Presto n'avait pas à l'origine de connecteur natif pour la base de données IRIS, mais heureusement, ce problème a été résolu avec un projet communautaire d'InterSystems "presto-iris"(https://openexchange.intersystems.com/package/presto-iris). C'est pourquoi nous pouvons maintenant exposer une couche MPP devant les référentiels IRIS d'InterSystems pour permettre des requêtes, des rapports et des tableaux de bord de haute performance à partir de données transactionnelles dans IRIS.

Dans cet article, nous suivrons un guide étape par étape pour configurer Presto, le connecter à IRIS et établir une couche MPP pour vos clients. Nous démontrerons également les principales fonctionnalités de Presto, ses commandes et outils principaux, toujours avec IRIS en tant que base de données source.

Caractéristiques de Presto

Les caractéristiques de Presto comprennent les fonctionnalités suivantes:

  1. Architecture simple mais extensible.
  2. Connecteurs enfichables (Presto prend en charge les connecteurs enfichables pour fournir des métadonnées et des données pour les requêtes).
  3. Exécutions en pipeline (cela évite les surcharges de latence d'E/S (I/O) inutiles).
  4. Fonctions définies par l'utilisateur (les analystes peuvent créer des fonctions personnalisées définies par l'utilisateur pour faciliter la migration).
  5. Traitement en colonne vectorisé.


Avantages de Presto

Vous trouverez ci-dessous une liste des avantages offerts par Apache Presto:

Opérations SQL spécialisées;
Installation et débogage faciles;
Abstraction de stockage simple;
Évolutivité rapide des données en pétaoctets avec une faible latence.

Architecture de Presto


  • Clients: Ils sont les utilisateurs de PrestoDB. Les clients utilisent le protocole JDBC/ODBC/REST pour communiquer avec les coordinateurs.
  • Les coordinateurs: Ils sont responsables de la gestion des nœuds de travail associés, de l'analyse, du traitement des requêtes et de la génération des plans d'exécution. Ils sont également chargés de la livraison des données pour le traitement entre les opérateurs, ce qui crée des plans logiques composés d'étapes, où chaque étape est exécutée de manière distribuée à l'aide de tâches entre les opérateurs.
  • Opérateurs: Il s'agit de nœuds de calcul pour l'exécution de tâches et le traitement de données, permettant le traitement et la consommation de données à grande échelle./li>
  • Communication: Chaque travailleur Presto communique avec le coordinateur à l'aide d'un serveur de découverte pour se préparer au travail.
  • Connecteurs: Chaque type de source de données possible possède un connecteur utilisé par Presto pour consommer les données. Le projet https://openexchange.intersystems.com/package/presto-iris permet l'utilisation d'InterSystems IRIS par Presto.
  • Catalogue: Il contient des informations sur l'emplacement des données, y compris les schémas et la source de données. Lorsque les utilisateurs exécutent une instruction SQL dans Presto, ils l'exécutent contre un ou plusieurs catalogues

Cas d'utilisation de Presto

InterSystems IRIS et Presto offrent ensemble les possibilités d'utilisation suivantes:

  • Requêtes ad hoc: Vous pouvez exécuter des requêtes ad hoc très performantes sur des téraoctets de données.
  • Rapports et tableaux de bord: Il existe un moteur permettant de réaliser des requêtes de données très performantes pour les rapports, l'informatique décisionnelle en libre-service et les outils d'analyse, par exemple Apache Superset (découvrez l'exemple dans cet article).
  • Mode lac de données (Open lakehouse): Presto has the connectors and catalogs to unify required data sources and deliver scalable queries and data using SQL between workers.

InterSystems IRIS est un partenaire idéal pour Presto. Comme il s'agit d'un référentiel de données à haute performance qui supporte le traitement distribué à l'aide de shards et associé aux opérateurs Presto, n'importe quel volume de données peut être interrogé en seulement quelques millisecondes.

Installation et lancement de PrestoDB

Il y a plusieurs options (Docker et Java JAR) pour l'installation de Presto. Vous pouvez trouver plus de détails à ce sujet sur https://prestodb.io/docs/current/installation/deployment.html. Dans cet article, nous utiliserons Docker. Pour faciliter la connaissance et permettre un démarrage rapide, nous avons mis à disposition un exemple d'application sur Open Exchange (il a été dérivé d'un autre logiciel https://openexchange.intersystems.com/package/presto-iris). Suivez les étapes suivantes pour vous en faire une idée:

  1. Accédez à https://openexchange.intersystems.com/package/iris-presto-sample pour télécharger l'échantillon utilisé dans ce tutoriel.
  2. Lancer l'environnement de démonstration avec docker-compose:
    docker-compose up -d --build
    Note : Pour les besoins de la démo, il utilise Apache Superset avec superset-iris et les exemples qui l'accompagnent. Il faut donc un certain temps pour que la démonstration se télécharge.
  3. L'interface utilisateur Presto sera disponible via ce lien: http://localhost:8080/ui/#.
  4. Attendez 15 à 20 minutes (il y a beaucoup d'échantillons de données à télécharger). Lorsque SuperSet aura fini de télécharger les échantillons après 10 à 15 minutes, il devrait être disponible sur le lien http://localhost:8088/databaseview/list (saisissez admin/admin comme nom d'utilisateur/mot de passe sur la page de connexion).
  5. Accédez maintenant à la rubrique Tableaux de bord:
  6. Si nous visitons http://localhost:8080/ui, nous pouvons remarquer que Presto a exécuté des requêtes et affiche des statistiques:

Ci-dessus, vous pouvez voir l'interface web de Presto pour surveiller et gérer les requêtes. On peut y accéder à partir du numéro de port spécifié dans les propriétés de configuration du coordinateur (pour cet article, le numéro de port est 8080).

Détails sur l'exemple de code

Fichier Dockerfile

Le fichier Dockerfile est utilisé pour créer une image Docker PrestoDB avec le plugin presto-iris et le fichier JDBC InterSystems IRIS inclus:

# Image officielle de PrestoDB sur Docker HubFROM prestodb/presto

# À partir de https://github.com/caretdev/presto-iris/releases# Ajout du plugin presto-iris dans l'image DockerADDhttps://github.com/caretdev/presto-iris/releases/download/0.1/presto-iris-0.1-plugin.tar.gz /tmp/presto-iris/presto-iris-0.1-plugin.tar.gz # À partir de https://github.com/intersystems-community/iris-driver-distribution# Ajout du pilote IRIS JDBC dans l'image DockerADDhttps://raw.githubusercontent.com/intersystems-community/iris-driver-distribution/refs/heads/main/JDBC/JDK18/com/intersystems/intersystems-jdbc/3.8.4/intersystems-jdbc-3.8.4.jar /opt/presto-server/plugin/iris/intersystems-jdbc-3.8.4.jar RUN --mount=type=bind,src=.,dst=/tmp/presto-iris
tar -zxvf /tmp/presto-iris/presto-iris-0.1-plugin.tar.gz -C /opt/presto-server/plugin/iris/ --strip-components=1

Fichier Docker-compose.yml

Ce fichier crée 3 instances de conteneurs : une instance pour InterSystems IRIS (service IRIS), une instance pour PrestoDB (service Presto) et une instance pour Superset (service Superset). Le Superset est un outil de visualisation analytique utilisé pour afficher des données dans des tableaux de bord.

# à partir du projet https://github.com/caretdev/presto-irisservices:# création d'une instance de conteneur InterSystems IRIS  iris:    image:intersystemsdc/iris-community    ports:      -1972      -52773    environment:      IRIS_USERNAME:_SYSTEM      IRIS_PASSWORD:SYS# création d'une instance de conteneur PrestoDB qui utilise la base de données IRIS  presto:    build:.    volumes:# PrestoDB utilisera iris.properties pour obtenir des informations sur la connexion       -./iris.properties:/opt/presto-server/etc/catalog/iris.properties    ports:      -8080:8080# création d'une instance de conteneur Superset (Outil d'analyse du tableau de bord)  superset:    image:apache/superset:3.0.2    platform:linux/amd64    environment:      SUPERSET_SECRET_KEY:supersecret# création d'une connexion InterSystems IRIS pour le chargement des échantillons de données      SUPERSET_SQLALCHEMY_EXAMPLES_URI:iris://_SYSTEM:SYS@iris:1972/USER    volumes:      -./superset_entrypoint.sh:/superset_entrypoint.sh      -./superset_config.py:/app/pythonpath/superset_config.py    ports:      -8088:8088    entrypoint:/superset_entrypoint.sh

Fichier iris.properties

Ce fichier contient les informations nécessaires pour connecter PrestoDB à InterSystems IRIS DB et créer une couche MPP pour des requêtes performantes et évolutives à partir des tableaux de bord Superset.

# from the project https://github.com/caretdev/presto-iris
connector.name=iris
connection-url=jdbc:IRIS://iris:1972/USER
connection-user=_SYSTEM
connection-password=SYS

Fichier superset_entrypoint.sh

Ce script installe la bibliothèque superset-iris (pour le support d'IRIS par Superset), démarre l'instance Superset et charge des échantillons de données dans la base de données InterSystems IRIS. Au moment de l'exécution, les données consommées par Superset proviendront de PrestoDB, qui sera une couche MPP pour IRIS DB.

#!/bin/bash
# Installation de l'extension InterSystems IRIS Superset
pip install superset-iris

superset db upgrade

superset fab create-admin
--username admin
--firstname Superset
--lastname Admin
--email admin@superset.com
--password ${ADMIN_PASSWORD:-admin} superset init

# Téléchargement d'échantillons dans IRIS superset load-examples

# Modification de l'URI de la base de données d'échantillons en Presto superset set-database-uri -d examples -u presto://presto:8080/iris

/usr/bin/run-server.sh

Au sujet de Superset

Il s'agit d'une plateforme moderne d'exploration et de visualisation des données qui peut remplacer ou renforcer les outils propriétaires de veille stratégique pour de nombreuses équipes.Superset s'intègre parfaitement à une grande variété de sources de données.
Superset offre les avantages suivants:

  • Une interface sans code pour construire rapidement des graphiques
  • Un éditeur SQL puissant basé sur le web pour des requêtes avancées
  • Une couche sémantique légère pour définir rapidement des dimensions et des métriques personnalisées
  • Un support prêt à l'emploi pour presque toutes les bases de données SQL ou moteurs de données
  • Un large éventail de visualisations magnifiques pour présenter vos données, allant de simples diagrammes à barres à des visualisations géospatiales
  • Une couche de mise en cache légère et configurable pour alléger la charge de la base de données
  • Des options d'authentification et des rôles de sécurité très extensibles
  • Une API pour la personnalisation programmatique
  • Une architecture cloud-native conçue à partir de zéro pour s'adapter à l'échelle

Sources et supports d'apprentissage supplémentaires

  1. Tutoriel complet sur PrestoDB: https://www.tutorialspoint.com/apache_presto/apache_presto_quick_guide.htm
  2. Documentation sur PrestoDB: https://prestodb.io/docs/current/overview.html
  3. Plugin Presto-iris: https://openexchange.intersystems.com/package/presto-iris
  4. Échantillon Iris-presto: https://openexchange.intersystems.com/package/iris-presto-sample
  5. Au sujet de Superset: https://github.com/apache/superset
  6. Superset et InterSystems IRIS: https://openexchange.intersystems.com/package/superset-iris  
0
0 38
InterSystems officiel Adeline Icard · Oct 3, 2024

Nous avons récemment mis à disposition une nouvelle version d'InterSystems IRIS dans le cadre du programme d'accès anticipé à la recherche vectorielle, comprenant un nouvel index Approximate Nearest Neighbor basé sur l'algorithme d'indexation Hierarchical Navigable Small World (HNSW). Cet ajout permet des recherches de voisins les plus proches très efficaces et approximatives sur de grands ensembles de données vectorielles, améliorant considérablement les performances et l'évolutivité des requêtes.

0
0 43
Article Guillaume Rongier · Mai 22, 2024 6m read

Il me semble que c'était hier, lorsque nous avons réalisé un petit projet en Java pour tester les performances d'IRIS, PostgreSQL et MySQL (vous pouvez consulter l'article que nous avons écrit en juin à la fin de cet article). Si vous vous souvenez bien, IRIS était supérieur à PostgreSQL et dépassait considérablement MySQL en termes d'insertions, sans grande différence au niveau des requêtes.

Peu de temps après , @Dmitry Maslennikov m'a demandé "Pourquoi ne le testez-vous pas à partir d'un projet Python ?". Voici donc la version Python des tests que nous avons effectués précédemment en utilisant les connexions JDBC.

Tout d'abord, sachez que je ne suis pas un expert en Python, donc si vous remarquez des choses qui pourraient être améliorées, n'hésitez pas à me contacter.

Pour cet exemple, j'ai utilisé Jupyter Notebook, qui simplifie considérablement le développement en Python et nous permet de voir étape par étape ce que nous faisons. En annexe de cet article vous trouverez l'application qui vous permettra d'effectuer vos propres tests.

Avertissement pour les utilisateurs de Windows

Si vous clonez le projet GitHub dans Visual Studio Code, pour déployer correctement les conteneurs, il vous faudra peut-être changer la configuration de fin de ligne par défaut de CRLF à LF:

Si vous souhaitez reproduire les tests sur vos ordinateurs, vous devez tenir compte des éléments suivants : Docker Desktop demandera des autorisations d'accès aux dossiers de vos ordinateurs dont il a besoin pour déployer le projet. Si vous n'avez pas configuré l'autorisation d'accès à ces dossiers avant de lancer les conteneurs Docker, la création initiale de la table de test dans PostgreSQL échouera, donc avant de lancer le projet, vous devez configurer l'accès partagé aux dossiers du projet dans votre DockerDesktop.

Pour ce faire, vous devez accéder à Paramètres -> Ressources -> Partage de fichiers ("Settings -> Resources -> File sharing") et ajouter à la liste le dossier dans lequel vous avez cloné le projet:

Vous êtes prévenus!

 

Test de performance

Pour ces tests, nous utiliserons une table relativement simple contenant les informations les plus basiques possibles sur un patient. Ici vous pouvez voir la commande pour créer la table en SQL:

CREATETABLE Test.Patient (
    NameVARCHAR(225),
    Lastname VARCHAR(225),
    Photo VARCHAR(5000),
    Phone VARCHAR(14),
    Address VARCHAR(225)    
)

Comme vous pouvez le voir, nous avons défini la photo du patient comme un VARCHAR(5000), puisque nous allons inclure (théoriquement) les informations vectorisées de la photo. Il y a quelques mois, j'ai publié un article expliquant comment utiliser Embedded Python pour implémenter IRIS, un système de reconnaissance faciale (ici) où vous pouvez voir comment les images sont transformées en vecteurs pour une comparaison ultérieure. La question de la vectorisation vient du fait que ce format vectoriel est la norme dans de nombreux modèles d'apprentissage automatique et qu'il est toujours utile de tester avec quelque chose de similaire à la réalité (juste quelque chose)./p>

 

Paramètres de Jupyter Notebook

Pour simplifier au maximum le développement du projet en Python, j'ai utilisé un outil magnifique, le Jupyter Notebook, qui permet de développer pas à pas chacune des fonctionnalités dont nous aurons besoin.

Voici un aperçu de notre Jupyter:

Jetons un coup d'œil aux points les plus intéressants de celui-ci:

Importation de bibliothèques:

import iris
import names
import numpy as np
from datetime import datetime
import psycopg2
import mysql.connector
import matplotlib.pyplot as plt
import random_address
from phone_gen import PhoneNumber

Connexion aux bases de données:

IRIS:

connection_string = "iris:1972/TEST"
username = "superuser"
password = "SYS"
connectionIRIS = iris.connect(connection_string, username, password)
cursorIRIS = connectionIRIS.cursor()
print("Connected")

PostgreSQL:

connectionPostgres = psycopg2.connect(database="testuser",
                        host="postgres",
                        user="testuser",
                        password="testpassword",
                        port="5432")
cursorPostgres = connectionPostgres.cursor()
print("Connected")

MySQL:

connectionMySQL = mysql.connector.connect(
  host="mysql",
  user="testuser",
  password="testpassword"
)
cursorMySQL = connectionMySQL.cursor()
print("Connected")

Génération des valeurs à insérer

phone_number = PhoneNumber("USA")
resultsIRIS = []
resultsPostgres = []
resultsMySQL = []
parameters =  []
for x in range(1000):
    rng = np.random.default_rng()
    parameter = []
    parameter.append(names.get_first_name())
    parameter.append(names.get_last_name())
    parameter.append(str(rng.standard_normal(50)))
    parameter.append(phone_number.get_number())
    parameter.append(random_address.real_random_address_by_state('CA')['address1'])
    parameters.append(parameter)

print("Parameters built")

Insertion dans IRIS

date_before = datetime.now()

cursorIRIS.executemany("INSERT INTO Test.Patient (Name, Lastname, Photo, Phone, Address) VALUES (?, ?, ?, ?, ?)", parameters) connectionIRIS.commit() difference = datetime.now() - date_before print(difference.total_seconds()) resultsIRIS.append(difference.total_seconds())

Insertion dans PostgreSQL

date_before = datetime.now()

cursorPostgres.executemany("INSERT INTO test.patient (name, lastname, photo, phone, address) VALUES (%s,%s,%s,%s,%s)", parameters) connectionPostgres.commit() difference = datetime.now() - date_before print(difference.total_seconds()) resultsPostgres.append(difference.total_seconds())

Insertion dans MySQL

date_before = datetime.now()

cursorMySQL.executemany("INSERT INTO test.patient (name, lastname, photo, phone, address) VALUES (%s,%s,%s,%s,%s)", parameters) connectionMySQL.commit() difference = datetime.now() - date_before print(difference.total_seconds()) resultsMySQL.append(difference.total_seconds())

Pour notre test, j'ai décidé d'insérer les valeurs suivantes dans chaque base de données:

  • 1 insertion de 1000 patients.
  • 1 insertion de 5000 patients.
  • 1 insertion de 20 000 patients.
  • 1 insertion de 50 000 patients.

Gardez à l'esprit, lors de l'exécution des tests, que le processus le plus long est la création des valeurs à insérer par Python. Pour se rapprocher de la réalité, j'ai lancé plusieurs tests à l'avance afin que les bases de données disposent déjà d'un ensemble significatif d'enregistrements (environ 200 000 enregistrements)./p>

Résultats des tests

Insertion de 1000 patients:

  • InterSystems IRIS: 0,037949 seconde.
  • PostgreSQL: 0,106508 seconde.
  • MySQL: 0,053338 seconde.

Insertion de 5,000 patients:

  • InterSystems IRIS: 0,162791 seconde.
  • PostgreSQL: 0,432642 seconde.
  • MySQL: 0,18925 seconde.

Insertion de 20,000 patients:

  • InterSystems IRIS: 0,601944 seconde.
  • PostgreSQL: 1,803113 seconde.
  • MySQL: 0,594396 seconde.

Insertion de 50,000 patients:

  • InterSystems IRIS: 1.482824 seconde.
  • PostgreSQL: 4,581251 secondes.
  • MySQL: 2,162996 secondes.

Bien qu'il s'agisse d'un test assez simple, il est très significatif car il nous permet de voir la tendance de chaque base de données en ce qui concerne les performances d'insertion.

Conclusions

Si nous comparons les performances des tests effectués avec le projet Java et le projet actuel en Python, nous constatons qu'à cette occasion, PostgreSQL est clairement inférieur au projet Python, étant 4 fois plus lent qu'InterSystems IRIS, tandis que MySQL s'est amélioré par rapport à la version Java.

InterSystems IRIS reste incontestablement le meilleur des trois, présentant un comportement plus linéaire et une meilleure performance d'insertion, quelle que soit la technologie utilisée.

Caractéristiques techniques de l'ordinateur portable utilisé pour les tests:

  • Système d'exploitation: Microsoft Windows 11 Pro.
  • Processeur: 13th Gen Intel(R) Core(TM) i9-13900H, 2600 Mhz.
  • Mémoire RAM: 64 Go.
0
0 79
Article Pierre LaFay · Jan 10, 2024 5m read

[Introduction]

La famille InterSystems IRIS a un utilitaire intéressant : ^SystemPerformance (aussi connu comme ^pButtons pour Caché et Ensemble) qui génère les informations sur les performances de la base de données dans un fichier HTML lisible. Lorsque vous exécutez ^SystemPerformance sur IRIS pour Windows, un fichier HTML est créé dans lequel notre propre journal de performances mgstat et notre journal de performances Windows sont inclus.

1
0 74
Article Pierre LaFay · Jan 7, 2024 3m read

Le sous-système Windows pour Linux (WSL) est une fonctionnalité de Windows qui vous permet d'exécuter un environnement Linux sur votre ordinateur Windows, sans avoir besoin d'une machine virtuelle distincte ni d'un double démarrage.

WSL est conçu pour offrir une expérience transparente et productive aux développeurs qui souhaitent utiliser à la fois Windows et Linux**.

0
0 104
Article Pierre LaFay · Déc 30, 2023 6m read

Cela semble être hier lorsque nous avons réalisé un petit projet en Java pour tester les performances d'IRIS, PostgreSQL et MySQL (vous pouvez consulter l'article que nous avons écrit en juin à la fin de cet article). Si vous vous en souvenez, IRIS était supérieur à PostgreSQL et clairement supérieur à MySQL en termes d'insertions, sans grande différence en termes de requêtes.

0
0 125
Article Lorenzo Scalese · Août 14, 2023 2m read

InterSystems IRIS propose plusieurs façons de profiler votre code. Dans la plupart des cas, il produit suffisamment d'informations pour trouver les endroits où l'on passe le plus de temps ou les ensembles les plus globaux. Mais il est parfois difficile de comprendre le flux d'exécution et comment il s'est terminé à ce point-là. Pour résoudre ce problème, j'ai décidé d'implémenter un moyen de construire un rapport d'une certaine manière, de sorte qu'il est possible de naviguer par pile vers le bas.

 

0
0 45
Article Sylvain Guilbaud · Juin 12, 2023 12m read

Il y a souvent des questions concernant la configuration idéale du Serveur Web HTTPD Apache pour HealthShare.  Le contenu de cet article décrit la configuration initiale recommandée du serveur Web pour tout produit HealthShare. 

APour commencer, la version 2.4.x (64 bits) d'Apache HTTPD est recommandée.  Des versions antérieures comme 2.2.x sont disponibles, mais la version 2.2 n'est pas recommandée pour les performances et l'évolutivité de HealthShare.

Configuration d'Apache

Module API Apache sans NSD {#ApacheWebServer-ApacheAPIModulewithoutNSD}

HealthShare requiert l'option d'installation Apache API Module without NSD. La version des modules liés dynamiquement dépend de la version d'Apache :

  • CSPa24.so (Apache Version 2.4.x)

La configuration de Caché Server Pages dans le fichier Apache httpd.conf doit être effectuée par l'installation de HealthShare qui est détaillée plus loin dans ce document. Cependant, la configuration peut être effectuée manuellement. Pour plus d'informations, veuillez consulter le guide de configuration d'Apache dans la documentation d'InterSystems : Recommended Option: Apache API Module without NSD (CSPa24.so)

Recommandations concernant le module multiprocesseur d'Apache (MPM) {#ApacheWebServer-ApacheMulti-ProcessingModule(MPM)Recommendations}

Apache Prefork MPM Vs. Worker MPM {#ApacheWebServer-ApachePreforkMPMVs.WorkerMPM}

Le serveur web HTTPD Apache est livré avec trois modules multiprocesseurs (MPM) : Prefork, Worker et Event.  Les MPM sont responsables de la liaison avec les ports réseau de la machine, de l'acceptation des requêtes et de l'envoi de fils pour traiter ces requêtes. Par défaut, Apache est généralement configuré avec le MPM Prefork, qui n'est pas adapté à des charges de travail importantes en termes de transactions ou d'utilisateurs simultanés.

Pour les systèmes de production HealthShare, le MPM Apache Worker doit être activé pour des raisons de performance et d'évolutivité. Worker MPM est préféré pour les raisons suivantes :

  • Prefork MPM utilise plusieurs processus enfants avec un fil d'exécution chacun et chaque processus gère une connexion à la fois. Lorsque Prefork est utilisé, les demandes simultanées souffrent car, comme chaque processus ne peut traiter qu'une seule demande à la fois, les demandes sont mises en attente jusqu'à ce qu'un processus serveur se libère. De plus, afin d'évoluer, il faut plus de processus enfants Prefork, ce qui consomme des quantités importantes de mémoire.
  • Worker MPM utilise plusieurs processus enfants avec de nombreux fils d'exécution chacun. Chaque fil d'exécution gère une connexion à la fois, ce qui favorise la concurrence et réduit les besoins en mémoire. Worker gère mieux la concurrence que Prefork, car il y aura généralement des fils d'exécution libres disponibles pour répondre aux demandes au lieu des processus Prefork à un seul fil d'exécution qui peuvent être occupés.

Paramètres MPM pour Apache Worker {#ApacheWebServer-ApacheWorkerMPMParameters}

En utilisant des fils d'exécution pour servir les demandes, Worker est capable de servir un grand nombre de demandes avec moins de ressources système que le serveur basé sur le processus Prefork. 
Les directives les plus importantes utilisées pour contrôler le MPM de Worker sont ThreadsPerChild qui contrôle le nombre de fils d'exécution déployés par chaque processus enfant et MaxRequestWorkers qui contrôle le nombre total maximum de fils d'exécution pouvant être lancés.
Les valeurs recommandées de la directive commune Worker MPM sont détaillées dans le tableau ci-dessous :

<caption>Paramètres recommandés pour le serveur web HTTPD Apache</caption>
<th>
  Valeur recommandée
</th>

<th>
  Commentaires
</th>
<td>
  Nombre maximal d'utilisateurs simultanés de HealthShare Clinical Viewer, ou des quatre autres composants de HealthShare, fixé à la somme de toutes les tailles de pool de services commerciaux entrants pour toutes les productions d'interfaces définies. * Note : Si toutes les inconnues au moment de la configuration commencent par une valeur de '1000'
</td>

<td>
    MaxRequestWorkers fixe la limite du nombre de demandes simultanées qui seront servies, c'est-à-dire qu'il restreint le nombre total de fils d'exécution qui seront disponibles pour servir les clients. Il est important que MaxRequestWorkers soit défini correctement car s'il est trop faible, les ressources seront gaspillées et s'il est trop élevé, les performances du serveur seront affectées. Notez que lorsque le nombre de connexions tentées est supérieur au nombre de travailleurs, les connexions sont placées dans une file d'attente. La file d'attente par défaut peut être ajustée avec la directive ListenBackLog.  
</td>
<td>
  250
</td>

<td>
    MaxSpareThreads traite les threads inactifs à l'échelle du serveur. S'il y a trop de threads inactifs dans le serveur, les processus enfants sont tués jusqu'à ce que le nombre de threads inactifs soit inférieur à ce nombre. L'augmentation du nombre de threads inutilisés par rapport à la valeur par défaut permet de réduire les risques de réactivation des processus.  
</td>
<td>
  75
</td>

<td>
    MinSpareThreads traite les fils d'exécution inactifs à l'échelle du serveur. S'il n'y a pas assez de fils d'exécution libres dans le serveur, des processus enfants sont créés jusqu'à ce que le nombre de fils d'exécution libres soit supérieur à ce nombre. En réduisant le nombre de fils d'exécution inutilisés par rapport à la valeur par défaut, on réduit les risques de réactivation des processus.  
</td>
<td>
  MaxRequestWorkers divisé par ThreadsPerChild
</td>

<td>
    Valeur maximale de MaxRequestWorkers pour la durée de vie du serveur. ServerLimit est une limite stricte du nombre de processus enfants actifs, et doit être supérieure ou égale à la directive MaxRequestWorkers divisée par la directive ThreadsPerChild. Avec worker, n'utilisez cette directive que si vos paramètres MaxRequestWorkers et ThreadsPerChild nécessitent plus de 16 processus serveur (valeur par défaut).  
</td>
<td>
  20
</td>

<td>
    La directive StartServers définit le nombre de processus de serveur enfant créés au démarrage. Comme le nombre de processus est contrôlé dynamiquement en fonction de la charge, il y a généralement peu de raisons d'ajuster ce paramètre, sauf pour s'assurer que le serveur est prêt à gérer un grand nombre de connexions dès son démarrage.  
</td>
<td>
  25
</td>

<td>
    Cette directive définit le nombre de threads créés par chaque processus enfant, 25 par défaut. Il est recommandé de conserver la valeur par défaut car l'augmenter pourrait conduire à une dépendance excessive vis-à-vis d'un seul processus.  
</td>
Les Directives MPM pour Apache Worker
MaxRequestWorkers 
MaxSpareThreads
MinSpareThreads
ServerLimit
StartServers
ThreadsPerChild

Pour plus d'informations, veuillez consulter la documentation relative à la version d'Apache concernée :

Exemple de configuration MPM du travailleur Apache 2.4 {#ApacheWebServer-ExampleApache2.4WorkerMPMConfiguration}

Cette section explique comment configurer Worker MPM pour un serveur Web RHEL7 Apache 2.4 nécessaire pour prendre en charge jusqu'à 500 utilisateurs simultanés de TrakCare.

  1. Vérifiez d'abord le MPM en utilisant la commande suivante :
  2. Modifiez le fichier de configuration /etc/httpd/conf.modules.d/00-mpm.conf selon les besoins, en ajoutant et en supprimant le caractère de commentaire # afin que seuls les modules Worker MPM soient chargés. Modifiez la section Worker MPM avec les valeurs suivantes dans le même ordre que ci-dessous :
  3. Redémarrer Apache
  4. Après avoir redémarré Apache avec succès, validez les processus worker en exécutant les commandes suivantes. Vous devriez voir quelque chose de similaire à ce qui suit confirmant le processus httpd.worker :

Renforcement d'Apache {#ApacheWebServer-ApacheHardening}

Modules requis pour Apache {#ApacheWebServer-ApacheRequiredModules}

L'installation du paquetage officiel d'Apache activera par défaut un ensemble spécifique de modules Apache. Cette configuration par défaut d'Apache chargera ces modules dans chaque processus httpd. Il est recommandé de désactiver tous les modules qui ne sont pas nécessaires à HealthShare pour les raisons suivantes :

  • réduire l'empreinte du processus du démon httpd.
  • réduire le risque d'un défaut de segmentation dû à un module malveillant.
  • réduire les vulnérabilités en matière de sécurité.

Le tableau ci-dessous détaille les modules Apache recommandés pour HealthShare. Tout module qui ne figure pas dans la liste ci-dessous peut être désactivé :

Nom du moduleDescription
aliasMappage des différentes parties du système de fichiers de l'hôte dans l'arborescence du document et pour la redirection des URL.
authz_hostFournit un contrôle d'accès basé sur le nom d'hôte du client, l'adresse IP.
dirPermet de rediriger les barres obliques et de servir les fichiers d'index des répertoires.
headersPour contrôler et modifier les en-têtes de demande et de réponse HTTP
log_configJournalisation des requêtes adressées au serveur.
mimeAssocie les extensions du nom de fichier demandé avec le comportement et le contenu du fichier
negotiationPermet de sélectionner le contenu du document qui correspond le mieux aux capacités du client.
setenvifPermet de définir des variables d'environnement en fonction des caractéristiques de la demande
statusAffiche l'état du serveur et les statistiques de performance

Désactivation des modules

Les modules inutiles doivent être désactivés pour renforcer la configuration qui réduira les vulnérabilités de sécurité. Le client est responsable de la politique de sécurité du serveur web. Au minimum, les modules suivants doivent être désactivés.

Nom du moduleDescription
asisEnvoie des fichiers qui contiennent leurs propres titres HTTP
autoindexGénère des indices de répertoire et affiche la liste des répertoires lorsqu'aucun fichier index.html n'est présent
envModifie la variable d'environnement transmise aux scripts CGI et aux pages SSI
cgicgi - Exécution de scripts CGI
actionsExécution de scripts CGI en fonction du type de média ou de la méthode de demande, déclenchement d'actions sur les demandes
includeDocuments HTML analysés par le serveur (Server Side includes)
filterFiltrage intelligent des demandes
versionGestion des informations de version dans les fichiers de configuration à l'aide de IfVersion
userdirMappage des requêtes vers des répertoires spécifiques à l'utilisateur. Par exemple, ~nom d'utilisateur dans l'URL sera traduit en un répertoire dans le serveur

Apache SSL/TLS {#ApacheWebServer-ApacheSSL/TLS}

Pour protéger les données en transit, assurer la confidentialité et l'authentification, InterSystems recommande que toutes les communications TCP/IP entre les serveurs et les clients de HealthShare soient cryptées avec SSL/TLS, et InterSystems recommande également d'utiliser HTTPS pour toutes les communications entre le navigateur client des utilisateurs et la couche serveur web de l'architecture proposée.   Veillez à consulter les politiques de sécurité de votre organisation pour vous assurer de la conformité à toute exigence de sécurité spécifique de votre organisation. 

Le client est responsable de la fourniture et de la gestion des certificats SSL/TLS. Si vous utilisez des certificats SSL, ajoutez le module ssl_ (mod_ssl.so).

Paramètres supplémentaires de durcissement d'Apache {#ApacheWebServer-AdditionalHardeningApacheParameters}

Pour renforcer la configuration d'Apache, apportez les modifications suivantes au fichier httpd.conf :

  • TraceEnable doit être désactivé pour éviter les problèmes potentiels de traçage intersites.

  • ServerSignature doit être désactivé afin que la version du serveur web ne soit pas affichée.

Paramètres de configuration supplémentaires d'Apache {#ApacheWebServer-SupplementalApacheConfigurationParameters}

Keep-Alive {#ApacheWebServer-Keep-Alive}

Le paramètre Apache Keep-Alive permet d'utiliser la même connexion TCP pour la communication HTTP au lieu d'ouvrir une nouvelle connexion pour chaque nouvelle demande, c'est-à-dire que Keep-Alive maintient une connexion persistante entre le client et le serveur. Lorsque l'option Keep-Alive est activée, l'amélioration des performances provient de la réduction de l'encombrement du réseau, de la réduction de la latence des requêtes ultérieures et de la diminution de l'utilisation du CPU causée par l'ouverture simultanée de connexions. L'option Keep-Alive est activée par défaut et la norme HTTP v1.1 stipule qu'elle doit être présumée activée.

Cependant, l'activation de la fonction Keep-Alive présente des inconvénients : Internet Explorer doit être IE10 ou supérieur pour éviter les problèmes de délai d'attente connus avec les anciennes versions d'IE. De même, les intermédiaires tels que les pare-feu, les équilibreurs de charge et les proxies peuvent interférer avec les "connexions TCP persistantes" et entraîner une fermeture inattendue des connexions.  

Lorsque vous activez la fonction "Keep-Alive", vous devez également définir le délai d'attente de cette fonction. Le délai d'attente par défaut pour Apache est trop faible et doit être augmenté pour la plupart des configurations, car des problèmes peuvent survenir en cas de rupture des requêtes AJAX (c'est-à-dire hyperevent). Ces problèmes peuvent être évités en s'assurant que le délai d'attente du serveur est supérieur à celui du client.  En d'autres termes, c'est le client, et non le serveur, qui devrait fermer la connexion.  Des problèmes surviennent - principalement dans IE mais dans une moindre mesure dans d'autres navigateurs - lorsque le navigateur tente d'utiliser une connexion (en particulier pour un POST) dont il s'attend à ce qu'elle soit ouverte. 

Voir ci-dessous les valeurs recommandées de KeepAlive et KeepAliveTimeout pour un serveur Web HealthShare.

Pour activer KeepAlive dans Apache, apportez les modifications suivantes au fichier httpd.conf :

Psserelle CSP {#ApacheWebServer-CSPGateway}

Pour le paramètre CSP Gateway KeepAlive, laissez la valeur par défaut No Action car le statut KeepAlive est déterminé par les titres de la réponse HTTP pour chaque requête.

0
0 465
Article Lorenzo Scalese · Juin 1, 2022 9m read

Un système de stockage global d'aspect plus industriel

Dans le premier article de cette série, nous avons étudié le modèle entité-attribut-valeur (EAV) dans les bases de données relationnelles, et nous avons examiné les avantages et les inconvénients du stockage de ces entités, attributs et valeurs dans des tables. Nous avons appris que, malgré les avantages de cette approche en termes de flexibilité, elle présente de réels inconvénients, notamment une inadéquation fondamentale entre la structure logique des données et leur stockage physique, qui entraîne diverses difficultés.

Pour résoudre ces problèmes, nous avons décidé de voir si l'utilisation de globales - qui sont optimisées pour le stockage d'informations hiérarchiques - serait efficace pour les tâches que l'approche EAV traite habituellement.

Dans la Partie 1, nous avons créé un catalogue pour une boutique en ligne, d'abord en utilisant des tables, puis en utilisant une seule globale. Maintenant, essayons d'implémenter la même structure pour quelques globales.

Dans la première globale, ^catalog, nous allons stocker la structure du répertoire. Dans la deuxième globale, ^good, nous allons stocker les marchandises. Et dans la globale ^index, nous allons stocker les index. Puisque nos propriétés sont liées à un catalogue hiérarchique, nous ne créerons pas de globale séparée pour elles.

Avec cette approche, pour chaque entité (à l'exception des propriétés), nous avons une globale séparée, ce qui est bon du point de vue de la logique. Voici la structure du catalogue global :

 
Set ^сatalog(root_id, "Properties", "capacity", "name") = "Capacity, GB"
Set ^сatalog(root_id, "Properties", "capacity", "sort") = 1

Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "name") = "Endurance, TBW"
Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "sort") = 2

Set ^сatalog(root_id, sub1_id, "goods", id_good1) = 1
Set ^сatalog(root_id, sub1_id, "goods", id_good2) = 1

Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "name") = "Rotate speed, ms"
Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "sort") = 3

Set ^сatalog(root_id, sub2_id, "goods", id_good3) = 1
Set ^сatalog(root_id, sub2_id, "goods", id_good4) = 1

 

Une globale avec des marchandises ressemblera à quelque chose comme ceci :

Set ^good(id_good, property1) = value1
Set ^good(id_good, property2) = value2
Set ^good(id_good, property3) = value3
Set ^good(id_good, "catalog") = catalog_id

 

Bien sûr, nous avons besoin d'index afin que pour toute section du catalogue contenant des marchandises, nous puissions trier par les propriétés dont nous avons besoin. Une globale d'index aura une structure semblable à quelque chose comme ceci :

Configurer ^index(id_catalog, property1, id_good) = 1
; Pour obtenir rapidement le chemin complet du sous-catalogue concret
Configurer ^index("path", id_catalog) = "^catalog(root_id, sub1_id)"

 

Ainsi, dans n'importe quelle section du catalogue, on peut obtenir une liste triée. Une globale d'index est facultative. Il n'est utile que si le nombre de produits dans cette section du catalogue est important.

Code ObjectScript pour travailler avec des données de démonstration Demo Data

Maintenant, nous allons utiliser ObjectScript pour travailler avec nos données. Pour commencer, nous allons obtenir les propriétés d'une marchandise spécifique. Nous avons l'ID d'une marchandise particulière et nous devons afficher ses propriétés dans l'ordre donné par la valeur de tri. Voici le code pour cela :

get_sorted_properties(path, boolTable)
{
  ; mémoriser toutes les propriétés dans la globale temporaire
  While $QLENGTH(@path) > 0 {
    if ($DATA(@path("Properties"))) {
      set ln=""
      for {
	    Set ln = $order(@path("Properties", ln))
	    Quit: ln = ""

        IF boolTable & @path("Properties", ln, "table_view") = 1 {
  	      Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
	    }
	  ELSE {
  	    Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
	  }
    }
  }
}

print_sorted_properties_of_good(id_good)
{
  Set id_catalog = ^good(id_good, "catalog")
  Set path = ^index("path", id_catalog)

  Do get_sorted_properties(path, 0)

  set ln =""
  for {
   Set ln = $order(^tmp(ln))
   Quit: ln = ""
   Set fn = ""
   for {
 	Set fn = $order(^tmp(ln, fn))
 	Quit: fn = ""
 	Write ^tmp(ln, fn), " ", ^good(id_good, fn),!
   }
  }
}

 

Ensuite, nous voulons récupérer les produits de la section catalogue sous la forme de la table, basé sur id_catalog :

print_goods_table_of_catalog(id_catalog)
{
  Set path = ^index("path", id_catalog)
  Do get_sorted_properties(path, 1)

  set id=""
  for {
    Set id = $order(@path("goods"), id)
    Quit: id = ""

    Write id," ", ^good(id, "price"), " "

    set ln =""
    for {
      Set ln = $order(^tmp(ln))
      Quit: ln = ""
      Set fn = ""
      for {
 	    Set fn = $order(^tmp(ln, fn))
 	    Quit: fn = ""
 	    Write ^tmp(ln, fn), " ", ^good(id, fn)
      }
      Write !
    }
  }
}

 

Lisibilité : EAV SQL contre les globales

Comparons maintenant l'utilisation d'EAV et de SQL par rapport à l'utilisation de globales. En ce qui concerne la clarté du code, il est évident qu'il s'agit d'un paramètre subjectif. Mais regardons, par exemple, la création d'un nouveau produit.

Nous allons commencer par l'approche EAV, en utilisant SQL. Tout d'abord, nous devons obtenir une liste des propriétés de l'objet. Il s'agit d'une tâche distincte qui prend beaucoup de temps. Supposons que nous connaissions déjà les IDs de ces trois propriétés : capacité, poids, et endurance.

START TRANSACTION
INSERT INTO good (name, price, item_count, catalog_id) VALUES ('F320 3.2TB AIC SSD', 700, 10, 15);

SET @last_id = LAST_INSERT_ID ();

INSERT INTO NumberValues ​​Values​​(@last_id, @id_capacity, 3200);
INSERT INTO NumberValues ​​Values​​(@last_id, @id_weight, 0.4);
INSERT INTO NumberValues ​​Values​​(@last_id, @id_endurance, 29000);
COMMIT

 

Dans cet exemple, nous n'avons que trois propriétés, et l'exemple ne semble donc pas si inquiétant. Dans le cas général, nous aurions toujours quelques insertions dans la table de texte à l'intérieur de la transaction :

INSERT INTO TextValues ​​Values​​(@last_id, @ id_text_prop1, 'Text value of property 1');
INSERT INTO TextValues ​​Values​​(@last_id, @ id_text_prop2, 'Text value of property 2');
...
INSERT INTO TextValues Values (@last_id, @id_text_propN, 'Text value of property N');

 

Bien sûr, nous pourrions simplifier un peu la version SQL si nous utilisions la notation textuelle à la place des propriétés ID, par exemple "capacité" au lieu d'un nombre. Mais dans le monde SQL, ce n'est pas acceptable. Il est plutôt d'usage d'utiliser un ID numérique pour énumérer les instances d'entités. Cela permet d'obtenir des index plus rapides (il faut indexer moins d'octets), il est plus facile de suivre l'unicité et il est plus facile de créer automatiquement un nouvel ID. Dans ce cas, le fragment d'insertion aurait l'apparence suivante :

INSERT INTO NumberValues ​​Values​​(@last_id, 'capacity', 3200);
INSERT INTO NumberValues ​​Values​​(@last_id, 'weight', 0.4);
INSERT INTO NumberValues ​​Values​​(@last_id, 'endurance', 29000);

 

Voici le même exemple en utilisant des globales :

TSTART
Set ^good(id, "name") = "F320 3.2TB AIC SSD"
Set ^("price") = 700, ^("item_count") = 10, ^("reserved_count") = 0, ^("catalog") = id_catalog
Set ^("capacity") = 3200, ^("weight") = 0.4, ^("endurance") = 29000
TCOMMIT

 

Supprimons maintenant une marchandise en utilisant l'approche EAV :

START TRANSACTION
DELETE FROM good WHERE id = @ good_id;
DELETE FROM NumberValues ​​WHERE good_id = @ good_id;
DELETE FROM TextValues ​​WHERE good_id = @ good_id;
COMMIT

 

Et ensuite, faisons la même chose avec les globales :

Kill ^good(id_good)

Nous pouvons également comparer les deux approches en termes de longueur de code. Comme vous pouvez le constater dans les exemples précédents, lorsque vous utilisez des globales, le code est plus court. C'est une bonne chose. Plus le code est court, moins il y a d'erreurs et plus il est facile à comprendre et à gérer.

En général, un code plus court est aussi plus rapide. Et, dans ce cas, c'est certainement vrai, puisque les globales constituent une structure de données de niveau inférieur aux tables relationnelles.

Mise à l'échelle des données avec EAV et Globales

Ensuite, examinons la mise à l'échelle horizontale. Avec l'approche EAV, nous devons au moins distribuer les trois plus grandes tables sur les serveurs : Good, NumberValues, et TextValues. Les tables contenant des entités et des attributs peuvent simplement être entièrement copiés sur tous les serveurs, car ils contiennent peu d'informations.

Dans chaque serveur, avec une mise à l'échelle horizontale, des produits différents seraient stockés dans les tables Good, NumberValues et TextValues. Nous devrions allouer certains blocs d'identification pour les produits sur chaque serveur afin d'éviter la duplication des identifiants pour des produits différents.

Pour une mise à l'échelle horizontale avec des globales, il faudrait configurer des plages d'ID dans la globale et attribuer une plage de globale à chaque serveur.

La complexité est à peu près la même pour EAV et pour les globales, sauf que pour l'approche EAV, nous devrions configurer des plages d'ID pour trois tables. Avec les globales, nous configurons les ID pour une seule globale. C'est-à-dire qu'il est plus facile d'organiser la mise à l'échelle horizontale pour les globales.

Perte de données avec EAV et avec Globales

Enfin, considérons le risque de perte de données dû à des fichiers de base de données corrompus. Où est-il plus facile de sauvegarder toutes les données : dans cinq tables ou dans trois globales ( y compris une globale d'index ) ?

Je pense que c'est plus facile dans trois globales. Avec l'approche EAV, les données des marchandises différentes sont mélangées dans des tables, alors que pour les globales, les informations sont stockées de manière plus holistique. Les branches sous-jacentes sont stockées et triées séquentiellement. Par conséquent, la corruption d'une partie de la globale est moins susceptible d'entraîner des dommages que la corruption de l'une des tables dans l'approche EAV, où les données sont stockées comme des pâtes entremêlées.

Un autre casse-tête dans la récupération des données est l'affichage des informations. Avec l'approche EAV, les informations sont réparties entre plusieures tables et des scripts spéciaux sont nécessaires pour les assembler en un seul ensemble. Dans le cas des globales, vous pouvez simplement utiliser la commande ZWRITE pour afficher toutes les valeurs et les branches sous-jacentes du nœud.

Les Globales d'InterSystems IRIS : Une meilleure approche ?

L'approche EAV est apparue comme une astuce pour stocker des données hiérarchiques. Les tables n'ont pas été conçus à l'origine pour stocker des données imbriquées. L'approche EAV de facto est l'émulation des globales dans les tables. Étant donné que les tables représentent une structure de stockage de données de plus haut niveau et plus lente que les globales, l'approche EAV échoue par rapport aux globales.

À mon avis, pour les structures de données hiérarchiques, les globales sont plus pratiques et plus compréhensibles en termes de programmation, tout en étant plus rapides.

Si vous avez prévu une approche EAV pour votre projet, je vous suggère d'envisager d'utiliser les globales d'InterSystems IRIS pour stocker les données hiérarchiques.

0
0 407
Article Lorenzo Scalese · Mai 30, 2022 9m read

Introduction

Dans le premier article de cette série, nous examinerons le modèle entité-attribut-valeur (EAV) dans les bases de données relationnelles pour voir comment il est utilisé et à quoi il sert. Ensuite, nous comparerons les concepts du modèle EAV aux globales.

Parfois, on dispose d'objets comportant un nombre inconnu de champs, ou peut-être des champs hiérarchiquement imbriqués, pour lesquels, en règle générale, il faut effectuer une recherche.

Par exemple, voici une boutique en ligne avec divers groupes de produits. Chaque groupe de produits a son propre ensemble de propriétés uniques et a également des propriétés communes. Par exemple, les disques SSD et les disques durs ont la propriété commune "capacité", mais tous deux ont également des propriétés uniques, "Endurance, TBW" pour les SSD et "temps moyen de positionnement de la tête" pour les disques durs.

Dans certaines situations, le même produit, fabriqué par différents fabricants, possède des propriétés uniques.

Ainsi, imaginons que nous ayons une boutique en ligne qui vend 50 groupes de marchandises différents. Chaque groupe de produits a ses cinq propriétés uniques, qui peuvent être numériques ou textuelles.

Si nous créons une table dans lequel chaque produit possède 250 propriétés, alors que seules cinq d'entre elles sont réellement utilisées, non seulement nous augmentons considérablement (50 fois !) les exigences en matière d'espace disque, mais nous réduisons aussi considérablement les caractéristiques de vitesse de la base de données, puisque le cache sera encombré de propriétés inutiles et vides.

Mais ce n'est pas tout. Chaque fois que nous ajoutons une nouvelle famille de produits avec ses propriétés propres, nous devons modifier la structure du tableau à l'aide de la commande ALTER TABLE. Sur les tables de grande taille, cette opération peut prendre des heures ou des jours, ce qui est inacceptable pour les entreprises.

"Oui", remarquera le lecteur attentif, "mais nous pouvons utiliser une table différente pour chaque groupe de produits." Bien sûr, vous avez raison, mais cette approche nous donne une base de données avec des dizaines de milliers de tables pour un grand magasin, ce qui est difficile à administrer. De plus, le code, qui doit être pris en charge, devient de plus en plus complexe.

D'autre part, il n'est pas nécessaire de modifier la structure de la base de données lors de l'ajout d'un nouveau groupe de produits. Il suffit d'ajouter une nouvelle table pour un nouveau groupe de produits.

Dans tous les cas, les utilisateurs doivent être capables de rechercher facilement les produits dans un magasin, d'obtenir une table pratique des marchandises indiquant leurs propriétés actuelles et de comparer les produits.

Comme vous pouvez l'imaginer, un formulaire de recherche comportant 250 champs serait extrêmement gênant pour l'utilisateur, tout comme le fait de voir 250 colonnes de propriétés diverses dans la table des produits alors que seulement cinq propriétés pour le groupe sont nécessaires. Il en va de même pour les comparaisons de produits.

Une base de données marketing pourrait également servir comme un autre exemple utile. Pour chaque personne stockée dans la base, de nombreuses propriétés (souvent imbriquées) doivent être ajoutées, modifiées ou supprimées en permanence. Dans le passé, une personne peut avoir acheté quelque chose pour un certain coût, ou avoir acheté certains groupes de produits, avoir participé à un événement, avoir travaillé quelque part, avoir de la famille, vivre dans une certaine ville, appartenir à une certaine classe sociale, et ainsi de suite. Il pourrait y avoir des milliers de champs possibles, en constante évolution. Les spécialistes du marketing réfléchissent sans cesse à la manière de distinguer différents groupes de clients et de leur proposer des offres spéciales convaincantes.

Pour résoudre ces problèmes et disposer en même temps d'une structure de base de données précise et définie, l'approche entité-attribut-valeur a été développée.

Approche EAV

L'essence de l'approche EAV est le stockage séparé des entités, des attributs et des valeurs d'attributs. En général, pour illustrer l'approche EAV, on utilise seulement trois tables, appelés Entité, Attribut et Valeur :

La structure des données de démonstration que nous allons stocker.

Implémentation de l'approche EAV à l'aide de tables

Considérons maintenant un exemple plus complexe utilisant cinq tables (quatre si vous choisissez de consolider les deux derniers tables pour en faire un seul).

La première table est Сatalog:

CREATE TABLE Catalog (
id INT,
name VARCHAR (128),
parent INT
);

Cette table correspond en fait à l'Entité dans l'approche EAV. Elle permettra de stocker les sections du catalogue hiérarchique des marchandises.

La deuxième table est ****Field :

CREATE TABLE Field (
id INT,
name VARCHAR (128),
typeOf INT,
searchable INT,
catalog_id INT,
table_view INT,
sort INT
);

Dans cette table, nous spécifions le nom de l'attribut, son type, et si l'attribut est recherchable. Nous indiquons également la section du catalogue qui contient les marchandises auxquelles ces propriétés appartiennent. Tous les produits de la section du catalogue de catalog_id ou inférieur peuvent avoir des propriétés différentes qui sont stockées dans cette table.

La troisième table est Good.EIle est conçue pour stocker les marchandises, avec leurs prix, la quantité totale des marchandises, la quantité réservée des marchandises, et le nom des marchandises. En principe, vous n'avez pas vraiment besoin de cette table mais, à mon avis, il est utile d'avoir une table séparée pour les marchandises.

CREATE TABLE Good (
id INT,
name VARCHAR (128),
price FLOAT,
item_count INT,
reserved_count,
catalog_id INT
);

La quatrième table (TextValues) et la cinquième table (NumberValues) sont conçues pour stocker les valeurs du texte et les attributs numériques des marchandises, et elles ont une structure similaire.

CREATE TABLE TextValues ​​(
good_id INT,
field_id INT,
fValue TEXT
);

CREATE TABLE NumberValues ​​(
good_id INT,
field_id INT,
fValue INT
);

Au lieu des tables de valeurs textuelles et numériques, vous pouvez utiliser une seule table CustomValues avec une structure de ce type :

CREATE TABLE CustomValues ​​(
good_id INT,
field_id INT,
text_value TEXT,
number_value INT
);

Je préfère stocker les différents types de données séparément car cela augmente la vitesse et économise de l'espace.

Accès aux données à l'aide de l'approche EAV

Commençons par afficher le mappage de la structure du catalogue à l'aide de SQL :

SELECT * FROM Catalog ORDER BY id;

Afin de former un arbre à partir de ces valeurs, un code distinct est nécessaire. En PHP, cela ressemblerait à quelque chose comme ceci :

$stmt = $ pdo-> query ('SELECT * FROM Catalog ORDER BY id');
$aTree = [];
$idRoot = NULL;

while ($row = $ stmt->fetch())
{
    $aTree [$row ['id']] = ['name' => $ row ['name']];

    if (! $row['parent'])
      $idRoot = $row ['id'];
    else
      $aTree [$row['parent']] ['sub'] [] = $row['id'];
}

À l'avenir, nous pourrons simplement dessiner l'arbre si nous partons du nœud racine $aTree[$ idRoot].

Maintenant, nous allons obtenir les propriétés d'un produit spécifique. 

Tout d'abord, nous allons obtenir une liste de propriétés spécifiques à ce produit, puis y attacher les propriétés qui sont dans la base de données. Dans la vie réelle, toutes les propriétés indiquées ne sont pas renseignées et nous sommes donc obligés d'utiliser LEFT JOIN :

SELECT * FROM
(
SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog_id = g.catalog_id
LEFT JOIN TextValues ​​as val ON tv.good = g.id AND f.id = val.field_id
WHERE g.id = $ nGood AND f.type_of = 'text'
UNION
SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog_id = g.catalog_id
LEFT JOIN NumberValues ​​as val ON val.good = g.id AND f.id = val.field_id
WHERE g.id = $nGood AND f.type_of = 'number'
) t
ORDER BY t.sort;

Si nous utilisons une seule table pour stocker les valeurs numériques et textuelles, la requête est considérablement simplifiée :

SELECT g. *, F.name, f.type_of, val.text_value, val.number_value, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog = g.catalog
LEFT JOIN CustomValues ​​as val ON tv.good = g.id AND f.id = val.field_id
WHERE g.id = $nGood
ORDER BY f.sort;

Maintenant, nous allons obtenir les produits sous la forme de table contenue dans la section du catalogue $nCatalog. Tout d'abord, nous obtenons une liste de propriétés qui doivent être reflétées dans la vue de la table pour cette section du catalogue :

SELECT f.id, f.name, f.type_of FROM Catalog as c
INNER JOIN Field as f ON f.catalog_id = c.id
WHERE c.id = $nCatalog AND f.table_view = 1
ORDER BY f.sort;

Ensuite, nous construisons la requête pour créer la table. Supposons que pour une vue tabulaire, nous ayons besoin de trois propriétés supplémentaires (sans compter celles de la table Good). Pour simplifier la requête, nous supposons que :

SELECT g.if, g.name, g.price,
            f1.fValue as f1_val,
            f2.fValue as f2_val,
            f3.fValue as f3_val,
FROM Good
LEFT JOIN TextValue as f1 ON f1.good_id = g.id
LEFT JOIN NumberValue as f2 ON f2.good_id = g.id
LEFT JOIN NumberValue as f3 ON f3.good_id = g.id
WHERE g.catalog_id = $nCatalog;

Les avantages et les inconvénients de l'approche EAV

L'avantage évident de l'approche EAV est sa flexibilité. Avec des structures de données fixes telles que les tables, nous pouvons nous permettre de stocker une grande variété d'ensembles de propriétés pour les objets. Et nous pouvons stocker différentes structures de données sans modifier le schéma de la base de données. 

Nous pouvons également utiliser SQL, qui est familier à un grand nombre de développeurs. 

Le défaut le plus évident est l'inadéquation entre la structure logique des données et leur stockage physique, qui entraîne diverses difficultés. 

En outre, la programmation implique souvent des requêtes SQL très complexes. Le débogage peut être difficile car vous devez créer des outils non-standards pour visualiser les données EAV. Enfin, vous pouvez être amené à utiliser des requêtes LEFT JOIN, qui ralentissent la base de données.

Globales : Une alternative à EAV

Comme je suis familier à la fois du monde SQL et du monde des globales, j'ai eu l'idée que l'utilisation des globales pour les tâches résolues par l'approche EAV serait beaucoup plus intéressante.

Les globales sont des structures de données qui vous permettent de stocker des informations dispersées et hiérarchiques. Un point très important est que les globales sont soigneusement optimisées pour le stockage d'informations hiérarchiques. Les globales sont elles-mêmes des structures de niveau inférieur aux tables, ce qui leur permet de travailler beaucoup plus rapidement que ces derniers.

Dans le même temps, la structure de globale elle-même peut être sélectionnée en fonction de la structure des données, ce qui rend le code très simple et clair.

Structure de globale pour le stockage des données démographiques

Une globale représente une structure tellement flexible et élégante pour le stockage des données que nous pourrions nous débrouiller avec une seule globale pour le stockage des données dans les sections du catalogue, les propriétés et les produits, par exemple, de la manière suivante :

Remarquez à quel point la structure de globale est similaire à la structure de données. Cette conformité simplifie grandement le codage et le débogage.

En pratique, il est préférable d'utiliser plusieurs globales, bien que la tentation de stocker toutes les informations dans une seule globale soit assez forte. Il est judicieux de créer des globales distinctes pour les indices. Vous pouvez également séparer le stockage de la structure de la partition du répertoire des marchandises.

Quelle est la suite ?

Dans le deuxième article de cette série, nous aborderons les détails et les avantages du stockage des données dans des globales InterSystems Iris au lieu de suivre le modèle EAV.

0
0 1036
Article Irène Mykhailova · Mai 9, 2022 14m read

Cet article est le premier d'une série d'articles sur les indexes SQL.

Partie 1 - Découvrez vos indexes

Qu'est-ce qu'un index, en fait ?

Imaginez la dernière fois où vous êtes allé à la bibliothèque. En général, les livres y sont classés par sujet (puis par auteur et par titre), et chaque étagère comporte une étiquette avec un code décrivant le sujet de ses livres. Si vous voulez collectionner des livres d'un certain sujet, au lieu de traverser chaque allée et de lire la couverture intérieure de chaque livre, vous pouvez vous diriger directement vers l'étagère étiquetée avec le sujet désiré et choisir vos livres.

Un index SQL a la même fonction générale : améliorer les performances en donnant une référence rapide à la valeur des champs pour chaque ligne de la table.

La mise en place d'index est l'une des principales étapes de la préparation de vos classes pour une performance SQL optimale.

Dans cet article, nous allons examiner les questions suivantes :

1. Qu'est-ce qu'un index et pourquoi/quand dois-je l'utiliser ?
2. Quels types d'indexes existent et pour quels scénarios sont-ils parfaitement adaptés ?
3. Qu'est-ce qu'un index ?
4. Comment le créer ?

  • Et si j'ai des index, qu'est-ce que j'en fais ?

Je vais me référer aux classes de notre schéma Sample. Celles-ci sont disponibles dans le stockage Github suivant, et elles sont également fournies dans l'espace de noms Samples dans les installations de Caché et Ensemble :

https://github.com/intersystems/Samples-Data

Les principes de base

Vous pouvez indexer chaque propriété persistante et chaque  propriété qui peut être calculée de manière fiable à partir de données persistantes.

Disons que nous voulons indexer la propriété TaxID dans Sample.Company. Dans Studio ou Atelier, nous ajouterions ce qui suit à la définition de la classe :

                Index TaxIDIdx On TaxID;

L'instruction SQL DDL équivalente ressemblerait à ceci :

                CREATE INDEX TaxIDIdx ON Sample.Company (TaxID);

La structure globale de l'index par défaut est la suivante :

                ^Sample.CompanyI("TaxIDIdx",<TaxIDValueAtRowID>,<RowID>) = ""

Notez qu'il y a moins d'index inférieurs à lire que de champs dans une globale de données typique.

Considérons la requête

SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349'

C'est logiquement simple et le plan de requête pour l'exécution de cette requête le reflète :

Ce plan indique essentiellement que nous vérifions l'index global pour les lignes avec la valeur TaxID donnée, puis nous nous référons à la globale de données ("carte principale") pour récupérer la ligne correspondante.

Considérons maintenant la même requête sans index sur TaxIDX. Le plan de requête résultant est, comme prévu, moins efficace :

Sans index, l'exécution de la requête sous-jacente d'IRIS repose sur la lecture en mémoire et l'application de la condition de la clause WHERE à chaque ligne de la table. Et comme nous ne nous attendons logiquement pas à ce qu'une société partage TaxID, nous faisons tout ce travail pour une seule ligne !

Bien sûr, avoir des indexes signifie avoir des données d'index et de ligne sur le disque. En fonction de ce sur quoi nous avons une condition et de la quantité de données que notre table contient, cela peut s'avérer avoir ses propres défis lorsque nous créons et alimentons un index.

Alors, quand ajoutons-nous un index à une propriété ?

Dans le cas général, nous avons fréquemment à remettre une propriété en état. Des exemples sont des informations d'identification telles que le SSN d'une personne ou un numéro de compte bancaire. Vous pouvez également considérer les dates de naissance ou les fonds d'un compte.  Pour en revenir à Sample.Company, la classe bénéficierait peut-être de l'indexation de la propriété Revenue si nous voulions collecter des données sur les organisations à hauts revenus. À l'inverse, les propriétés sur lesquelles il est peu probable que nous remettions des conditions sont moins appropriées pour être indexées : disons un slogan ou une description d'entreprise.

Facile - sauf qu'il faut aussi considérer quel type d'index est le meilleur !

Types d'indexes

Il existe six principaux types d'index que je vais aborder ici : standard, bitmap, compound, collection, bitslice et data. Je vais également aborder brièvement les index iFind, qui sont basés sur les flux. Il y a des chevauchements possibles ici et nous avons déjà abordé les indexes standards avec l'exemple ci-dessus.

Je vais présenter des exemples sur la façon de créer des indexes dans votre définition de classe, mais l'ajout de nouveaux index à une classe est plus complexe que le simple ajout d'une ligne dans votre définition de classe. Nous aborderons des considérations supplémentaires dans la partie suivante.

Prenons l'exemple de Sample.Person. Notez que Person a une sous-classe Employee, ce qui sera utile pour comprendre certains exemples. Employee partage son stockage global de données avec Person, et tous les indexes de Person sont hérités par Employee - ce qui signifie qu'Employee utilise l'index global de Person pour ces indexes hérités.

Si vous n'êtes pas familier avec ces classes, voici un aperçu général de celles-ci : Person a les propriétés SSN, DOB, Name, Home (un objet d'adresse intégré contenant l'état et la ville), Office (également une adresse), et la collection de listes FavoriteColors. Employee a une propriété supplémentaire Salary (que j'ai moi-même définie).

Standard

Index DateIDX On DOB;

J'utilise ici le terme "standard" pour désigner les indexes qui stockent la valeur brute d'une propriété (par opposition à une représentation binaire). Si la valeur est une chaîne de caractères, elle sera stockée sous une certaine collation - celle de SQLUPPER par défaut.

Par rapport aux index bitmap ou bitslice, les indexes standard sont plus compréhensibles pour les humains et relativement faciles à maintenir. Nous avons un nœud global pour chaque ligne de la table.

Voici comment DateIDX est stocké au niveau global.

^Sample.PersonI("DateIDX",51274,100115)="~Sample.Employee~" ; Date is 05/20/81

Notez que le premier index inférieur après le nom de l'index est la valeur de la date, le dernier index inférieur est l'ID de la personne ayant cette date de naissance, et la valeur stockée sur ce noeud global indique que cette personne est également membre de la sous-classe Sample.Employee. Si cette personne n'était membre d'aucune sous-classe, la valeur du noeud serait une chaîne vide.

Cette structure de base sera cohérente avec la plupart des indexes non binaires, où les indexes sur plus d'une propriété créent plus d'indexes inférieurs dans la globale, et où le fait d'avoir plus d'une valeur stockée au nœud produit un objet $listbuild, par exemple :

                ^Package.ClassI(IndexName,IndexValue1,IndexValue2,IndexValue3,RowID) = $lb(SubClass,DataValue1,DataValue2)

Bitmap - Une représentation binaire de l'ensemble des ID-codes correspondant à une valeur de propriété.

Index HomeStateIDX On Home.State [ Type = bitmap];

Les indexes bitmap sont stockés par valeur unique, contrairement aux indexes standard, qui sont stockés par ligne.

Pour aller plus loin dans l'exemple ci-dessus, disons que la personne avec l'ID 1 vit dans le Massachusetts, avec l'ID 2 à New York, avec l'ID 3 dans le Massachusetts et avec l'ID 4 à Rhode Island. HomeStateIDX est essentiellement stocké comme suit :

ID

1

2

3

4

(…)

(…)

0

0

0

0

-

MA

1

0

1

0

-

NY

0

1

0

0

-

RI

0

0

0

1

-

(…)

0

0

0

0

-

Si nous voulions qu'une requête renvoie les données des personnes vivant en Nouvelle-Angleterre, le système effectue un bitwise OR sur les lignes pertinentes de l'index bitmap. On voit rapidement que nous devons charger en mémoire des objets Personne avec les ID 1, 3 et 4 au minimum.

Les bitmaps peuvent être efficaces pour les opérateurs AND, RANGE et OR dans vos clauses WHERE. 

Bien qu'il n'y ait pas de limite officielle au nombre de valeurs uniques que vous pouvez avoir pour une propriété avant qu'un index bitmap soit moins efficace qu'un index standard, la règle générale est d'environ 10 000 valeurs distinctes. Ainsi, si un index bitmap peut être efficace pour un état des États-Unis, un index bitmap pour une ville ou un comté des États-Unis ne serait pas aussi utile.

Un autre concept à prendre en compte est l'efficacité du stockage. Si vous prévoyez d'ajouter et de supprimer fréquemment des lignes de votre table, le stockage de votre index bitmap peut devenir moins efficace. Prenons l'exemple ci-dessus : supposons que nous ayons supprimé de nombreuses lignes pour une raison quelconque et que notre table ne contienne plus de personnes vivant dans des états moins peuplés tels que le Wyoming ou le Dakota du Nord. Le bitmap comporte donc plusieurs lignes contenant uniquement des zéros. D'un autre côté, la création de nouvelles lignes dans les grandes tables peut finir par devenir plus lente, car le stockage bitmap doit accueillir un plus grand nombre de valeurs uniques.

Dans ces exemples, j'ai environ 150 000 lignes dans Sample.Person. Chaque nœud global stocke jusqu'à 64 000 ID, de sorte que l'index bitmap global à la valeur MA est divisé en trois parties :

      ^Sample.PersonI("HomeStateIDX"," MA",1)=$zwc(135,7992)_$c(0,(...))

^Sample.PersonI("HomeStateIDX"," MA",2)=$zwc(404,7990,(…))

^Sample.PersonI("HomeStateIDX"," MA",3)=$zwc(132,2744)_$c(0,(…))

Cas particulier : Bitmap étendu 

Un bitmap étendue, souvent appelé $<ClassName>, est un index bitmap sur les ID d'une classe - cela donne à IRIS un moyen rapide de savoir si une ligne existe et peut être utile pour les requêtes COUNT ou les requêtes sur les sous-classes. Ces indexes sont générés automatiquement lorsqu'un index bitmap est ajouté à la classe ; vous pouvez également créer manuellement un index bitmap d'étendue dans une définition de classe comme suit :

Index Company [ Extent, SqlName = "$Company", Type = bitmap ];

Ou via le mot-clé DDL appelé BITMAPEXTENT :

CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company

Composés - Les indexes basés sur deux ou plusieurs propriétés

Index OfficeAddrIDX On (Office.City, Office.State);

Le cas général d'utilisation des index composés est le conditionnement de requêtes fréquentes sur deux propriétés ou plus.

L'ordre des propriétés dans un index composé est important en raison de la manière dont l'index est stocké au niveau global. Le fait d'avoir la propriété la plus sélective en premier est plus efficace en termes de performances car cela permet d'économiser les lectures initiales du disque de l'index global ; dans cet exemple, Office.City est en premier car il y a plus de villes uniques que d'états aux États-Unis.

Le fait d'avoir une propriété moins sélective en premier est plus efficace en termes d'espace. En termes de structure globale, l'arbre d'indexation serait plus équilibré si State était placé en premier. Pensez-y : chaque état contient de nombreuses villes, mais certains noms de ville n'appartiennent qu'à un seul état.

Vous pouvez également vous demander si vous vous attendez à exécuter des requêtes fréquentes ne conditionnant qu'une seule de ces propriétés - cela peut vous éviter de définir un autre index.

Voici un exemple de la structure globale des indexes composés :

^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~"

Commentaires : Index composé ou index bitmap ?

Pour les requêtes comportant des conditions sur plusieurs propriétés, vous pouvez également vous demander si des indexes bitmap séparés seraient plus efficaces qu'un seul index composé.

Les opérations par bit sur deux indexes différents peuvent être plus efficaces à condition que les indexes bitmap conviennent à chaque propriété.

Il est également possible d'avoir des indexes bitmap composés, c'est-à-dire des indexes bitmap dont la valeur unique est l'intersection de plusieurs propriétés sur lesquelles vous effectuez l'indexation. Considérez la table donnée dans la section précédente, mais au lieu des états, nous avons toutes les paires possibles d'un état et d'une ville (par exemple, Boston, MA, Cambridge, MA, même Los Angeles, MA, etc.), et les cellules obtiennent des 1 pour les lignes qui adhèrent aux deux valeurs.

Collection - Les index basés sur les propriétés de la collection

Nous avons ici la propriété FavoriteColors définie comme suit :

Property FavoriteColors As list Of %String;

Avec chacun des indexes suivants définis à titre de démonstration :

Index fcIDX1 On FavoriteColors(ELEMENTS);
Index fcIDX2 On FavoriteColors(KEYS);

J'utilise ici le terme "collection" pour désigner plus largement les propriétés à cellule unique contenant plus d'une valeur. Les propriétés List Of et Array Of sont pertinentes ici, et si vous le souhaitez, même les chaînes de caractères délimitées.

Les propriétés de la collection sont automatiquement analysées pour construire leurs indexes. Pour les propriétés délimitées, comme un numéro de téléphone, vous devez définir cette méthode, <PropertyName>BuildValueArray(value, .valueArray), explicitement.

Compte tenu de l'exemple ci-dessus pour FavoriteColors, fcIDX1 ressemblerait à ceci pour une personne dont les couleurs préférées sont le bleu et le blanc :

^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~"

(…)

^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~"

fcIDX2 ressemblerait à :

         ^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~"      

^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~"

Dans ce cas, puisque FavoriteColors est une collection de listes, un index basé sur ses clés est moins utile qu'un index basé sur ses éléments.

Veuillez vous référer à notre documentation pour des considérations plus approfondies sur la création et la gestion des indexes sur les propriétés des collections.

Bitslice - Représentation en bitmap de la représentation en chaîne de bits des données numériques

Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee

Contrairement aux indexes bitmap, qui contiennent des balises indiquant quelles lignes contiennent une valeur spécifique, les indexes bitslice convertissent d'abord les valeurs numériques de la décimale à la binaire, puis créent un bitmap sur chaque chiffre de la valeur binaire.

Reprenons l'exemple ci-dessus et, par souci de réalisme, simplifions le salaire en unités de 1 000 dollars. Ainsi, si le salaire d'un employé est enregistré sous la forme 65, il est compris comme représentant 65 000 dollars.

Disons que nous avons un employé avec l'ID 1 qui a un salaire de 15, l'ID 2 un salaire de 40, l'ID 3 un salaire de 64 et l'ID 4 un salaire de 130. Les valeurs binaires correspondantes sont :

15

0

0

0

0

1

1

1

1

40

0

0

1

0

1

0

0

0

64

0

1

0

0

0

0

0

0

130

1

0

0

0

0

0

1

0

Notre chaîne de bits s'étend sur 8 chiffres. La représentation bitmap correspondante - les valeurs d'indexes bitslice - est essentiellement stockée comme suit :

^Sample.PersonI("SalaryIDX",1,1) = "1000" ; La ligne 1 a une valeur à la place 1

^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Les lignes 1 et 4 ont des valeurs à la place 2

^Sample.PersonI("SalaryIDX",3,1) = "1000" ; La ligne 1 a une valeur à la place 4

^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Les lignes 1 et 2 ont des valeurs à la place 8

^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc…

^Sample.PersonI("SalaryIDX",6,1) = "0100"

^Sample.PersonI("SalaryIDX",7,1) = "0010"

^Sample.PersonI("SalaryIDX",8,1) = "0001"

Notez que les opérations modifiant Sample.Employee ou les salaires dans ses lignes, c'est-à-dire les INSERTs, UPDATESs et DELETEs, nécessitent maintenant la mise à jour de chacun de ces nœuds globaux, ou bitslices. L'ajout d'un index bitslice à plusieurs propriétés d'une table ou à une propriété fréquemment modifiée peut présenter des risques pour les performances. En général, la maintenance d'un index bitslice est plus coûteuse que celle des indexes standard ou bitmap.

Les indexes Bitslice sont hautement spécialisés et ont donc des cas d'utilisation spécifiques : les requêtes qui doivent effectuer des calculs agrégés, par exemple SUM, COUNT ou AVG.

En outre, ils ne peuvent être utilisés efficacement que sur des valeurs numériques - les chaînes de caractères sont converties en un 0 binaire.

Notez que si la table de données, et non les index, doit être lu pour vérifier la condition d'une requête, les indexes bitslice ne seront pas choisis pour exécuter la requête. Supposons que Sample.Person ne possède pas d'index sur Name. Si nous calculions le salaire moyen des employés portant le nom de famille Smith :

SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,'

nous aurions besoin de lire des lignes de données pour appliquer la condition WHERE, et donc l'index bitslice ne serait pas utilisé en pratique.

Des problèmes de stockage similaires se posent pour les indexes bitslice et bitmap sur les tables où des lignes sont fréquemment créées ou supprimées.

Data - Index dont les données sont stockées dans leurs nœuds globaux.

Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];

Dans plusieurs des exemples précédents, vous avez peut-être observé la chaîne “~Sample.Employee~” stockée comme valeur au niveau du noeud lui-même. Rappelez-vous que Sample.Employee hérite des indexes de Sample.Person. Lorsque nous effectuons une requête sur les employés en particulier, nous lisons la valeur aux nœuds d'index correspondant à notre condition de propriété pour vérifier que ladite personne est également un employé.

On peut aussi définir explicitement les valeurs à stocker. Le fait d'avoir des données définies au niveau des nœuds globaux de l'index permet d'éviter la lecture de l'ensemble des données globales ; cela peut être utile pour les requêtes sélectives ou les requêtes ordonnées fréquentes.

Considérons l'index ci-dessus comme un exemple. Si nous voulions extraire des informations d'identification sur une personne à partir de tout ou une partie de son nom (par exemple, pour rechercher des informations sur les clients dans une application de réception), nous pourrions avoir une requête telle que 

SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name

Puisque les conditions de notre requête sur le nom et les valeurs que nous récupérons sont toutes contenues dans les nœuds globaux QuickSearchIDX, il nous suffit de lire notre I globale pour exécuter cette requête.

Notez que les valeurs de données ne peuvent pas être stockées avec des indexes de bitmap ou de bitslice.

^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.")

iFind Indexes

Vous en avez déjà entendu parler ? Moi non plus. Les indexes iFind sont utilisés sur les propriétés des flux, mais pour les utiliser vous devez spécifier leurs noms avec des mots-clés dans la requête.

Je pourrais vous en dire plus, mais Kyle Baxter a déjà rédigé un article utile à ce sujet.

0
0 119
Article Lorenzo Scalese · Avr 15, 2022 26m read

Depuis Caché 2017, le moteur SQL comprend un nouvel ensemble de statistiques. Celles-ci enregistrent le nombre de fois qu'une requête est exécutée et le temps qu'elle prend pour s'exécuter.

C'est une mine d'or pour quiconque surveille et tente d'optimiser les performances d'une application qui comprend de nombreuses instructions SQL, mais il n'est pas aussi facile d'accéder aux données que certaines personnes le souhaitent.

0
0 59
Article Guillaume Rongier · Avr 6, 2022 10m read

Dans les parties précédentes (1 et 2) nous avons parlé des globales en tant qu'arbres. Dans cet article, nous allons les considérer comme des listes éparses.

Une liste éparse - est un type de liste où la plupart des valeurs ont une valeur identique.

En pratique, vous verrez souvent des listes éparses si volumineuses qu'il est inutile d'occuper la mémoire avec des éléments identiques. Il est donc judicieux d'organiser les listes éparses de telle sorte que la mémoire ne soit pas gaspillée pour stocker des valeurs en double.

Dans certains langages de programmation, les listes éparses font partie intégrante du langage - par exemple, in J, MATLAB. Dans d'autres langages, il existe des bibliothèques spéciales qui vous permettent de les utiliser. Pour le C++, il s'agit de Eigen et d'autres bibliothèques de ce type.

Les globales sont de bons candidats pour la mise en œuvre de listes éparses pour les raisons suivantes :

  1. Ils stockent uniquement les valeurs de nœuds particuliers et ne stockent pas les valeurs indéfinies ;

  2. L'interface d'accès à une valeur de nœud est extrêmement similaire à ce que de nombreux langages de programmation proposent pour accéder à un élément d'une liste multidimensionnelle.   Set ^a(1, 2, 3)=5 Write ^a(1, 2, 3)

  3. Une structure globale est une structure de niveau assez bas pour le stockage des données, ce qui explique pourquoi les globales possèdent des caractéristiques de performance exceptionnelles (des centaines de milliers à des dizaines de millions de transactions par seconde selon le matériel, voir 1)

Puisqu'une globale est une structure persistante, il n'est logique de créer des listes éparses sur leur base que dans les situations où vous savez à l'avance que vous disposerez de suffisamment de mémoire pour elles.   L'une des nuances de la mise en œuvre des listes éparses est le retour d'une certaine valeur par défaut si vous vous adressez à un élément indéfini.

Ceci peut être mis en œuvre en utilisant la fonction $GET dans COS. Prenons l'exemple d'une liste tridimensionnelle.

SET a = $GET(^a(x,y,z), defValue)

Quel type de tâches nécessite des listes éparses et comment les globales peuvent-elles vous aider ?

Matrice d'adjacence

Ces matrices sont utilisées pour la représentation des graphiques :

Il est évident que plus un graphe est grand, plus il y aura de zéros dans la matrice. Si nous regardons le graphe d'un réseau social, par exemple, et que nous le représentons sous la forme d'une matrice de ce type, il sera principalement constitué de zéros, c'est-à-dire qu'il s'agira d'une liste éparse.

Set ^m(id1, id2) = 1
Set ^m(id1, id3) = 1
Set ^m(id1, id4) = 1
Set ^m(id1) = 3
Set ^m(id2, id4) = 1
Set ^m(id2, id5) = 1
Set ^m(id2) = 2
....

Dans cet exemple, nous allons sauvegarder la matrice d'adjacence dans le ^m globale, ainsi que le nombre d'arêtes de chaque nœud (qui est ami et avec qui et le nombre d'amis).

Si le nombre d'éléments du graphique ne dépasse pas 29 millions (ce nombre est calculé comme 8 * longueur maximale de la chaîne), il existe même une méthode plus économique pour stocker de telles matrices - les chaines binaires, car elles optimisent les grands espaces d'une manière spéciale.

Les manipulations de chaînes binaires sont effectuées à l'aide de la fonction $BIT.

; setting a bit
SET $BIT(rowID, positionID) = 1
; getting a bit
Write $BIT(rowID, positionID)

Tableau des commutateurs FSM

Le graphe des commutateurs FSM étant un graphe régulier, le tableau des commutateurs FSM est essentiellement la même matrice d'adjacence dont nous avons parlé précédemment.

Automates cellulaires

L'automate cellulaire le plus célèbre est le jeu "Life", dont les règles (lorsqu'une cellule a de nombreux voisins, elle meurt) en font essentiellement une liste éparse.

Stephen Wolfram estime que les automates cellulaires représentent un nouveau domaine de la science. En 2002, il a publié un livre de 1280 pages intitulé "A New Kind of Science", dans lequel il affirme que les réalisations dans le domaine des automates cellulaires ne sont pas isolées, mais sont plutôt stables et importantes pour tous les domaines de la science.

Il a été prouvé que tout algorithme qui peut être traité par un ordinateur peut également être mis en œuvre à l'aide d'un automate cellulaire. Les automates cellulaires sont utilisés pour simuler des environnements et des systèmes dynamiques, pour résoudre des problèmes algorithmiques et à d'autres fins.

Si nous avons un champ considérable et que nous devons enregistrer tous les états intermédiaires d'un automate cellulaire, il est logique d'utiliser les globales.

Cartographie

La première chose qui me vient à l'esprit lorsqu'il s'agit d'utiliser des listes éparses est la cartographie.

En règle générale, les cartes comportent beaucoup d'espace vide. Si nous imaginons que la carte du monde est composée de grands pixels, nous verrons que 71 % de tous les pixels de la Terre seront occupés par le réseau creux de l'océan. Et si nous ajoutons uniquement des structures artificielles à la carte, il y aura plus de 95 % d'espace vide.

Bien sûr, personne ne stocke les cartes sous forme de tableaux bitmap, tout le monde utilise plutôt la représentation vectorielle.
Mais en quoi consistent les cartes vectorielles ? C'est une sorte de cadre avec des polylignes et des polygones.
En fait, il s'agit d'une base de données de points et de relations entre eux.

L'une des tâches les plus difficiles en cartographie est la création d'une carte de notre galaxie réalisée par le télescope Gaia. Au sens figuré, notre galaxie est un gigantesque réseau creux : d'immenses espaces vides avec quelques points lumineux occasionnels - des étoiles. C'est 99,999999.......% d'espace absolument vide. Caché, une base de données basée sur des globales, a été choisie pour stocker la carte de notre galaxie.

Je ne connais pas la structure exacte des globales dans ce projet, mais je peux supposer que c'est quelque chose comme ça :

Set ^galaxy(b, l, d) = 1; le numéro de catalogue de l'étoile, s'il existe
Set ^galaxy(b, l, d, "name") = "Sun"
Set ^galaxy(b, l, d, "type") = "normal" ; les autres options peuvent inclure un trou noir, quazar, red_dwarf et autres.
Set ^galaxy(b, l, d, "weight") = 14E50
Set ^galaxy(b, l, d, "planetes") = 7
Set ^galaxy(b, l, d, "planetes", 1) = "Mercure"
Set ^galaxy(b, l, d, "planetes", 1, weight) = 1E20
...

Où b, l, d représententcoordonnées galactiques: la latitude, la longitude et la distance par rapport au Soleil.

La structure flexible des globales vous permet de stocker toutes les caractéristiques des étoiles et des planètes, puisque les bases de données basées sur les globales sont exemptes de schéma.

Caché a été choisi pour stocker la carte de notre univers non seulement en raison de sa flexibilité, mais aussi grâce à sa capacité à sauvegarder rapidement un fil de données tout en créant simultanément des globales d'index pour une recherche rapide.

Si nous revenons à la Terre, les globales ont été utilisées dans des projets axés sur les cartes comme OpenStreetMap XAPI et FOSM, un branchement d'OpenStreetMap.

Récemment, lors d'un hackathon Caché, un groupe de développeurs a mis en œuvre des index géospatiaux en utilisant cette technologie. Pour plus de détails, consultez l'article.

Mise en œuvre d'index géospatiaux à l'aide de globales dans OpenStreetMap XAPI

Les illustrations sont tirées de cette présentation.

Le globe entier est divisé en carrés, puis en sous-carrés, puis en encore plus de sous-carrés, et ainsi de suite. Au final, nous obtenons une structure hiérarchique pour laquelle les globales ont été créées.

À tout moment, nous pouvons instantanément demander n'importe quelle case ou la vider, et toutes les sous-carrés seront également retournées ou vidées.

Un schéma détaillé basé sur les globales peut être mis en œuvre de plusieurs façons.

Variante 1:

Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 1) = idPointOne
Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 2) = idPointTwo
...

Variante 2:

Set ^m('abacdabcdabacdabcda', 1) = idPointOne
Set ^m('abacdabcdabacdabcda', 2) = idPointTwo
...

Dans les deux cas, il ne sera pas très difficile dans COS/M de demander des points situés dans un carré de n'importe quel niveau. Il sera un peu plus facile de dégager des segments d'espace carrés de n'importe quel niveau dans la première variante, mais cela est rarement nécessaire.

Un exemple de carré de bas niveau :

Et voici quelques globales du projet XAPI : représentation d'un index basé sur des globales :

La globale ^voie est utilisé pour stocker les sommets des polylines (routes, petites rivières, etc.) et des polygones (zones fermées : bâtiments, bois, etc.).

Une classification approximative de l'utilisation des listes éparses dans les globales.

  1. Nous stockons les coordonnées de certains objets et leur état (cartographie, automates cellulaires).
  2. Nous stockons des matrices creuses.

Dans la variante 2), lorsqu'une certaine coordonnée est demandée et qu'aucune valeur n'est attribuée à un élément, nous devons obtenir la valeur par défaut de l'élément de la liste éparse.

Les avantages que nous obtenons en stockant des matrices multidimensionnelles dans les globales

Suppression et/ou sélection rapide de segments d'espace qui sont des multiples de chaînes, de surfaces, de cubes, etc. Pour les cas avec des index intégraux, il peut être pratique de pouvoir supprimer et/ou sélectionner rapidement des segments d'espace qui sont des multiples de chaînes, de surfaces, de cubes, etc.

La commande Kill permet de supprimer un élément autonome, une chaîne de caractères et même une surface entière. Grâce aux propriétés de la globale, elle se produit très rapidement, mille fois plus vite que la suppression élément par élément.

L'illustration montre un tableau tridimensionnel dans la globale ^a et différents types d'enlèvements.

Pour sélectionner des segments d'espace par des indices connus, vous pouvez utiliser la commande Merge.

Sélection d'une colonne de la matrice dans la colonne Variable :

; Définissons un tableau tridimensionnel creux 3x3x3
Set ^a(0,0,0)=1,^a(2,2,0)=1,^a(2,0,1)=1,^a(0,2,1)=1,^a(2,2,2)=1,^a(2,1,2)=1
Colonne de fusion = ^a(2,2)
; Produisons la colonne Variable
Zwrite colonne

Produit :

Column(0)=1
Colonne(2)=1

Ce qui est intéressant, c'est que nous avons obtenu un tableau épars dans la colonne Variable que vous pouvez adresser via $GET puisque les valeurs par défaut ne sont pas stockées ici.

La sélection de segments d'espace peut également se faire à l'aide d'un petit programme utilisant la fonction $Order. Ceci est particulièrement utile pour les espaces dont les indices ne sont pas quantifiés (cartographie).

Conclusion

Les réalités d'aujourd'hui posent de nouveaux défis. Les graphes peuvent comporter des milliards de sommets, les cartes peuvent avoir des milliards de points, certains peuvent même vouloir lancer leur propre univers basé sur des automates cellulaires (1, 2).

Lorsque le volume de données dans les listes éparses ne peut pas être comprimé dans la RAM, mais que vous devez quand même travailler avec elles, vous devriez envisager de mettre en œuvre de tels projets en utilisant des globales et des COS.

Clause de non-responsabilité :: cet article et les commentaires le concernant reflètent uniquement mon opinion et n'ont rien à voir avec la position officielle de la société d'InterSystems.
0
0 116
Article Guillaume Rongier · Avr 4, 2022 13m read

3. Variantes des structures lors de l'utilisation de globales

Une structure, telle qu'un arbre ordonné, présente plusieurs cas particuliers. Examinons ceux qui ont une valeur pratique pour le travail avec les globales.

3.1 Cas particulier 1. Un nœud sans branches

Les globales peuvent être utilisées non seulement comme une liste de données, mais aussi comme des variables ordinaires. Par exemple, pour créer un compteur :  

Set ^counter = 0  ; setting counter
Set id=$Increment(^counter) ;  atomic incrementation

En même temps, une globale peut avoir des branches outre sa valeur. L'un n'exclut pas l'autre.

3.2 Cas particulier 2. Un nœud et plusieurs branches

En fait, il s'agit d'une base classique clé-valeur. Et si nous enregistrons des tuples de valeurs au lieu de valeurs, nous obtiendrons une table ordinaire avec une clé primaire.

Afin d'implémenter une table basé sur des globales, nous devrons former des chaînes de caractères à partir des valeurs des colonnes, puis les enregistrer dans une globale par la clé primaire. Afin de pouvoir diviser la chaîne en colonnes lors de la lecture, nous pouvons utiliser ce qui suit :

  1. Caractère de délimitation.
Set ^t(id1) = "col11/col21/col31"
Set ^t(id2) = "col12/col22/col32"
  1. Un schéma fixe, dans lequel chaque champ occupe un nombre particulier d'octets. C'est ainsi qu'on procède généralement dans les bases de données relationnelles.

  2. Une fonction spéciale $LB (introduite dans Caché) qui compose une chaîne de caractères à partir de valeurs.

Set ^t(id1) = $LB("col11", "col21", "col31")
Set ^t(id2) = $LB("col12", "col22", "col32")

Ce qui est intéressant, c'est qu'il n'est pas difficile de faire quelque chose de similaire aux clés étrangères dans les bases de données relationnelles en utilisant des globales. Appelons ces structures des index globaux. Un index global est un arbre supplémentaire permettant d'effectuer des recherches rapides sur des champs qui ne font pas partie intégrante de la clé primaire de la globale principale. Vous devez écrire un code supplémentaire pour le remplir et l'utiliser.

Nous créons un index global basé sur la première colonne.

Set ^i("col11", id1) = 1
Set ^i("col12", id2) = 1

Pour effectuer une recherche rapide par la première colonne, vous devrez regarder dans la ^i globale et trouver les clés primaires (id) correspondant à la valeur nécessaire dans la première colonne.

Lors de l'insertion d'une valeur, nous pouvons créer à la fois des valeurs et des index globaux pour les champs nécessaires. Pour plus de fiabilité, nous allons l'intégrer dans une transaction.

TSTART
Set ^t(id1) = $LB("col11", "col21", "col31")
Set ^i("col11", id1) = 1
TCOMMIT

Plus d'informations sont disponibles ici making tables in M using globals and emulation of secondary keys.

Ces tables fonctionneront aussi rapidement que dans les bases de données traditionnelles (ou même plus rapidement) si les fonctions d'insertion/mise à jour/suppression sont écrites en COS/M et compilées.

J'ai vérifié cette affirmation en appliquant un grand nombre d'opérations INSERT et SELECT à une seule table à deux colonnes, en utilisant également les commandes TSTART et TCOMMIT (transactions).

Je n'ai pas testé de scénarios plus complexes avec des accès concurrents et des transactions parallèles.

Sans utiliser de transactions, la vitesse d'insertion pour un million de valeurs était de 778 361 insertions/seconde.

Pour 300 millions de valeurs, la vitesse était de 422 141 insertions/seconde.

Lorsque des transactions ont été utilisées, la vitesse a atteint 572 082 insertions/seconde pour 50 millions de valeurs. Toutes les opérations ont été exécutées à partir du code M compilé. J'ai utilisé des disques durs ordinaires, pas des SSD. RAID5 avec Write-back. Le tout fonctionnant sur un processeur Phenom II 1100T.

Pour effectuer le même test pour une base de données SQL, il faudrait écrire une procédure stockée qui effectuerait les insertions en boucle. En testant MySQL 5.5 (stockage InnoDB) avec la même méthode, je n'ai jamais obtenu plus de 11K insertions par seconde.

En effet, l'implémentation de tables avec des globales est plus complexe que de faire la même chose dans des bases de données relationnelles. C'est pourquoi les bases de données industrielles basées sur les globales ont un accès SQL pour simplifier le travail avec les données tabulaires.

En général, si le schéma de données ne change pas souvent, que la vitesse d'insertion n'est pas critique et que l'ensemble de la base de données peut être facilement représenté par des tables normalisées, il est plus facile de travailler avec SQL, car il offre un niveau d'abstraction plus élevé.

Dans ce cas, je voulais montrer que les globales peuvent être utilisées comme un constructeur pour créer d'autres bases de données. Comme le langage assembleur qui peut être utilisé pour créer d'autres langages. Et voici quelques exemples d'utilisation des globales pour créer des contreparties de key-values, lists, sets, tabular, document-oriented DB's.

Si vous devez créer une base de données non standard avec un minimum d'efforts, vous devriez envisager d'utiliser les globales.

3.3 Cas particulier 3. Un arbre à deux niveaux dont chaque nœud de deuxième niveau a un nombre fixe de branches

Vous l'avez probablement deviné : il s'agit d'une implémentation alternative des tables utilisant des globales. Comparons-la avec la précédente.

<th>
  Pros
</th>
<td>
  Un accès plus rapide aux valeurs de certaines colonnes, puisque vous n'avez pas besoin d'analyser la chaîne de caractères. D'après mes tests, c'est 11,5 % plus rapide pour 2 colonnes et encore plus rapide pour plus de colonnes. Il est plus facile de modifier le schéma de données et de lire le code.
</td>
Tables dans un arborescence deux niveaux vs. Tables dans un arborescence mono niveau.
Cons
Insertions plus lentes, car le nombre de nœuds doit être égal au nombre de colonnes. Une plus grande consommation d'espace sur le disque dur, car les index globaux (comme les index de table) avec les noms de colonne occupent de l'espace sur le disque dur et sont dupliqués pour chaque ligne.  

Conclusion: Rien d'extraordinaire. Les performances étant l'un des principaux avantages des globales, il n'y a pratiquement aucun intérêt à utiliser cette approche, car il est peu probable qu'elle soit plus rapide que les tables ordinaires des bases de données relationnelles.

3.4 Cas général. Arbres et clés ordonnées

Toute structure de données qui peut être représentée comme un arbre s'adapte parfaitement aux globales.

3.4.1 Objets avec des sous-objets

C'est dans ce domaine que les globales sont traditionnellement utilisées. Il existe de nombreuses maladies, médicaments, symptômes et méthodes de traitement dans le domaine médical. Il est irrationnel de créer une table avec un million de champs pour chaque patient, d'autant plus que 99% d'entre eux seront vides.

Imaginez une base de données SQL composée des tables suivants : " Patient " ~ 100 000 champs, " Médicament " 100 000 champs, " Thérapie " 100 000 champs, " Complications " 100 000 champs et ainsi de suite. Comme alternative, vous pouvez créer une BD avec des milliers de tableaux, chacun pour un type de patient particulier (et ils peuvent aussi se superposer !), un traitement, un médicament, ainsi que des milliers de tables pour les relations entre ces tables.

Les globales s'adaptent parfaitement aux soins de santé, puisqu'elles permettent à chaque patient de disposer d'un dossier complet, de la liste des thérapies, des médicaments administrés et de leurs effets, le tout sous la forme d'un arbre, sans gaspiller trop d'espace disque en colonnes vides, comme ce serait le cas avec les bases de données relationnelles.

Les globales fonctionnent bien pour les bases de données contenant des données personnelles, lorsque la tâche consiste à accumuler et à systématiser le maximum de données personnelles diverses sur un client. C'est particulièrement important dans les domaines de la santé, de la banque, du marketing, de l'archivage et autres.

Il est évident que SQL permet également d'émuler un arbre en utilisant seulement quelques tables (EAV, 1,2,3,4,5,6, 7,8), mais c'est beaucoup plus complexe et plus lent. En fait, nous devrions écrire une globale basé sur des tables et cacher toutes les routines liées aux tables sous une couche d'abstraction. Il n'est pas correct d'émuler une technologie de niveau inférieur (les globales) à l'aide d'une technologie de niveau supérieur (SQL). C'est tout simplement injustifié.

Ce n'est pas un secret que la modification d'un schéma de données dans des tableaux gigantesques (ALTER TABLE) peut prendre un temps considérable. MySQL, par exemple, effectue l'opération ALTER TABLE ADD|DROP COLUMN en copiant toutes les données de l'ancienne tableau vers la nouvelle (je l'ai testé sur MyISAM et InnoDB). Cela peut bloquer une base de données de production contenant des milliards d'enregistrements pendant des jours, voire des semaines.

Si nous utilisons des globales, la modification de la structure des données ne nous coûte rien Nous pouvons ajouter de nouvelles propriétés à n'importe quel objet, à n'importe quel niveau de la hiérarchie et à n'importe quel moment. Les changements qui nécessitent de renommer les branches peuvent être appliqués en arrière-plan avec la base de données en fonctionnement.


Par conséquent, lorsqu'il s'agit de stocker des objets comportant un grand nombre de propriétés facultatives, les globales fonctionnent parfaitement.

Je vous rappelle que l'accès à l'une des propriétés est instantané, puisque dans une globale, tous les chemins sont un B-Arbre.

Dans le cas général, les bases de données basées sur des globales sont un type de bases de données orientées documents qui supportent le stockage d'informations hiérarchiques. Par conséquent, les bases de données orientées documents peuvent concurrencer efficacement les globales dans le domaine du stockage des cartes médicales.

Mais ce n'est pas encore le cas.

Prenons MongoDB, par exemple. Dans ce champ, il perd face aux globales pour les raisons suivantes :
  1. Taille du document. L'unité de stockage est un texte au format JSON (BSON, pour être exact) dont la taille maximale est d'environ 16 Mo. Cette limitation a été introduite dans le but de s'assurer que la base de données JSON ne devienne pas trop lente lors de l'analyse syntaxique, lorsqu'un énorme document JSON y est enregistré et que des valeurs de champ particulières sont traitées. Ce document est censé contenir des informations complètes sur un patient. Nous savons tous à quel point les cartes de patient peuvent être épaisses. Si la taille maximale de la carte est plafonnée à 16 Mo, cela permet de filtrer immédiatement les patients dont les cartes contiennent des IRM, des radiographies et d'autres documents. Une seule branche d'une entreprise mondiale peut contenir des gigaoctets, des pétaoctets ou des téraoctets de données. Tout est dit, mais laissez-moi vous en dire plus.
  2. Le temps nécessaire à la création/modification/suppression de nouvelles propriétés de la carte du patient. Une telle base de données devrait copier la carte entière dans la mémoire (beaucoup de données !), analyser les données BSON, ajouter/modifier/supprimer le nouveau nœud, mettre à jour les index, remballer le tout en BSON et sauvegarder sur le disque. Une globale n'aurait besoin que d'adresser la propriété nécessaire et d'effectuer l'opération nécessaire.
  3. La vitesse d'accès à des propriétés particulières. Si le document possède de nombreuses propriétés et une structure à plusieurs niveaux, l'accès à des propriétés particulières sera plus rapide car chaque chemin dans la globale est le B-Arbre. En BSON, vous devrez analyser linéairement le document pour trouver la propriété nécessaire.

3.3.2 Tables associatives

Les tables associatives (même avec les tables imbriquées) fonctionnent parfaitement avec les globales. Par exemple, cette table PHP ressemblera à la première illustration en 3.3.1.

$a = array(
  "name" => "Vince Medvedev",
  "city" => "Moscow",
  "threatments" => array(
    "surgeries" => array("apedicectomy", "biopsy"),
    "radiation" => array("gamma", "x-rays"),
    "physiotherapy" => array("knee", "shoulder")
  )
);

3.3.3 Documents hiérarchiques : XML, JSON

Ils peuvent également être facilement stockés dans des globales et décomposés de manières différentes.

XML

La méthode la plus simple pour décomposer le XML en globales consiste à stocker les attributs des balises dans les nœuds. Et si vous avez besoin d'un accès rapide aux attributs des attributs, nous pouvons les placer dans des branches séparées.

<note id=5>
<to>Alex</to>
<from>Sveta</from>
<heading>Reminder</heading>
<body>Call me tomorrow!</body>
</note>

Dans COS, le code ressemblera à ceci :

Set ^xml("note")="id=5"
Set ^xml("note","to")="Alex"
Set ^xml("note","from")="Sveta"
Set ^xml("note","heading")="Reminder"
Set ^xml("note","body")="Call me tomorrow!"

Note: Pour XML, JSON et les tables associatives, vous pouvez imaginer un certain nombre de méthodes pour les afficher dans les globales. Dans ce cas particulier, nous n'avons pas reflété l'ordre des balises imbriquées dans la balise "note". Dans la globale ^xml, les balises imbriquées seront affichés dans l'ordre alphabétique. Pour un affichage précis de l'ordre, vous pouvez utiliser le modèle suivant, par exemple :

JSON.

Le contenu de ce document JSON est présenté dans la première illustration de la section 3.3.1 :

var document = {
  "name": "Vince Medvedev",
  "city": "Moscow",
  "threatments": {
    "surgeries": ["apedicectomy", "biopsy"],
    "radiation": ["gamma", "x-rays"],
    "physiotherapy": ["knee", "shoulder"]
  },
};

3.3.4 Structures identiques liées par des relations hiérarchiques

Exemples : structure des bureaux de vente, positions des personnes dans une structure MLM, base des débuts aux échecs.

Base de données des débuts. Vous pouvez utiliser une évaluation de la force du mouvement comme valeur de l'indice de nœud d'une globale. Dans ce cas, vous devrez sélectionner une branche ayant le poids le plus élevé pour déterminer le meilleur déplacement. Dans la globale, toutes les branches de chaque niveau seront triées en fonction de la force du mouvement.

La structure des bureaux de vente, des personnes dans une société MLM. Les noeuds peuvent stocker certaines valeurs de cache reflétant les caractéristiques de la sous-arborescence entière. Par exemple, les ventes de cette sous-arborescence particulière. Nous pouvons obtenir des informations exactes sur les réalisations de n'importe quelle branche à tout moment.

4. Situations où l'utilisation des globales est avantageuse

La première colonne contient une liste de cas où l'utilisation des globales vous donnera un avantage considérable en termes de performance, et la seconde - une liste de situations où elles simplifieront le développement ou le modèle de données.

<th>
  Commodité du traitement/de la présentation des données
</th>
<td>
  1.  Objets/instances avec un grand nombre de propriétés/instances non requises [et/ou imbriquées] 
    
  2.   Données sans schéma - de nouvelles propriétés peuvent souvent être ajoutées et d'anciennes supprimées
    
  3.   Vous devez créer une BD non standard.Bases de données de chemins et arbres de solutions 
    
  4.   Lorsque les chemins peuvent être représentés de manière pratique sous forme d'arbre
    
  5.  On doit supprimer les structures hiérarchiques sans utiliser la récursion  
    
Vitesse
1. Insertion [avec tri automatique à chaque niveau], [indexation par la clé primaire] 2. Suppression de sous-arbres 3. Objets comportant de nombreuses propriétés imbriquées auxquelles vous devez accéder individuellement 4. Structure hiérarchique avec possibilité de parcourir les branches enfant à partir de n'importe quelle branche, même inexistante 5. Parcours en profondeur de l'arbre
Clause de non-responsabilité: cet article et les commentaires le concernant reflètent uniquement mon opinion et n'ont rien à voir avec la position officielle de la société InterSystems.
0
0 107
Article Guillaume Rongier · Mars 29, 2022 9m read

Les globales, ces épées magiques destinées à stocker des données, existent depuis un certain temps, mais peu de gens savent les utiliser efficacement ou connaissent cette super-arme.

Si vous utilisez les globales pour des tâches où ils sont vraiment utiles, les résultats peuvent être étonnants, que ce soit en termes d'amélioration des performances ou de simplification spectaculaire de la solution globale (1, 2).

Les globales offrent une façon particulière de stocker et de traiter les données, qui est complètement différente des tableaux SQL. Ils ont été introduits en 1966 dans le langage de programmation M(UMPS), qui était initialement utilisé dans les bases de données médicales. Il est toujours utilisé de la même manière, mais a également été adopté par d'autres secteurs où la fiabilité et les hautes performances sont des priorités absolues : finance, commerce, etc.

Plus tard, M(UMPS) a évolué vers Caché ObjectScript (COS). COS a été développé par InterSystems comme un superset de M. Le langage original est toujours accepté par la communauté des développeurs et existe dans quelques implémentations. Il y a plusieurs signes d'activité sur le web : MUMPS Google group, Mumps User's group), effective ISO Standard, etc.

Les DBMS (systèmes de base de données) globales modernes prennent en charge les transactions, la journalisation, la réplication et le partitionnement. Cela signifie qu'ils peuvent être utilisés pour construire des systèmes distribués modernes, fiables et rapides.

Les globales ne vous limitent pas aux frontières du modèle relationnel. Ils vous donnent la liberté de créer des structures de données optimisées pour des tâches particulières. Pour de nombreuses applications, l'utilisation raisonnable des globales peut être une véritable solution miracle offrant des vitesses dont les développeurs d'applications relationnelles classiques ne peuvent que rêver.

Les globales, en tant que méthode de stockage des données, peuvent être utilisés dans de nombreux langages de programmation modernes, tant de haut niveau que de bas niveau. Par conséquent, cet article se concentrera spécifiquement sur les globales et non sur le langage dont ils sont issus.

2. Comment fonctionnent les globales

Commençons par comprendre comment fonctionnent les globales et quels sont leurs avantages. Les globales peuvent être vus sous différents angles. Dans cette partie de l'article, nous les verrons comme des arbres ou des stockages de données hiérarchiques.

En termes simples, une globale est une liste de données persistantes. Une liste qui est automatiquement sauvegardé sur le disque.

Il est difficile d'imaginer quelque chose de plus simple pour stocker des données. Dans le code du programme (écrit en langage COS/M), la seule différence avec un tableau associatif ordinaire est le symbole ^ qui précède leur nom.

Il n'est pas nécessaire de connaître SQL pour enregistrer des données dans une globale, car toutes les commandes nécessaires sont très simples et peuvent être apprises en une heure.

Commençons par l'exemple le plus simple, un arborescence mono niveau avec deux branches. Les exemples sont écrits en COS.

Set ^a("+7926X") = "John Sidorov"
Set ^a("+7916Y") = "Sergey Smith"

  Lorsque des données sont insérées dans une globale (la commande Set), 3 choses se produisent automatiquement:

  1. Sauvegarde des données sur le disque.
  2. Indexation. Ce qui est entre les parenthèses est un indice, ce qui est à droite du signe égal est la valeur du nœud.
  3. Tri. Les données sont triées par une clé. La prochaine traversée mettra "Sergey Smith" en première position, suivi de "John Sidorov". Lorsque l'on obtient une liste d'utilisateurs à partir d'une globale, la base de données ne passe pas de temps à trier. Vous pouvez en fait demander une liste triée à partir de n'importe quelle clé, même une clé inexistante (la sortie commencera à partir de la première clé réelle suivant celle-ci).

Toutes ces opérations sont effectuées à une vitesse incroyable. Sur mon système personnel (i5-3340, 16GB, HDD WD 1TB Blue), j'ai réussi à atteindre 1 050 000 insertions/sec en un seul processus. Sur les systèmes multi-cœurs, les vitesses peuvent atteindre des dizaines de millions of insertions/sec.

Bien sûr, la vitesse de saisie des données en elle-même ne dit pas grand-chose. Nous pouvons, par exemple, saisir des données dans des fichiers texte - c'est ainsi que le traitement fonctionne chez Visa, selon les rumeurs. Cependant, avec les globales, nous obtenons un stockage structuré et indexé avec lequel vous pouvez travailler en profitant de sa grande vitesse et de sa facilité d'utilisation.

  • La plus grande force des globales est la vitesse d'insertion de nouveaux nœuds dans ceux-ci.
  • Les données sont toujours indexées dans une globale. Les traversées en profondeur et les traversées arborescentes mono-niveau sont toujours très rapides.

Ajoutons quelques branches de deuxième et troisième niveau de la globale.

Set ^a("+7926X", "city") = "Moscow"
Set ^a("+7926X", "city", "street") = "Req Square"
Set ^a("+7926X", "age") = 25
Set ^a("+7916Y", "city") = "London"
Set ^a("+7916Y", "city", "street") = "Baker Street"
Set ^a("+7916Y", "age") = 36

Apparemment, vous pouvez construire des arbres à plusieurs niveaux en utilisant des globales. L'accès à n'importe quel nœud est presque instantané grâce à l'indexation automatique après chaque insertion. Les branches de l'arbre à n'importe quel niveau sont triées par une clé.

Comme vous pouvez le constater, les données peuvent être stockées à la fois sous forme de clés et de valeurs. La longueur combinée d'une clé (la somme des longueurs de tous les index) peut atteindre 511 octets et les valeurs peuvent atteindre une taille de 3,6 MB dans Caché. Le nombre de niveaux dans un arbre (nombre de dimensions) est plafonné à 31.

Une autre chose intéressante : vous pouvez construire un arbre sans définir les valeurs des nœuds de niveau supérieur.

Set ^b("a", "b", "c", "d") = 1
Set ^b("a", "b", "c", "e") = 2
Set ^b("a", "b", "f", "g") = 3

Les cercles vides sont des nœuds sans valeur.

Pour mieux comprendre les globales, comparons-les à d'autres arbres : les arbres de jardin et les arbres de noms de systèmes de fichiers.

Comparons les globales aux structures hiérarchiques les plus familières : les arbres réguliers qui poussent dans les jardins et les champs, ainsi que les systèmes de fichiers.

Comme nous pouvons le constater, les feuilles et les fruits ne poussent qu'à l'extrémité des branches des arbres ordinaires.
Systèmes de fichiers - les informations sont également stockées à l'extrémité des branches, également connues comme des noms de fichiers complets.

Et voici la structure de données d'une globale.

Differences:

  1. Nœuds internes: les informations d'une globale peuvent être stockées dans tous les nœuds et pas seulement aux extrémités des branches.
  2. Nœuds externes: les globales doivent avoir des extrémités de branche définies (extrémités avec des valeurs), ce qui n'est pas obligatoire pour les systèmes de fichiers et les arbres de jardin.

En ce qui concerne les noeuds internes, nous pouvons traiter la structure de globale comme un sur-ensemble des arbres de noms des systèmes de fichiers et des arbres de jardins. La structure de globale est donc plus flexible.

En général, une globale est un arbre structuré qui prend en charge la sauvegarde des données dans chaque nœud.

Afin de mieux comprendre le fonctionnement des globales, imaginons ce qui se passerait si les créateurs d'un système de fichiers utilisaient une approche identique à celle des globales pour stocker les informations ?

  1. Si le dernier fichier d'un dossier était supprimé, le dossier lui-même serait également supprimé, ainsi que tous les dossiers de niveau supérieur qui ne contenaient que ce dossier supprimé.
  2. Il n'y aurait pas besoin de dossiers du tout. Il y aurait des fichiers avec des sous-fichiers et des fichiers sans sous-fichiers. Si vous le comparez à un arbre normal, chaque branche deviendrait un fruit.


  3. Des choses comme README.txt ne seraient probablement plus nécessaires. Tout ce que vous avez besoin de dire sur le contenu d'un dossier pourrait être écrit dans le fichier du dossier lui-même. En général, les noms de fichiers ne peuvent pas être distingués des noms de dossiers (par exemple, /etc/readme peut être soit un dossier, soit un fichier), ce qui signifie que nous pourrions nous contenter d'exploiter des fichiers. 4. Les dossiers comportant des sous-dossiers et des fichiers pourraient être supprimés beaucoup plus rapidement. Il existe des articles sur le net qui racontent à quel point il est long et difficile de supprimer des millions de petits fichiers (1, 2, 3). En revanche, si vous créez un pseudo-système de fichiers basé sur une globale, cela prendra quelques secondes, voire des fractions de seconde. Lorsque j'ai testé la suppression de sous-arbres sur mon ordinateur personnel, j'ai réussi à supprimer 96 à 341 millions de nœuds d'un arbre à deux niveaux sur un disque dur (pas un disque dur externe). Et il convient de mentionner que nous parlons de la suppression d'une partie d'un arbre global, et non de la suppression d'un fichier entier contenant une globale.

La suppression des sous-arbres est encore un autre avantage des globaux : vous n'avez pas besoin de récursion pour cela. C'est incroyablement rapide. Dans notre arbre, cela pourrait être fait avec une commande Kill.

Kill ^a("+7926X")

Vous trouverez ci-dessous un petit tableau qui vous permettra de mieux comprendre les actions que vous pouvez effectuer sur une globale.

Commandes et fonctions clés liées aux globales dans COS
Set Paramétrage (initialisation) des branches jusqu'à un noeud (si non défini) et valeur du noeud
Merge Copie d'un sous-arbre
Kill Suppression d'un sous-arbre
ZKill Suppression de la valeur d'un nœud particulier. Le sous-arbre issu de ce noeud n'est pas affecté
$Query Traversée complète et approfondie de l'arbre
$Order Renvoie l'indice suivant au même niveau
$Data Vérifier si un nœud est défini
$Increment Incrémentation atomique de la valeur d'un nœud afin d'éviter la lecture et l'écriture pour ACID. La dernière recommandation est d'utiliser $Sequence à la place.

Merci de votre attention, je serai heureux de répondre à vos questions.

Démenti: Cet article reflète l'opinion privée de l'auteur et n'a aucun rapport avec la position officielle d'InterSystems.

0
0 133