#Indexation

0 Abonnés · 11 Publications

Comment indexer les structures de données dans les bases de données.

Article Guillaume Rongier · Sept 9, 2025 3m read

img

Ce court article est consacré aux méthodes dunder de Python, également appelées méthodes magiques.

Qu'est-ce que les méthodes Dunder?

Les méthodes Dunder sont des méthodes spéciales en Python qui commencent et se terminent par deux traits de soulignement (__). Elles vous permettent de définir le comportement de vos objets pour les opérations intégrées, telles que l'addition, la soustraction, la représentation sous forme de chaîne, etc.

Parmi les méthodes dunder courantes, on peut citer:

  • __init__(self, ...): Appelé lorsqu'un objet est créé.
    • Comme notre méthode %OnNew dans ObjectScript.
  • __str__(self): Appelée par la fonction intégrée str() et print pour représenter l'objet sous forme de chaîne.
  • __repr__(self): Appelée par la fonction intégrée repr() pour représenter l'objet à des fins de débogage.
  • __add__(self, other): Appelée lorsque l'opérateur + est utilisé.
  • __len__(self): Appelée par la fonction intégrée len() pour renvoyer la longueur de l'objet.
  • __getitem__(self, key): Appelée pour récupérer un élément d'une collection à l'aide de la syntaxe d'indiçage.
  • __setitem__(self, key, value): Appelée pour définir un élément dans une collection à l'aide de la syntaxe d'indiçage.
  • ... et bien d'autres encore.

Pourquoi les méthodes Dunder sont-elles importantes et pertinentes dans le contexte IRIS?

Dans ObjectScript, nous n'avons pas de sucre syntaxique comme en Python, mais nous pouvons obtenir un comportement similaire à l'aide des méthodes dunder.

Exemple : nous avons importé un module Python qui contient une fonction renvoyant une liste Python, et nous souhaitons l'utiliser dans ObjectScript. Nous devons utiliser la méthode dunder __getitem__ pour accéder aux éléments de la liste.

# src/python/article/dunder_example.py
def get_list():
    return [1, 2, 3, 4, 5]
Class Article.DunderExample Extends %RegisteredObject
{

ClassMethod Run()
{
    Set sys = ##class(%SYS.Python).Import("sys")
    do sys.path.append("/irisdev/app/src/python/article")
    set dunderExample = ##class(%SYS.Python).Import("dunder_example")
    set myList = dunderExample."get_list"()
    for i=0:1:myList."__len__"()-1 {
        write myList."__getitem__"(i), !
    }
}

}

Lançons-la:

iris session iris -U IRISAPP '##class(Article.DunderExample).Run()'

Le résultat sera le suivant:

1
2
3
4
5

Cela montre comment utiliser les méthodes dunder pour interagir avec des objets Python dans un contexte IRIS, ce qui vous permet de tirer parti des capacités de Python tout en travaillant dans l'environnement ObjectScript.

Supplément

Une bonne utilisation de dunder consisterait à placer à la fin de votre script Python un bloc if __name__ == " __main__ ": afin d'empêcher l'exécution du code lorsque le script est importé en tant que module.

Rappelez-vous, le premier article expliquait que lorsque vous importiez un script, le code y était exécuté. Ce bloc vous permet de définir du code qui ne doit s'exécuter que lorsque le script est exécuté directement, et non lorsqu'il est importé.

Exemple:

# src/python/article/dunder_example.py
def get_list():
    return [1, 2, 3, 4, 5]

if __name__ == "__main__":
    print(get_list())

Conclusion

Ce que vous pouvez faire en Python, même avec son sucre syntaxique, vous pouvez le faire en ObjectScript avec les méthodes dunder.

0
0 30
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
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 Lorenzo Scalese · Juil 29, 2024 9m read

Supposons que vous ayez une application qui permette aux utilisateurs d'écrire des articles et de les commenter. (Attendez...  ça me dit quelque chose...)

L'objectif est de répertorier, pour un utilisateur donné, tous les messages publiés avec lesquels il a interagi, c'est-à-dire dont il est l'auteur ou qu'il a commentés. Comment faites-vous cela aussi vite que possible?

Voici à quoi pourraient ressembler les définitions de notre classe %Persistent comme point de départ (les définitions de stockage sont importantes, mais omises par souci de concision):

Class DC.Demo.Post Extends%Persistent
{

Property Title As%String(MAXLEN = 255) [ Required ];Property Body As%String(MAXLEN = "") [ Required ];Property Author As DC.Demo.Usr [ Required ];Property Draft As%Boolean [ InitialExpression = 1, Required ]; }

Class DC.Demo.Usr Extends%Persistent {

Property Name As%String [ Required ]; }

Class DC.Demo.Comment Extends%Persistent {

Property Post As DC.Demo.Post [ Required ];Property Usr As DC.Demo.Usr [ Required ];Property Comment As%String(MAXLEN = "") [ Required ]; }

Et notre requête, comme point de départ:

selectIDfrom DC_Demo.Post where (Author = ? orIDin (selectdistinct Post from DC_Demo.Comment where Usr = ?)) and Draft = 0

L' approche naïve consisterait simplement à:

  • Ajoutez des indices bitmap sur Author et Draft dans DC.Demo.Post.
  • Ajoutez un index standard sur (Usr, Post) dans DC.Démo.Commentaire..

Et ce n'est pas du tout une mauvaise approche! Pour certains cas d'utilisation, elle peut même être " suffisante ". Que va faire IRIS SQL sous le capot ? Nous pouvons examiner le plan de requête:

 Générer un flux de valeurs idkey en utilisant la combinaison multi-index:
     ((bitmap index DC_Demo.Post.Draft) INTERSECT ((bitmap index DC_Demo.Post.Author) UNION (bitmap index DC_Demo.Post.Draft)))
 Pour chaque valeur d'idkey:
     Affichage de la ligne.

Sous-requête C:
 Lecture de la carte d'index DC_Demo.Comment.UsrPost, en utilisant l'Usr et le Post donnés, et en bouclant sur l'ID.
 Pour chaque ligne:
     Détermination du résultat de la sous-requête.

Ce n'est pas dramatique. Supposons qu'il y ait 50000 publications et que chaque utilisateur ait commenté 500 d'entre elles en moyenne. Combien de références globales cette requête implique-t-elle? Eh bien, au minimum, trois pour les index bitmap, et environ 500 dans la sous-requête (itération sur l'index UsrPost). Il est clair que la sous-requête est le goulot d'étranglement. Comment pouvons-nous la rendre plus rapide?

La réponse est d'utiliser un index fonctionnel (une sous-classe de %Library.FunctionalIndex) avec la %FIND condition de prédicat (et une sous-classe de %SQL.AbstractFind). Notre index fonctionnel sera défini dans la classe Comment (commentaires), mais ne contiendra pas les identifiants des commentaires comme le ferait un index bitmap classique. Au lieu de cela, pour chaque utilisateur, il aura un bitmap d'identifiants de Post pour lesquels cet utilisateur a au moins un commentaire. Nous pouvons ensuite combiner très efficacement cette image bitmap avec d'autres conditions indexées par image bitmap dans la table Post. Il est évident que cela entraîne une certaine surcharge pour l'insertion/mise à jour/suppression de nouveaux commentaires, mais l'avantage en termes de performances pour les lectures peut la compenser.

Un index fonctionnel doit définir le comportement de l'index pour les opérations d'insertion, de mise à jour et de suppression, et mettre en œuvre quelques autres méthodes (purge, début de tri, fin de tri). Une Une implémentation %SQL.AbstractFind doit mettre en œuvre des méthodes pour parcourir et récupérer des fragments d'index bitmap. Pour s'amuser, nous utiliserons une implémentation générique %SQL.AbstractFind qui examine une structure d'index bitmap standard (avec une référence globale à son nœud racine).

Remarque - si vous ne savez pas ce qu'est un "fragment de bitmap" ou si vous avez l'impression que tout cela est du Chinois, nous vous conseillons de lire la documentation  sur les index de bitmap, en particulier les parties relatives à leur structure et à leur manipulation.

Passons au code, DC.Demo.ExistenceIndex est notre index fonctionnel:

Include %IFInclude/// Données:/// /// <code>/// Class Demo.ClassC Extends %Persistent/// {/// Properiété PropA As Demo.ClassA;/// Properiété PropB As Demo.ClassB;/// Index BValuesForA On (PropA, PropB) As DC.Demo.ExistenceIndex;/// }/// </code>/// /// Appel à partir de SQL comme suit, étant donné une valeur de PropA de 21532, pour retourner les valeurs de PropB associées à PropA=21532 dans ClassB:/// <code>/// selectionner * de Demo.ClassC où ID %FIND Demo.ClassB_BValuesForAFind(21532) et <other-bitmap-index-conditions>/// </code>Class DC.Demo.ExistenceIndex Extends%Library.FunctionalIndex [ System = 3 ]
{

/// Retourne une sous-classe %SQL.AbstractFind appropriée pour cet index fonctionnelClassMethod Find(pSearch As%Binary) As%Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id")) Set name = $Replace(name,$$$QUOTE("id"),"pSearch") $$$GENERATE(" Quit ##class(DC.Demo.ReferenceFind).%New($Name("name"))") } }

/// Retrouve un "true" s'il existe un enregistrement avec (prop1val, prop2val).ClassMethod Exists(prop1val, prop2val) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set indexProp1 = $$$comSubMemberKeyGet(%class,$$$cCLASSindex,%property,$$$cINDEXproperty,1,$$$cINDEXPROPproperty) Set indexProp2 = $$$comSubMemberKeyGet(%class,$$$cCLASSindex,%property,$$$cINDEXproperty,2,$$$cINDEXPROPproperty) Set table = $$$comClassKeyGet(%class,$$$cCLASSsqlschemaname)"."$$$comClassKeyGet(%class,$$$cCLASSsqltablename) Set prop1 = $$$comMemberKeyGet(%class,$$$cCLASSproperty,indexProp1,$$$cPROPsqlfieldname) If (prop1 = "") { Set prop1 = indexProp1 } Set prop2 = $$$comMemberKeyGet(%class,$$$cCLASSproperty,indexProp2,$$$cPROPsqlfieldname) If (prop2 = "") { Set prop2 = indexProp2 } $$$GENERATE(" &sql(select top 1 1 from "table" where "prop1" = :prop1val and "prop2" = :prop2val)") $$$GENERATE(" Quit (SQLCODE = 0)") } }

/// Cette méthode est invoquée lorsqu'une nouvelle instance d'une classe est insérée dans la base de donnéesClassMethod InsertIndex(pID As%CacheString, pArg... As%Binary) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id","chunk")) Set name = $Replace(name,$$$QUOTE("chunk"),"chunk")

    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" If ($Get(pArg(1)) '= """") &amp;&amp; ($Get(pArg(2)) '= """") { "</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  $$$IFBITOFFPOS(pArg(2),chunk,position)"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  Set $Bit("</span>_<span class="hljs-built_in">$Replace</span>(name,<span class="hljs-built_in">$$$QUOTE</span>(<span class="hljs-string">"id"</span>),<span class="hljs-string">"pArg(1)"</span>)_<span class="hljs-string">",position) = 1"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" }"</span>)
}

}

/// Cette méthode est invoquée lorsqu'une instance existante d'une classe est mise à jour.ClassMethod UpdateIndex(pID As%CacheString, pArg... As%Binary) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id","chunk")) Set name = $Replace(name,$$$QUOTE("chunk"),"chunk") $$$GENERATE(" If ($Get(pArg(3)) '= """") && ($Get(pArg(4)) '= """") { ") $$$GENERATE(" $$$IFBITOFFPOS(pArg(4),chunk,position)") $$$GENERATE(" Set $Bit("$Replace(name,$$$QUOTE("id"),"pArg(3)")",position) = .."%property"Exists(pArg(3),pArg(4))") $$$GENERATE(" }")

    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" If ($Get(pArg(1)) '= """") &amp;&amp; ($Get(pArg(2)) '= """") { "</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  $$$IFBITOFFPOS(pArg(2),chunk,position)"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">"  Set $Bit("</span>_<span class="hljs-built_in">$Replace</span>(name,<span class="hljs-built_in">$$$QUOTE</span>(<span class="hljs-string">"id"</span>),<span class="hljs-string">"pArg(1)"</span>)_<span class="hljs-string">",position) = 1"</span>)
    <span class="hljs-built_in">$$$GENERATE</span>(<span class="hljs-string">" }"</span>)
}

}

/// Cette méthode est invoquée lorsqu'une instance existante d'une classe est supprimée.ClassMethod DeleteIndex(pID As%CacheString, pArg... As%Binary) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) Set name = $Name(@tIdxGlobal@("id","chunk")) Set name = $Replace(name,$$$QUOTE("chunk"),"chunk") $$$GENERATE(" If ($Get(pArg(1)) '= """") && ($Get(pArg(2)) '= """") { ") $$$GENERATE(" $$$IFBITOFFPOS(pArg(2),chunk,position)") $$$GENERATE(" Set $Bit("$Replace(name,$$$QUOTE("id"),"pArg(1)")",position) = .."%property"Exists(pArg(1),pArg(2))") $$$GENERATE(" }") } }

/// Méthode auxiliaire permettant d'obtenir la référence globale pour le stockage d'un index donné.ClassMethod IndexLocationForCompile(pClassName As%String, pIndexName As%String) As%String { Set tStorage = ##class(%Dictionary.ClassDefinition).%OpenId(pClassName).Storages.GetAt(1).IndexLocation Quit$Name(@tStorage@(pIndexName)) }

/// Purge l'indexClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) $$$GENERATE(" Kill " _ tIdxGlobal) } }

/// Appelle SortBegin avant les opérations de masseClassMethod SortBeginIndex() [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) // No-op$$$GENERATE(" Quit") } }

/// Appelle SortEnd après les opérations de masseClassMethod SortEndIndex(pCommit As%Integer = 1) [ CodeMode = generator, ServerOnly = 1 ] { If (%mode '= "method") { Set tIdxGlobal = ..IndexLocationForCompile(%class,%property) // No-op$$$GENERATE(" Quit") } }

}

DC.Demo.ReferenceFind est notre implémentation générique de %SQL.AbstractFind qui permet de visualiser un tableau de fragments de cartes bitmap:

/// Utility class to wrap use of %SQL.AbstractFind against a bitmap index global referenceClass DC.Demo.ReferenceFind Extends%SQL.AbstractFind [ System = 3 ]
{

/// Référence globale à itérer sur / prendre en compte pour les méthodes d'opération %SQL.AbstractFind %FINDProperty reference As%String [ Private ]; Method %OnNew(pReference As%String) As%Status [ Private, ServerOnly = 1 ] { Set..reference = pReference Quit$$$OK }

Method NextChunk(ByRef pChunk As%Integer = "") As%Binary { Set pChunk=$Order(@i%reference@(pChunk),1,tChunkData) While pChunk'="",$bitcount(tChunkData)=0 { Set pChunk=$Order(@i%reference@(pChunk),1,tChunkData) } Return$Get(tChunkData) }

Method PreviousChunk(ByRef pChunk As%Integer = "") As%Binary { Set pChunk=$Order(@i%reference@(pChunk),-1,tChunkData) While pChunk'="",$bitcount(tChunkData)=0 { Set pChunk=$Order(@i%reference@(pChunk),-1,tChunkData) } Return$Get(tChunkData) }

Method GetChunk(pChunk As%Integer) As%Binary { If$Data(@i%reference@(pChunk),tChunkData) { Return tChunkData } Else { Return"" } }

}

Ainsi, DC.Demo.Comment ressemble maintenant à ceci, avec deux indices bitmap ajoutés (et les clés externes appropriées pour faire bonne mesure).:

Class DC.Demo.Comment Extends%Persistent
{

Property Post As DC.Demo.Post [ Required ];Property Usr As DC.Demo.Usr [ Required ];Property Comment As%String(MAXLEN = "") [ Required ]; Index Usr On Usr [ Type = bitmap ]; Index Post On Post [ Type = bitmap ]; Index UserPosts On (Usr, Post) As DC.Demo.ExistenceIndex; ForeignKey UsrKey(Usr) References DC.Demo.Usr(); ForeignKey PostKey(Post) References DC.Demo.Post() [ OnDelete = cascade ]; }

Notre requête SQL devient alors:

selectIDfrom DC_Demo.Post where (Author = ? orID %FIND DC_Demo.Comment_UserPostsFind(?)) and Draft = 0

Et le plan de requête devient:

 Générer un flux de valeurs idkey en utilisant la combinaison multi-index:
     ((bitmap index DC_Demo.Post.Draft) INTERSECT ((bitmap index DC_Demo.Post.Author) UNION (given bitmap filter for DC_Demo.Post.%ID)))
 Pour chaque valeur d'idkey:
     Afficher la ligne.

Combien de références globales y a-t-il maintenant ? Une pour Author bitmap, une pour Draft bitmap, et une pour le nœud d'index de bitmap "posts for a given user" dans DC.Demo.Comment. Désormais, la liste "Quels sont les publications auxquelles j'ai participé?" ne sera pas (autant) ralentie si vous commentez de plus en plus!

Avertissement : malheureusement, la communauté des développeurs n'est pas réellement soutenue par InterSystems IRIS, vous ne devriez donc probablement pas faire autant de commentaires.

0
0 37
Annonce Irène Mykhailova · Mai 9, 2024

Bonjour la communauté IRIS,

Dans le cadre du développement d'un examen de certification pour les spécialistes SQL d'InterSystems IRIS, InterSystems Certification souhaite que vous participiez au test bêta de l'examen si vous correspondez à la description du candidat à l'examen présentée ci-dessous. L'examen sera disponible pour un test bêta du 9 au 12 juin 2024 lors du sommetInterSystems Global Summit 2024, mais uniquement pour les personnes inscrites au sommet (visitez cette page pour en savoir plus sur la Certification au GS24). Les tests bêta seront ouverts à tous les autres bêta-testeurs intéressés le 24 juin 2024. Toutefois, les bêta-testeurs intéressés peuvent s'inscrire dès maintenant en envoyant un courriel à certification@intersystems.com (veuillez nous indiquer si vous effectuerez le bêta-test au Sommet mondial ou dans notre environnement surveillé en ligne) Les tests bêta doivent être terminés au plus tard le 2 août 2024.

Quelles sont mes responsabilités en tant que bêta-testeur?

L'examen vous sera attribué et vous devrez le passer avant le 2 août 2024. L'examen sera organisé dans un environnement surveillé en ligne (surveillé en direct pendant le sommet), gratuitement (les frais standard de 150$ par examen sont supprimés pour tous les bêta-testeurs), puis l'équipe d'InterSystems Certification effectuera une analyse statistique minutieuse de toutes les données du test bêta afin de fixer un score de passage pour l'examen. L'analyse des résultats du test bêta prendra de 6 à 8 semaines, et une fois le score de passage établi, vous recevrez une notification par courriel d'InterSystems Certification vous informant des résultats. Si votre score à l'examen est égal ou supérieur au score de passage, vous aurez obtenu la certification! 

Remarque: Les scores des tests bêta sont entièrement confidentiels.

Détails de l'examen

Titre de l'examen: Spécialiste SQL d'InterSystems IRIS

Description du candidat: Un professionnel de l'informatique qui utilise le logiciel SQL d'InterSystems IRIS pour:< / p>

  • concevoir des applications SQL d'InterSystems IRIS,
  • gérer les opérations SQL d'InterSystems IRIS,
  • charger et interroger efficacement des ensembles de données dans le cadre de SQL d'InterSystems IRIS.

Nombre de questions: 48

Temps alloué pour passer l'examen: 2 heures

Préparation recommandée: Révisez le contenu du the tableau ci-dessous avant de participer à l'examen.

Expérience pratique recommandée:

  • Expérience de base avec la norme ANSI SQL
  • 1 à 2 ans d'expérience dans la conception et la gestion d'applications SQL d'InterSystems IRIS ou expertise dans d'autres plateformes SQL et 1 an d'expérience dans la gestion d'applications SQL d'IRIS.
  • Réviser la série de questions pratiques (disponible le 15 mai 2024)

Questions pratiques d'examen

Une série de questions pratiques sera disponible à partir du 15 mai 2024 (sur cette page) et est fournie pour familiariser les candidats avec les formats et les approches des questions.

Format de l'examen

Les questions sont présentées sous deux formes : les questions de choix multiple et les questions de réponse multiple. L'accès à la documentation d'InterSystems IRIS sera disponible pendant l'examen. 

< p>AVERTISSEMENT: Veuillez noter que cet examen est limité à 2 heures. La documentation d'InterSystems sera disponible pendant l'examen, mais les candidats n'auront pas le temps de la consulter pour chaque question. Par conséquent, il est indispensable d'effectuer la préparation recommandée avant de passer l'examen et de ne consulter la documentation qu'en cas de nécessité absolue au cours de l'examen!

Configuration requise pour les tests bêta (veuillez noter que des ordinateurs portables seront fournis et configurés pour répondre à ces exigences lors du sommet mondial)

  • Caméra et microphone fonctionnels
  • Processeur double cœur
  • Au moins 2 Go de mémoire RAM disponible
  • Au moins 500 Mo d'espace disque disponible
  • Vitesse minimale de l'internet:
    • Téléchargement- 500 Ko/s
    • Envoi- 500 Ko/s

Thèmes et contenu de l'examen

L'examen contient des questions qui couvrent les domaines du rôle indiqué, comme le montre le tableau des thèmes d'examen ci-dessous.

Thème

Sous-thème

Connaissances, compétences et aptitudes

1. Conception des applications SQL d'InterSystems IRIS

1. Conception d'un schéma SQL

  1. Identification du rôle de 'bitmap extent index' (index de l'étendue de la représentation en mode point)
  2. Détermination des cas d'utilisation des index
  3. Distinction entre les cas d'utilisation des différents types d'index​
  4. Distinction entre les clés primaires, les clés uniques et les clés d'identification
  5. Identification des propriétés pour les relations de clé étrangère
  6. Identification du comportement des actions référentielles pour la mise à jour et la suppression
  7. Identification des compromis pour ajouter toujours plus d'index
  8. Identification des cas d'utilisation des index composites

2. Conception des schémas avancés

  1. Distinction entre les types de données date/heure

3. Chargement des données

  1. Utilisation de la commande LOAD DATA
  2. Utilisation des tables de diagnostic SQL (%SQL_Diag)
  3. Identification des options d'exportation des données (physiques ou logiques)
 

4. Rédaction de la logique métier

  1. Définition des procédures stockées
  2. Définition des déclencheurs
  3. Identification des options linguistiques pour la rédaction de la logique métier
 

5. Développement d'applications objet/relationnelles

  1. Rappel du mappage objet/relationnel par défaut
  2. Rappel des bonnes pratiques SQL lors de la définition des classes
 

6. Déploiement des applications SQL

  1. Énumération des mécanismes de déploiement des applications SQL
  2. Détermination de la nature de ce qui doit faire partie d'un déploiement

2. Utilisation des systèmes SQL d'InterSystems

1. Gèstion du traitement des requêtes d'IRIS InterSystems

  1. Énumération des éléments pris en compte par l'optimiseur
  2. Distinction entre les erreurs de syntaxe et les erreurs d'exécution
  3. Utilisation de l'index "Statement Index" pour trouver les métadonnées des instructions.
  4. Distinction entre l'utilisation de paramètres et de constantes dans une requête

2. Interprètation des plans de requêtes

  1. Identification des façons d'afficher les plans de requête pour une instruction
  2. Identification des parcours complets des tables (Full Table Scans) dans un plan de requête
  3. Identification de l'utilisation des index dans un plan de requête
  4. Distinction entre les boucles et les recherches dans un plan de requête
  5. Distinction entre les modules invoqués une seule fois et ceux qui sont invoqués à plusieurs reprises dans un plan de requête
  6. Rappel du rôle des statistiques de table dans la planification des requêtes
  7. Utilisation des astuces pour la résolution des problèmes de planification des requêtes
  8. Identification des opportunités pour les index, sur la base d'un plan de requête
  9. Rappel de la signification du coût estimé

3. Utilisation des SQL d'InterSystems IRIS dans des applications

  1. Définition des connexions d'InterSystems IRIS
  2. Identification des étapes de préparation/exécution
  3. Rappel de la manière dont l'utilisation correcte des paramètres permet de se prémunir contre l'injection SQL
  4. Exploitation efficace des contrôles explicites des transactions

4. Utilisation des capacités SQL spécifiques à InterSystems IRIS

  1. Utilisation de SelectMode de manière appropriée
  2. Utilisation de la syntaxe des flèches pour les jointures implicites

5. Utilisation efficace des transactions

  1. Détermination des limites appropriées de la transaction
  2. Utilisation de CommitMode de manière appropriée
  3. Rappel de l'impact des transactions importantes ou de longue durée

2. Gèstion des opérations SQL d'InterSystems IRIS

1. Gèrstion des opérations SQL

  1. Utilisation de la vue de processus SQL pour surveiller l'activité SQL (2022.2+)
  2. Collection des statistiques de table
  3. Utilisation des statistiques d'exécution dans de l'index "Statement Index" pour déterminer des opportunités d'optimisation

2. Gèstion de la sécurité SQL

  1. Attribution des privilèges SQL aux utilisateurs et aux rôles
  2. Distinction entre la vérification des privilèges SQL et la sécurité des applications
  3. Identification de l'impact du lancement de la vérification SQL
 

3. Utilisation de PTools pour une analyse avancée des performances

  1. Distinction entre les mesures de performance "temps consacré", "références globales" et "commandes exécutées"

Est-ce que cela vous intéresse de participer ? Envoyez un courriel certification@intersystems.com maintenant!

0
0 50
Article Irène Mykhailova · Mai 27, 2022 3m read

Il est possible de construire (reconstruire) l'index pendant que des données sont enregistrées/supprimées, mais si vous construisez l'index pendant ce processus, il sera référencé pendant sa mise à jour, utilisez donc l'utilitaire dédié et procédez à la construction de l'index.

La procédure est la suivante.

  1. Masquez le nom d'index que vous prévoyez d'ajouter l'optimiseur de requête.
  2. Ajoutez la définition de l'index et effectuez la construction de l'index.
  3. Une fois la construction de l'index est terminée, publiez l'index ajouté dans l'optimiseur.

L'exemple d'exécution est le suivant.

* Dans l'exemple, l'index standard HomeStateIdx est défini pour la colonne Home_State (informations d'état de l'adresse de contact) de Sample.Person.

1. Masquez le nom d'index que vous prévoyez d'ajouter l'optimiseur de requête.
SAMPLES>write $system.SQL.SetMapSelectability("Sample.Person","HomeStateIdx",0)
1


2.Après avoir ajouté la définition d'index, reconstruisez-la.
  Exemple de définition: Index HomeStateIdx On Home.State;

SAMPLES>do ##class(Sample.Person).%BuildIndices($LB("HomeStateIdx"))


3. Une fois la construction de l'index est terminée, publiez l'index ajouté dans l'optimiseur.

SAMPLES>write $system.SQL.SetMapSelectability("Sample.Person","HomeStateIdx",1)
1

Reportez-vous au plan de requête pour voir si l'index a été utilisé/non utilisé.
Dans l'exemple suivant, le résultat de la confirmation du plan avec le terminal basculé sur l'environnement d'exécution SQL avec $system.SQL.Shell() s'affiche (lors du référencement dans le Management Portal, après avoir exécuté SQL sur l'écran d'exécution de la requête, cliquez sur le bouton "Affichage du plan").

SAMPLES>do $system.SQL.Shell()
SQL Command Line Shell
----------------------------------------------------
The command prefix is currently set to: <>.
Enter q to quit, ? for help.
SAMPLES>>select ID,Name from Sample.Person where Home_State='NY'
1.      select ID,Name from Sample.Person where Home_State='NY'
ID      Name
61      Alton,Debby O.
138     Isaksen,Charlotte L.
175     Walker,Emily O.
3 Rows(s) Affected
statement prepare time(s)/globals/lines/disk: 0.0026s/35/974/0ms
          execute time(s)/globals/lines/disk: 0.0017s/216/2447/0ms
                          cached query class: %sqlcq.SAMPLES.cls1
---------------------------------------------------------------------------
SAMPLES>>show plan    // ★ Affichage du plan lorsque l'index n'est pas utilisé DECLARE QRS CURSOR FOR SELECT ID , Name FROM Sample . Person WHERE Home_State = ?
Read master map Sample.Person.IDKEY, looping on ID.
For each row:
    Output the row.
SAMPLES>>show plan    // ★ Affichage du plan lors de l'utilisation de l'index DECLARE QRS CURSOR FOR SELECT ID , Name FROM Sample . Person WHERE Home_State = ?
Read index map Sample.Person.HomeStateIdx, using the given %SQLUPPER(Home_State), and looping on ID.
For each row:
    Read master map Sample.Person.IDKEY, using the given idkey value.
    Output the row.
SAMPLES>>

Pour plus de détails, veuillez consulter les documents suivants.
Building Indices on a READ and WRITE Active System【IRIS】

Building Indices on a READ and WRITE Active System

0
0 81
Article Lorenzo Scalese · Mai 18, 2022 14m read

La recherche d'images comme celle de Google est une fonctionnalité intéressante qui m'émerveille - comme presque tout ce qui est lié au traitement des images.

Il y a quelques mois, InterSystems a publié un aperçu de Python Embedded. Comme Python dispose de nombreuses librairies pour le traitement d'images, j'ai décidé de lancer ma propre tentative pour jouer avec une sorte de recherche d'images - une version beaucoup plus modeste en fait :-)


--- #### Un peu de théorie 🤓

Pour réaliser un système de recherche d'images, il faut d'abord sélectionner un ensemble de caractéristiques à extraire des images - ces caractéristiques sont également appelées descripteurs. L'étendue de chaque composante de ces descripteurs crée ce qu'on appelle un espace de caractéristiques, et chaque instance de cet espace est appelée un vecteur. Le nombre d de composantes nécessaires pour décrire les vecteurs, définit la dimension de l'espace des caractéristiques et des vecteurs, appelé d-dimensionnel.


Figure 1 - Un espace caractéristique tridimensionnel et un vecteur descripteur dans cet espace.
Credits: https://tinyurl.com/ddd76dln
---

Une fois l'ensemble des descripteurs définis, tout ce que vous avez à faire pour rechercher une image dans la base de données est d'extraire les mêmes descripteurs d'une image à rechercher et de les comparer aux descripteurs des images de la base de données - qui ont été précédemment extraits.

Dans ce travail, on a simplement utilisé la couleur dominante de l'image comme descripteur (j'ai dit que c'était une version modeste...). Comme une représentation RVB des couleurs a été utilisée, l'espace caractéristique est un espace tridimensionnel - 3d en abrégé. Chaque vecteur dans un tel espace a 3 composantes - (r,g,b), dans la gamme [0, 255].


Figure 2 - L'espace tridimensionnel des caractéristiques RVB
Credits: https://www.baslerweb.com/fp-1485687434/media/editorial/content_images/faqs/faq_RGB_1.gif
---

En traitement du signal, il est très fréquent d'avoir des espaces à n dimensions avec des valeurs de n bien supérieures à 3. En fait, vous pouvez combiner un grand nombre de descripteurs dans un même vecteur afin d'obtenir une meilleure précision. C'est ce qu'on appelle la sélection de caractéristiques et c'est une étape très importante dans les tâches de classification/reconnaissance.

Il est également courant de normaliser la plage de dimensions en [0, 1], mais pour des raisons de simplicité, ce travail utilise la plage par défaut [0, 255].

L'avantage de modéliser des caractéristiques sous forme de vecteurs est la possibilité de les comparer à travers des métriques de distance. Il existe de nombreuses distances, chacune ayant ses avantages et ses inconvénients, selon que l'on recherche la performance ou la précision. Dans ce travail, j'ai choisi des distances faciles à calculer - manhattan et chebyshev, qui sont essentiellement des différences absolues avec une précision raisonnable.


Figure 3 - Représentation de certains paramètres de distance
Credits: https://i0.wp.com/dataaspirant.com/wp-content/uploads/2015/04/cover_post_final.png
---

Index fonctionnel

Mais il ne s'agit que des outils nécessaires pour comparer les images en fonction de leur contenu. Si vous ne disposez pas d'un langage de requête comme SQL, vous vous retrouverez avec des méthodes et des paramètres de recherche fastidieux... De plus, en utilisant SQL, vous pouvez combiner cet index avec d'autres opérateurs bien connus, créant ainsi des requêtes complexes.

C'est ici où Functional Index d'InterSystems est très utile.

Un index fonctionnel est une classe qui implémente la classe abstraite %Library.FunctionalIndex qui implémente certaines méthodes afin de gérer la tâche d'indexation dans une instruction SQL. Ces méthodes traitent essentiellement les insertions, les suppressions et les mises à jour.

/// Indexation fonctionnelle permettant d'optimiser les requêtes sur les données d'image
Class dc.multimodel.ImageIndex.Index Extends %Library.FunctionalIndex [ System = 3 ]
{

/// Cardinalité de l'espace des caractéristiques
/// Comme cette classe est destinée à indexer l'image dans l'espace RVB, sa cardinalité est de 3
Paramètre Cardinalité = 3 ;

/// Cette méthode est invoquée lorsqu'une instance existante d'une classe est supprimée.
ClassMethod DeleteIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
	If (%mode '= "method") {
		$$$GENERATE("Set indexer = ##class(dc.multimodel.ImageIndex.Indexer).GetInstance("""_%class_""", """_%property_""")")
		$$$GENERATE("Set indexer.Cardinality = "_..#Cardinality)
		$$$GENERATE("Do indexer.Delete(pID, pArg...)")
	}
	Return $$$OK
}

ClassMethod Find(pSearch As %Binary) As %Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ]
{
	If (%mode '= "method") {
		$$$GENERATE("Set result = """"")
		$$$GENERATE("Set result = ##class(dc.multimodel.ImageIndex.SQLFind).%New()")
		$$$GENERATE("Set indexer = ##class(dc.multimodel.ImageIndex.Indexer).GetInstance("""_%class_""", """_%property_""")")
		$$$GENERATE("Set indexer.Cardinality = "_..#Cardinality)
		$$$GENERATE("Set result.Indexer = indexer")
		$$$GENERATE("Do result.PrepareFind(pSearch)")
		$$$GENERATE("Return result")
	}
	Return $$$OK
}

/// Cette méthode est invoquée lorsqu'une nouvelle instance d'une classe est insérée dans la base de données.
ClassMethod InsertIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
	If (%mode '= "method") {
		$$$GENERATE("Set indexer = ##class(dc.multimodel.ImageIndex.Indexer).GetInstance("""_%class_""", """_%property_""")")
		$$$GENERATE("Set indexer.Cardinality = "_..#Cardinality)
		$$$GENERATE("Do indexer.Insert(pID, pArg...)")
	}
	Return $$$OK
}

ClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ]
{
	If (%mode '= "method") {
		$$$GENERATE("Set indexer = ##class(dc.multimodel.ImageIndex.Indexer).GetInstance("""_%class_""", """_%property_""")")
		$$$GENERATE("Set indexer.Cardinality = "_..#Cardinality)
		$$$GENERATE("Set indexGbl = indexer.GetIndexLocation()")
		$$$GENERATE("Do indexer.Purge()")
	}
	Return $$$OK
}

/// Cette méthode est invoquée lorsqu'une instance existante d'une classe est mise à jour.
ClassMethod UpdateIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
	If (%mode '= "method") {
		$$$GENERATE("Set indexer = ##class(dc.multimodel.ImageIndex.Indexer).GetInstance("""_%class_""", """_%property_""")")
		$$$GENERATE("Set indexer.Cardinality = "_..#Cardinality)
		$$$GENERATE("Do indexer.Update(pID, pArg...)")
	}
	Return $$$OK
}

}

J'ai caché une partie du code d'implémentation pour des raisons de lisibilité ; vous pouvez consulter le code dans le lien OpenExchange.

Une autre classe abstraite doit être implémentée, c'est %SQL.AbstractFind, afin de rendre disponible l'utilisation de l'opérateur %FIND pour demander au moteur SQL d'utiliser votre index personnalisé.

Une explication beaucoup plus détaillée et conviviale des index fonctionnels est donnée par @alexander-koblov qui constitue également un excellent exemple d'index fonctionnel. Je vous recommande vivement de le lire.

Si vous souhaitez aller plus loin, vous pouvez jouer avec le code source des index %iFind et %UIMA index.

Dans ce travail, j'ai configuré une classe de test de persistance simple, où le chemin des images est stocké, et un index personnalisé pour la recherche d'images est défini pour ce champ.

Class dc.multimodel.ImageIndex.Test Extends %Persistent
{

Property Name As %String;

Property ImageFile As %String(MAXLEN = 1024);

Index idxName On Name [ Type = bitmap ];

Index idxImageFile On (ImageFile) As dc.multimodel.ImageIndex.Index;

Notez que idxImageFile est un index personnalisé (dc.multimodel.ImageIndex.Index) pour le champ Image (qui stocke le chemin de l'image).

Le tour de Python (et COS) !

Ainsi, les classes abstraites d'index fonctionnel vous donneront les points d'entrée où vous pourrez effectuer l'extraction de caractéristiques et la recherche lors de l'exécution des instructions SQL. Maintenant, c'est au tour de Python !

Vous pouvez importer et exécuter le code Python dans un contexte COS en utilisant Python intégré. Par exemple, pour extraire la couleur dominante d'une image :

Method GetDominantColorRGB(pFile As %String, ByRef pVector) As %Status
{
  Set sc = $$$OK
  Try {
    Set json = ##class(%SYS.Python).Import("json")
    Set fastcolorthief = ##class(%SYS.Python).Import("fast_colorthief")
    Set imagepath = pFile
    Set dominantcolor = fastcolorthief."get_dominant_color"(imagepath, 1)
    Set vector = {}.%FromJSON(json.dumps(dominantcolor))
    Set n = ..Cardinality - 1
    For i = 0:1:n {
      Set pVector(i) = vector.%Get(i)
    }
  } Catch(e) {
    Set sc = e.AsStatus()
  }
  Return sc
}

Dans cette méthode, deux librairies Python sont importées (json et fast_colorthief). La librairie fast_colorthief renvoie une représentation Python de type tableau 3-d avec les valeurs de RGB ; l'autre librairie - json, sérialise ce tableau dans un %DynamicArray.

La couleur dominante est extraite pour chaque enregistrement qui est inséré ou mis à jour - une fois que l'index fonctionnel lève les appels aux méthodes InsertIndex et UpdateIndex en réponse aux insertions et mises à jour dans le tableau. Ces caractéristiques sont stockées dans l'index global du tableau :

Method Insert(pID As %CacheString, pArgs... As %Binary)
{
	// pArgs(1) has the image path
	$$$ThrowOnError(..GetDominantColor(pArgs(1), .rgb))
	Set idxGbl = ..GetIndexLocation()
	Set @idxGbl@("model", pID) = ""
  	Merge @idxGbl@("model", pID, "rgb") = rgb
  	Set @idxGbl@("last-modification") = $ZTIMESTAMP
}

Method Update(pID As %CacheString, pArg... As %Binary)
{
	// pArgs(1) has the image path
  	Set idxGbl = ..GetIndexLocation()
  	Do ..GetDominantColor(pArg(1), .rgb)
  	Kill @idxGbl@("model", pID)
  	Set @idxGbl@("model", pID) = ""
  	Merge @idxGbl@("model", pID, "rgb") = rgb
  	Set @idxGbl@("last-modification") = $ZTIMESTAMP
}

De la même manière, lorsque des enregistrements sont supprimés, l'index fonctionnel lance des appels aux méthodes DeleteIndex et PurgeIndex. À leur tour, les fonctionnalités doivent être supprimées de l'index global du tableau :

Method Delete(pID As %CacheString, pArg... As %Binary)
{
  	Set idxGbl = ..GetIndexLocation()
  	Kill @idxGbl@("model", pID)
  	Set @idxGbl@("last-modification") = $ZTIMESTAMP
}

Method Purge(pID As %CacheString, pArg... As %Binary)
{
  	Set idxGbl = ..GetIndexLocation()
  	Kill @idxGbl
  	Set @idxGbl@("last-modification") = $ZTIMESTAMP
}

L'index global est récupéré par introspection dans la classe persistante :

Method GetIndexLocation() As %String
{
	Set storage = ##class(%Dictionary.ClassDefinition).%OpenId(..ClassName).Storages.GetAt(1).IndexLocation
	Return $NAME(@storage@(..IndexName))
}

Lorsque les utilisateurs utilisent l'index dans les clauses WHERE, la méthode Find() est activée par l'index de la fonction. Les instructions de la requête sont transmises afin que vous puissiez les analyser et décider de ce qu'il faut faire. Dans ce travail, les paramètres sont sérialisés en JSON afin de faciliter leur analyse. Les paramètres de la requête ont la structure suivante :

SELECT ImageFile
FROM dc_multimodel_ImageIndex.Test
WHERE ID %FIND search_index(idxImageFile, '{"color_similarity":{"image":"/data/img/test/161074693598711.jpg","first":5,"strategy":"knn"}}')

Dans cette instruction, vous pouvez voir l'utilisation de l'opérateur %FIND et de la fonction search_index. C'est ainsi que SQL accède à notre index personnalisé.

Les paramètres de search_index définissent l'index à rechercher - idxImageFile, dans ce cas ; et la valeur à envoyer à l'index. Ici, l'index attend un objet JSON, avec une configuration d'objet définissant : (i) le chemin de l'image, (ii) une limite pour les résultats, et (iii) une stratégie de recherche.

Une stratégie de recherche est simplement l'algorithme à utiliser pour effectuer la recherche. Actuellement, deux stratégies sont mises en œuvre : (i) fullscan et (ii) knn, qui correspond à k-proches voisins.

La stratégie fullscan consiste simplement en une recherche exhaustive mesurant la distance entre l'image recherchée et chaque image stockée dans la base de données.

Method FullScanFindStrategy(ByRef pSearchVector, ByRef pResult) As %Status
{
	Set sc = $$$OK
	Try {
		Set idxGbl = ..Indexer.GetIndexLocation()
		Set rankGbl = ..Indexer.GetRankLocation()

		Set id = $ORDER(@idxGbl@("model", ""))
		While (id '= "") {
			If ($ISVALIDNUM(id)) {
				Merge vector = @idxGbl@("model", id, "rgb")
				Set distance = ..Indexer.GetL1Distance(.pSearchVector, .vector)
				Set result(distance, id) = ""
			}
			Set id = $ORDER(@idxGbl@("model", id))
		}

		Kill @rankGbl@(..ImagePath, ..FindStrategy)
		If (..First '= "") {
			Set c = 0
			Set distance = $ORDER(result(""))
			While (distance '= "") && (c < ..First) {
				Merge resultTmp(distance) = result(distance)

				Set id = $ORDER(result(distance, ""))
				While (id '= "") {
					Set @rankGbl@(..ImagePath, ..FindStrategy, id) = distance
					Set id = $ORDER(result(distance, id))
				}

				Set c = c + 1
				Set distance = $ORDER(result(distance))
			}
			Kill result
			Merge result = resultTmp
		}

		Merge pResult = result
	}
	Catch ex {
		Set sc = ex.AsStatus()
	}
	Return sc
}

La stratégie KNN utilise une approche plus sophistiquée. Elle utilise une librairie Python pour créer une structure arborescente appelée Ball Tree. Une telle arborescence convient à une recherche efficace dans un espace à n dimensions.

Method KNNFindStrategy(ByRef pSearchVector, ByRef pResult) As %Status
{
	Do ..Log(" ------ KNNFindStrategy ------ ")
	Set sc = $$$OK
	Try {
		Set idxGbl = ..Indexer.GetIndexLocation()
		Set rankGbl = ..Indexer.GetRankLocation()

		Set json = ##class(%SYS.Python).Import("json")
		Set knn = ##class(%SYS.Python).Import("knn")

		Set first = ..First
		Set k = $GET(first, 5)

		Set n = ..Indexer.Cardinality - 1
		Set x = ""
		For i = 0:1:n {
			Set $LIST(x, * + 1) = pSearchVector(i)
		}
		Set x = "[["_$LISTTOSTRING(x, ",")_"]]"

		$$$ThrowOnError(..CreateOrUpdateKNNIndex())
		Set ind = knn.query(x, k, idxGbl)
		Set ind = {}.%FromJSON(json.dumps(ind.tolist()))
		Set ind = ind.%Get(0)

		Kill result
		Kill @rankGbl@(..ImagePath, ..FindStrategy)
		Set n = k - 1
		For i=0:1:n {
			Set id = ind.%Get(i)
			Set result(i, id) = ""
			Set @rankGbl@(..ImagePath, ..FindStrategy, id) = i
		}
		Merge pResult = result
	}
	Catch ex {
		Set sc = ex.AsStatus()
	}
	Return sc
}

Le code Python pour générer une arborescence Ball Tree est présenté ci-dessous :

from sklearn.neighbors import BallTree
import numpy as np
import pickle
import base64
import irisnative

def get_iris():
  ip = "127.0.0.1"
  port = 1972
  namespace = "USER"
  username = "superuser"
  password = "SYS"

  connection = irisnative.createConnection(ip,port,namespace,username,password)
  dbnative = irisnative.createIris(connection)

  return (connection, dbnative)

def release_iris(connection):
  connection.close()

def normalize_filename(filename):
  filename = filename.encode('UTF-8')
  return base64.urlsafe_b64encode(filename).decode('UTF-8')

def create_index(index_global, cardinality):
  connection, dbnative = get_iris()
  X = get_data(dbnative, index_global, cardinality)
  tree = BallTree(X, metric = "chebyshev")
  filename = f"/tmp/${normalize_filename(index_global)}.p"
  pickle.dump(tree, open(filename, "wb"))
  release_iris(connection)
  return tree

def get_data(dbnative, index_global, cardinality):
  X = []
  iter_ = dbnative.iterator(index_global, "model")
  for subscript, value in iter_.items():
    id_ = subscript
    v = []
    for i in range(cardinality):
      v.append(
        dbnative.get(index_global, "model", id_, "rgb", i) / 255
      )
    X.append(v)
  return X

def query(x, k, index_global):
  filename = f"/tmp/${normalize_filename(index_global)}.p"
  tree = pickle.load(open(filename, "rb"))
  x = eval(x)
  x_ = [xi / 255 for xi in x[0]]
  dist, ind = tree.query([x_], k)
  return ind

Lorsqu'une image est recherchée, l'index personnalisé appelle la méthode de requête de l'objet Ball Tree en Python. Vous pouvez également noter l'utilisation de l'API native d'IRIS afin d'accéder aux valeurs RVB globales de l'index pour la construction de l'arborescence Ball Tree.

Pour ordonner les images par similarité, il a été développé une procédure SQL qui traverse une globale stockant les distances précédemment calculées pour chaque image recherchée :

Method DiffRank(pSearch As %Binary, pId As %String) As %Float
{
	Set search = {}.%FromJSON(pSearch)
	If (search.%IsDefined("color_similarity")) {
		Set config = search.%Get("color_similarity")
		Set imagePath = config.%Get("image")
		If (config.%IsDefined("strategy")) {
			Set findStrategy = config.%Get("strategy")
		}
		Set rankGbl = ..Indexer.GetRankLocation()
		Set rank = $GET(@rankGbl@(imagePath, findStrategy, pId))
		Return rank
	}
	Return ""
}

Vous pouvez donc modifier l'instruction SQL pour classer les résultats par similarité :

SELECT ImageFile, dc_multimodel_ImageIndex.Test_idxImageFileDiffRank('{"color_similarity":{"image":"/data/img/test/161074693598711.jpg","first":5,"strategy":"knn"}}', id) AS DiffRank
FROM dc_multimodel_ImageIndex.Test
WHERE ID %FIND search_index(idxImageFile, '{"color_similarity":{"image":"/data/img/test/161074693598711.jpg","first":5,"strategy":"knn"}}')
ORDER BY DiffRank

Conclusion

L'objectif de ce travail était de montrer comment combiner la définition d'index fonctionnels dans COS avec des appels au code Python utilisant leurs étonnantes bibliothèques. De plus, en utilisant cette technique, vous pouvez accéder à des fonctionnalités complexes fournies par les librairies Python dans des instructions SQL, ce qui vous permet d'ajouter de nouvelles fonctionnalités à vos applications.

0
0 93
Article Lorenzo Scalese · Mai 16, 2022 11m read

Les modèles de données objet et relationnel de la base de données Caché supportent trois types d'index, à savoir standard, bitmap et bitslice. En plus de ces trois types natifs, les développeurs peuvent déclarer leurs propres types d'index personnalisés et les utiliser dans toutes les classes depuis la version 2013.1. Par exemple, les index de texte iFind utilisent ce mécanisme.

Un Custom Index Type est une classe qui implémente les méthodes de l'interface %Library.FunctionalIndex pour effectuer des insertions, des mises à jour et des suppressions. Vous pouvez spécifier une telle classe comme type d'index lorsque vous déclarez un nouvel index.

Exemple:

Property A As %String;
Property B As %String;
Index someind On (A,B) As CustomPackage.CustomIndex;

La classe CustomPackage.CustomIndex est la classe même qui implémente les index personnalisés.

Par exemple, analysons le petit prototype d'un index à base de quadtrees pour les données spatiales qui a été développé pendant le Hackathon par notre équipe : Andrey Rechitsky, Aleksander Pogrebnikov et moi-même. (Le Hackathon a été organisé dans le cadre de la formation annuelle de l'école d'innovation d'InterSystems Russie, et nous remercions tout particulièrement le principal inspirateur du Hackathon, Timur Safin.)

Dans cet article, je ne vais pas parler des [quadtrees] (https://en.wikipedia.org/wiki/Quadtree) et de la façon de les utiliser. Nous allons plutôt examiner comment créer une nouvelle classe qui implémente l'interface %Library.FunctionalIndex pour l'implémentation de l'algorithme quadtree existant. Dans notre équipe, cette tâche a été confiée à Andrey. Andrey a créé la classe SpatialIndex.Indexer avec deux méthodes :

  • Insert(x, y, id)
  • Delete(x, y, id)

Lors de la création d'une nouvelle instance de la classe SpatialIndex.Indexer, il était nécessaire de définir un nom de nœud global dans lequel nous stockons les données d'index. Tout ce que j'avais à faire était de créer la classe SpatialIndex.Index avec les méthodes InsertIndex, UpdateIndex, DeleteIndex et PurgeIndex. Les trois premières méthodes acceptent l'Id de la chaîne à modifier et les valeurs indexées exactement dans le même ordre que celui dans lequel elles ont été définies dans la déclaration de l'index au sein de la classe correspondante. Dans notre exemple, les arguments d'entrée sont pArg(1)A and pArg(2)B.

Class SpatialIndex.Index Extends %Library.FunctionalIndex [ System = 3 ]
{

ClassMethod InsertIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
    if %mode'="method" {
        set IndexGlobal = ..IndexLocation(%class,%property)
        $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))")
        $$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)")
    }
}

ClassMethod UpdateIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
    if %mode'="method" {
        set IndexGlobal = ..IndexLocation(%class,%property)
        $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))")
        $$$GENERATE($C(9)_"do indexer.Delete(pArg(3),pArg(4),pID)")
        $$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)")
    }
}
ClassMethod DeleteIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ]
{
    if %mode'="method" {
        set IndexGlobal = ..IndexLocation(%class,%property)
        $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))")
        $$$GENERATE($C(9)_"do indexer.Delete(pArg(1),pArg(2),pID)")
    }
}

ClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ]
{
    if %mode'="method" {
        set IndexGlobal = ..IndexLocation(%class,%property)
        $$$GENERATE($C(9)_"kill " _ IndexGlobal)
    }
}

ClassMethod IndexLocation(className As %String, indexName As %String) As %String
{
    set storage = ##class(%Dictionary.ClassDefinition).%OpenId(className).Storages.GetAt(1).IndexLocation
    quit $Name(@storage@(indexName))
}

}

IndexLocation est une méthode supplémentaire qui renvoie le nom du nœud dans le global où la valeur de l'index est enregistrée.

Analysons maintenant la classe de test dans laquelle l'index du type SpatialIndex.Index est utilisé :

Class SpatialIndex.Test Extends %Persistent
{
  Property Name As %String(MAXLEN = 300);
  Property Latitude As %String;
  Property Longitude As %String;
  Index coord On (Latitude, Longitude) As SpatialIndex.Index;
}

Lorsque la classe SpatialIndex.Test est compilée, le système génère les méthodes suivantes dans le code INT pour chaque index du type SpatialIndex.Index :

zcoordInsertIndex(pID,pArg...) public {
    set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))
    do indexer.Insert(pArg(1),pArg(2),pID) }
zcoordPurgeIndex() public {
    kill ^SpatialIndex.TestI("coord") }
zcoordSegmentInsert(pIndexBuffer,pID,pArg...) public {
    do ..coordInsertIndex(pID, pArg...) }
zcoordUpdateIndex(pID,pArg...) public {
    set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord")))
    do indexer.Delete(pArg(3),pArg(4),pID)
    do indexer.Insert(pArg(1),pArg(2),pID)
}

Les méthodes %SaveData, %DeleteData, %SQLInsert, %SQLUpdate et %SQLDelete appellent les méthodes de notre index. Par exemple, le code suivant fait partie de la méthode %SaveData :

if insert {
     ...
     do ..coordInsertIndex(id,i%Latitude,i%Longitude,"")
      ...
 } else {
      ...
     do ..coordUpdateIndex(id,i%Latitude,i%Longitude,zzc27v3,zzc27v2,"")
      ...
 }

Un exemple pratique est toujours mieux que la théorie, vous pouvez donc télécharger les fichiers depuis notre entrepôt : https://github.com/intersystems-ru/spatialindex/tree/no-web-interface. Ceci est un lien vers une branche sans l'interface web. Pour utiliser ce code :

  1. Importez les classes
  2. Décompresser RuCut.zip
  3. Importez les données en utilisant les appels suivants :
    do $system.OBJ.LoadDir("c:\temp\spatialindex","ck")
    do ##class(SpatialIndex.Test).load("c:\temp\rucut.txt")
    

Le fichier rucut.txt contient des données sur 100 000 villes et villages de Russie, avec leur nom et leurs coordonnées. La méthode Load lit chaque chaîne de caractères du fichier, puis l'enregistre comme une instance distincte de la classe SpatialIndex.Test. Une fois la méthode Load exécutée, le fichier global ^SpatialIndex.TestI("coord") contient un quadtree avec les coordonnées de latitude et de longitude.

Et maintenant, exécutons des requêtes !

La construction des index n'est pas la partie la plus intéressante. Nous voulons utiliser notre index dans diverses requêtes. Dans Caché, il existe une syntaxe standard pour les index non standard :

SELECT *
FROM SpatialIndex.Test
WHERE %ID %FIND search_index(coord, 'window', 'minx=56,miny=56,maxx=57,maxy=57')

%ID %FIND search_index est la partie fixe de la syntaxe. Ensuite, il y a le nom de l'index, coord - et notez qu'aucun guillemet n'est nécessaire. Tous les autres paramètres ('window', 'minx=56,miny=56,maxx=57,maxy=57') sont transmis à la méthode Find, qui doit également être définie dans la classe du type d'index (qui, dans notre exemple, est SpatialIndex.Index) :

ClassMethod Find(queryType As %Binary, queryParams As %String) As %Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ]
{
    if %mode'="method" {
        set IndexGlobal = ..IndexLocation(%class,%property)
        set IndexGlobalQ = $$$QUOTE(IndexGlobal)
        $$$GENERATE($C(9)_"set result = ##class(SpatialIndex.SQLResult).%New()")
        $$$GENERATE($C(9)_"do result.PrepareFind($Name("_IndexGlobal_"), queryType, queryParams)")
        $$$GENERATE($C(9)_"quit result")
    }
}

Dans cet exemple de code, nous avons seulement deux paramètres - queryType et queryParams, mais vous pouvez ajouter autant de paramètres que vous le souhaitez.

Lorsque vous compilez une classe dans laquelle la méthode SpatialIndex.Index est utilisée, la méthode Find génère une méthode supplémentaire appelée z<IndexName>Find, qui est ensuite utilisée pour exécuter des requêtes SQL :

zcoordFind(queryType,queryParams) public { Set:'$isobject($get(%sqlcontext)) %sqlcontext=##class(%Library.ProcedureContext).%New()
    set result = ##class(SpatialIndex.SQLResult).%New()
    do result.PrepareFind($Name(^SpatialIndex.TestI("coord")), queryType, queryParams)
    quit result }

La méthode Find doit retourner une instance de la classe qui implémente l'interface %SQL.AbstractFind. Les méthodes de cette interface, NextChunk et PreviousChunk, renvoient des chaînes de bits par tranches de 64 000 bits chacune. Lorsqu'un enregistrement avec un certain ID répond aux critères de sélection, le bit correspondant (chunk_number * 64000 + position_number_within_chunk) est mis à 1.

Class SpatialIndex.SQLResult Extends %SQL.AbstractFind
{

Property ResultBits [ MultiDimensional, Private ];

Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
    kill i%ResultBits
    kill qHandle
    quit $$$OK
}


Method PrepareFind(indexGlobal As %String, queryType As %String, queryParams As %Binary) As %Status
{
    if queryType = "window" {
        for i = 1:1:4 {
            set item = $Piece(queryParams, ",", i)
            set IndexGlobal = ..IndexLocation(%class,%property)
            $$$GENERATE($C(9)_"kill " _ IndexGlobal)   set param = $Piece(item, "=", 1)
            set value = $Piece(item, "=" ,2)
            set arg(param) = value
        }
        set qHandle("indexGlobal") = indexGlobal
        do ##class(SpatialIndex.QueryExecutor).InternalFindWindow(.qHandle,arg("minx"),arg("miny"),arg("maxx"),arg("maxy"))
        set id = ""
        for  {
            set id = $O(qHandle("data", id),1,idd)
            quit:id=""
            set tChunk = (idd\64000)+1, tPos=(idd#64000)+1
            set $BIT(i%ResultBits(tChunk),tPos) = 1
        }
    }
    quit $$$OK
}

Method ContainsItem(pItem As %String) As %Boolean
{
    set tChunk = (pItem\64000)+1, tPos=(pItem#64000)+1
    quit $bit($get(i%ResultBits(tChunk)),tPos)
}

Method GetChunk(pChunk As %Integer) As %Binary
{
    quit $get(i%ResultBits(pChunk))
}

Method NextChunk(ByRef pChunk As %Integer = "") As %Binary
{
    set pChunk = $order(i%ResultBits(pChunk),1,tBits)
    quit:pChunk="" ""
    quit tBits
}

Method PreviousChunk(ByRef pChunk As %Integer = "") As %Binary
{
    set pChunk = $order(i%ResultBits(pChunk),-1,tBits)
    quit:pChunk="" ""
    quit tBits
}
}

Comme le montre l'exemple de code ci-dessus, la méthode InternalFindWindow de la classe SpatialIndex.QueryExecutor recherche les points situés dans le rectangle spécifié. Ensuite, les ID des lignes correspondantes sont écrits dans les bitsets dans la boucle FOR.

Dans notre projet Hackathon, Andrey a également implémenté la fonctionnalité de recherche pour les ellipses :

SELECT *
FROM SpatialIndex.Test
WHERE %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2')
and name %StartsWith 'Z'

Un peu plus à propos de %FIND

Le prédicat %FIND possède un paramètre supplémentaire, SIZE, qui aide le moteur SQL à estimer le nombre de lignes correspondantes. En fonction de ce paramètre, le moteur SQL décide d'utiliser ou non l'index spécifié dans le prédicat %FIND.

Par exemple, ajoutons l'index suivant dans la classe SpatialIndex.Test :

Index ByName on Name;

Maintenant, recompilons la classe et construisons cet index :

write ##class(SpatialIndex.Test).%BuildIndices($LB("ByName"))

Et enfin, lancez TuneTable :

do $system.SQL.TuneTable("SpatialIndex.Test", 1)

Voici le plan de la requête :

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((10))

Comme l'index coord est susceptible de retourner peu de lignes, le moteur SQL n'utilise pas l'index sur la propriété Name.

Il y a un plan différent pour la requête suivante :

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((1000))

Le moteur SQL utilise les deux index pour exécuter cette requête.

Et, comme dernier exemple, créons une requête qui utilise uniquement l'index sur le champ Name, puisque l'index coord renverra probablement environ 100 000 lignes et sera donc très peu utilisable :

SELECT *
FROM SpatialIndex.Test
WHERE name %startswith 'za'
and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((100000))

Merci à tous ceux qui ont lu ou au moins parcouru cet article.

Outre les liens de documentation ci-dessous, vous pouvez également trouver utile d'examiner les implémentations alternatives des interfaces %Library.FunctionalIndex et %SQL.AbstractFind. Pour visualiser ces implémentations, ouvrez l'une de ces classes dans Caché Studio et choisissez Class > Inherited Classes dans le menu.

Liens:

0
0 115
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 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