#Bases de données

0 Abonnés · 34 Publications

La base de données InterSystems Caché est un fichier où sont stockés l'ensemble des données, des scripts d'application, des utilisateurs, des rôles et des configurations de sécurité. En général, le nom du fichier est cache.dat.

Documentation.

Article Lorenzo Scalese · Mai 30, 2022 9m read

Introduction

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

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

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

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

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

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

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

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

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

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

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

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

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

Approche EAV

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

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

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

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

La première table est Сatalog:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

SELECT * FROM Catalog ORDER BY id;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Globales : Une alternative à EAV

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

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

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

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

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

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

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

Quelle est la suite ?

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

0
0 1036
Article 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 Guillaume Rongier · Avr 13, 2022 7m read

Ce texte est la suite de mon article où j'ai expliqué la structure d'une base de données Caché. Dans cet article, j'ai décrit les types de blocs, les connexions entre eux et leur relation avec les globales. L'article est purement théorique. J'ai fait un projet qui aide à visualiser l'arbre des blocs - et cet article explique comment il fonctionne en détail.

Pour les besoins de la démonstration, j'ai créé une nouvelle base de données et l'ai débarrassée des globales que Caché initialise par défaut pour toutes les nouvelles bases de données. Créons une globale simple :
set ^colors(1)="red"
 set ^colors(2)="blue"
 set ^colors(3)="green"
​ set ^colors(4)="yellow"

Notez l'image illustrant les blocs du global créé. Celui-ci est simple, c'est pourquoi nous voyons sa description dans le bloc de type 9 (bloc catalogue des globales). Il est suivi par le bloc "pointeur supérieur et inférieur" (type 70), car l'arbre des globales n'est pas encore profond, et vous pouvez utiliser un pointeur vers un bloc de données qui tient encore dans un seul bloc de 8 Ko.

Maintenant, écrivons tant de valeurs dans une autre globale qu'elles ne peuvent pas être placées dans un seul bloc - et nous verrons de nouveaux nœuds dans le bloc de pointeurs pointant vers de nouveaux blocs de données qui ne pouvaient pas être placés dans le premier.

Écrivons 50 valeurs, de 1000 caractères chacune. Rappelez-vous que la taille du bloc dans notre base de données est de 8192 octets.

   set str=""
   for i=1:1:1000 {
       set str=str_"1"
   }
   for i=1:1:50 {
       set ^test(i)=str
   }
​   quit

Regardez l'image suivante :

Nous avons plusieurs nœuds au niveau du bloc de pointeurs pointant vers des blocs de données. Chaque bloc de données contient des pointeurs vers le bloc suivant ("lien correct"). Offset - pointe vers le nombre d'octets occupés dans ce bloc de données.

Essayons de simuler une division de bloc. Ajoutons tellement de valeurs au bloc que la taille totale du bloc dépasse 8 Ko, ce qui provoquera la division du bloc en deux.

Exemple de code

   set str=""
   for i=1:1:1000 {
       set str=str_"1"
   }
   set ^test(3,1)=str
   set ^test(3,2)=str
​   set ^test(3,3)=str

Le résultat est présenté ci-dessous :

Le bloc 50 est divisé et rempli de nouvelles données. Les valeurs remplacées se trouvent maintenant dans le bloc 58 et un pointeur vers ce bloc apparaît maintenant dans le bloc des pointeurs. Les autres blocs sont restés inchangés.

Un exemple avec de longues chaînes de caractères

Si nous utilisons des chaînes plus longues que 8 Ko (la taille du bloc de données), nous obtiendrons des blocs de "données longues". Nous pouvons simuler une telle situation en écrivant des chaînes de caractères de 10000 octets, par exemple.

Exemple de code

   set str=""
   for i=1:1:10000 {
       set str=str_"1"
   }
   for i=1:1:50 {
       set ^test(i)=str
​   }

Voyons le résultat :

En conséquence, la structure des blocs de l'image est restée la même, puisque nous n'avons pas ajouté de nouveaux nœuds globaux, mais seulement modifié les valeurs. Cependant, la valeur Offset (nombre d'octets occupés) a changé pour tous les blocs. Par exemple, la valeur Offset du bloc #51 est maintenant 172 au lieu de 7088. Il est clair que maintenant, lorsque la nouvelle valeur ne peut pas être insérée dans le bloc, le pointeur vers le dernier octet de données devrait être différent, mais où sont nos données ? Pour le moment, mon projet ne supporte pas la possibilité d'afficher des informations sur les "grands blocs". Utilisons l'outil ^REPAIR pour obtenir des informations sur le nouveau contenu du bloc #51.

Laissez-moi vous expliquer le fonctionnement de cet outil. Nous voyons un pointeur sur le bloc correct #52, et le même numéro est spécifié dans le bloc du pointeur parent dans le noeud suivant. Le collatéral de la globale est défini sur le type 5. Le nombre de noeuds avec des chaînes longues est de 7. Dans certains cas, le bloc peut contenir à la fois des valeurs de données pour certains noeuds et des chaînes longues pour d'autres, le tout dans un seul bloc. Nous voyons également quelle référence de pointeur suivante doit être attendue au début du bloc suivant.

Concernant les blocs de longues chaînes de caractères : nous voyons que le mot clé "BIG" est spécifié comme valeur du global. Cela nous indique que les données sont en fait stockées dans des "gros blocs". La même ligne contient la longueur totale de la chaîne contenue, et la liste des blocs stockant cette valeur. Jetons un coup d'oeil au "bloc de chaînes longues", le bloc #73.

Malheureusement, ce bloc est montré encodé. Cependant, nous pouvons remarquer que les informations de service de l'en-tête du bloc (qui font toujours 28 octets) sont suivies de nos données. Connaître le type de données rend le décodage du contenu de l'en-tête assez facile :

<td>
  Value
</td>

<td>
  Description
</td>

<td>
  Comment
</td>
<td>
  E4 1F 00 00
</td>

<td>
  Offset pointant vers la fin des données
</td>

<td>
  Nous avons 8164 octets, plus 28 octets d'en-tête pour un total de 8192 octets, le bloc est plein.
</td>
<td>
  18
</td>

<td>
  Type de bloc
</td>

<td>
  Comme on s'en souvient, 24 est l'identifiant de type pour les longues chaînes de caractères.
</td>
<td>
  05
</td>

<td>
  Collate
</td>

<td>
  Collate 5 signifie "Caché standard"
</td>
<td>
  4A 00 00 00
</td>

<td>
  Lien correct
</td>

<td>
  Nous obtenons 74 ici, car nous nous souvenons que notre valeur est stockée dans les blocs 73 et 74
</td>
Position
0-3
4
5
8-11

Je vous rappelle que les données du bloc 51 n'occupent que 172 octets. Cela s'est produit lorsque nous avons enregistré de grandes valeurs. Il semble donc que le bloc soit devenu presque vide avec seulement 172 octets de données utiles, et pourtant il occupe 8ko ! Il est clair que dans une telle situation, l'espace libre sera rempli de nouvelles valeurs, mais Caché nous permet également de compresser une telle globale. Pour cela, la classe %Library.GlobalEdit dispose de la méthode CompactGlobal. Pour vérifier l'efficacité de cette méthode, utilisons notre exemple avec un grand volume de données - par exemple, en créant 500 noeuds.

Voici ce que nous avons obtenu.

   kill ^test
   for l=1000,10000 {
       set str=""
       for i=1:1:l {
           set str=str_"1"
       }
       for i=1:1:500 {
           set ^test(i)=str
       }
   }
   quit

Nous n'avons pas montré tous les blocs ci-dessous, mais le résultat devrait être clair. Nous avons beaucoup de blocs de données, mais avec un petit nombre de noeuds.

Exécution de la méthode CompactGlobal :

write ##class(%GlobalEdit).CompactGlobal("test","c:\intersystems\ensemble\mgr\test")

Jetons un coup d'oeil au résultat. Le bloc des pointeurs ne compte plus que 2 nœuds, ce qui signifie que toutes nos valeurs sont allées à deux nœuds, alors que nous avions initialement 72 nœuds dans le bloc des pointeurs. Nous nous sommes donc débarrassés de 70 nœuds et avons ainsi réduit le temps d'accès aux données lors du passage par la globale, puisque cela nécessite moins d'opérations de lecture de bloc.

CompactGlobal accepte plusieurs paramètres, comme le nom du global, la base de données et la valeur de remplissage cible, 90% par défaut. Et maintenant nous voyons que Offset (le nombre d'octets occupés) est égal à 7360, ce qui est autour de ces 90%. Quelques paramètres de sortie de la fonction : le nombre de mégaoctets traités et le nombre de mégaoctets après compression. Auparavant, les globales étaient compressés à l'aide de l'outil ^GCOMPACT qui est maintenant considéré comme obsolète.

Il convient de noter qu'une situation où les blocs ne sont que partiellement remplis est tout à fait normale. De plus, la compression des globales peut parfois être indésirable. Par exemple, si votre globale est principalement lue et rarement modifiée, la compression peut s'avérer utile. Mais si la globale change tout le temps, une certaine sparsité dans les blocs de données permet d'éviter de diviser les blocs trop souvent, et l'enregistrement de nouvelles données sera plus rapide.

0
0 103
Article Guillaume Rongier · Avr 12, 2022 7m read

Les globales d'InterSystems Caché offrent des fonctionnalités très pratiques pour les développeurs. Mais pourquoi les globales sont-elles si rapides et efficaces ?

Théorie

Fondamentalement, la base de données Caché est un catalogue portant le même nom que la base de données et contenant le fichier CACHE.DAT. Sur les systèmes Unix, la base de données peut également être une partition de disque ordinaire.

Toutes les données dans Caché sont stockées dans des blocs qui, à leur tour, sont organisés sous forme d'un arbre B* équilibré. En tenant compte du fait que tous les globales sont fondamentalement stockées dans un arbre, les indices des globales seront représentés comme des branches, tandis que les valeurs des indices des globales seront stockées comme des feuilles. La différence entre un arbre B* équilibré et un arbre B ordinaire est que ses branches ont également des liens corrects qui peuvent aider à itérer à travers les souscripts (c'est-à-dire les globales dans notre cas) en utilisant rapidement les fonctions $Order et $Query sans revenir au tronc de l'arbre. 

Par défaut, chaque bloc du fichier de la base de données a une taille fixe de 8 192 octets. Vous ne pouvez pas modifier la taille du bloc pour une base de données déjà existante. Lorsque vous créez une nouvelle base de données, vous pouvez choisir des blocs de 16 Ko, 32 Ko ou même 64 Ko, en fonction du type de données que vous allez stocker. Cependant, gardez toujours à l'esprit que toutes les données sont lues bloc par bloc - en d'autres termes, même si vous demandez une valeur unique d'un octet, le système lira plusieurs blocs parmi lesquels le bloc de données demandé sera le dernier. Vous devez également vous rappeler que Caché utilise des buffers globaux pour stocker les blocs de base de données en mémoire pour une seconde utilisation, et que les buffers ont la même taille que les blocs. Vous ne pouvez pas monter une base de données existante ou en créer une nouvelle si un buffer global avec la taille de bloc correspondante est absent du système. Vous devez définir la taille de la mémoire que vous souhaitez allouer pour la taille spécifique des blocs. Il est possible d'utiliser des blocs de buffer plus grands que les blocs de base de données, mais dans ce cas, chaque bloc de buffer ne stockera qu'un seul bloc de base de données, voire plus petit.


Dans cette image, une mémoire pour le buffer global d'une taille de 8 ko est allouée pour l'utilisation avec des bases de données constituées de blocs de 8 ko. Les blocs qui ne sont pas vides dans cette base de données sont définis dans des cartes, de sorte qu'une des cartes définit 62 464 blocs (pour des blocs de 8 ko). 

Types de blocs

Le système prend en charge plusieurs types de blocs. À chaque niveau, les liens corrects d'un bloc doivent pointer vers un bloc du même type ou vers un bloc nul qui définit la fin des données.

  • Type 9: Le système prend en charge plusieurs types de blocs. À chaque niveau, les liens corrects d'un bloc doivent pointer vers un bloc du même type ou vers un bloc nul qui définit la fin des données.
  • Type 66: Bloc de pointeurs de haut niveau. Seul un bloc d'un catalogue global peut se trouver au-dessus de ces blocs.
  • Type 6: Bloc de pointeurs de bas niveau. Seuls les blocs de pointeurs de haut niveau peuvent se trouver au-dessus de ces blocs et seuls les blocs de données peuvent être placés plus bas.
  • Type 70: Bloc de pointeurs de haut niveau et de bas niveau. Ces blocs sont utilisés lorsque la globale correspondante stocke un petit nombre de valeurs et que plusieurs niveaux de blocs ne sont donc pas nécessaires. Ces blocs pointent généralement vers des blocs de données, tout comme le font les blocs d'un catalogue global.
  • Type 2: Bloc de pointeurs pour le stockage de globales relativement grandes. Afin de répartir uniformément les valeurs entre les blocs de données, vous pouvez créer des niveaux supplémentaires de blocs de pointeurs. Ces blocs sont généralement placés entre les blocs de pointeurs.
  • Type 8: Bloc de données. Ces blocs stockent généralement les valeurs de plusieurs nœuds globaux plutôt que celles d'un seul nœud.
  • Type 24: Bloc pour les grandes chaînes de caractères. Lorsque la valeur d'une seule globale est plus grande qu'un bloc, cette valeur est enregistrée dans un bloc spécial pour les grandes chaînes de caractères, tandis que le nœud de bloc de données stocke les liens vers la liste des blocs pour les grandes chaînes de caractères ainsi que la longueur totale de cette valeur.
  • Type 16: Bloc de carte. Ces blocs sont conçus pour stocker des informations sur les blocs non alloués.

Ainsi, le premier bloc d'une base de données Caché typique contient des informations de service sur le fichier de base de données lui-même, tandis que le deuxième bloc fournit une carte des blocs. Le premier bloc de catalogue va sur la troisième place (bloc #3), et une seule base de données peut avoir plusieurs blocs de catalogue. Les blocs suivants sont des blocs de pointeurs (branches), des blocs de données (feuilles) et des blocs de grandes chaînes de caractères. Comme je l'ai mentionné ci-dessus, les blocs de catalogues globaux stockent des informations sur tous les globales existants dans la base de données ou les paramètres globaux (si aucune donnée n'est disponible dans une telle globale). Dans ce cas, un nœud qui décrit une telle globale aura un pointeur inférieur nul. Vous pouvez consulter la liste des globales existantes à partir du catalogue de globales sur le portail de gestion. Ce portail vous permet également de sauvegarder une globale dans le catalogue après sa suppression (par exemple, sauvegarder sa séquence de collationnement) ainsi que de créer une nouvelle globale avec un collationnement par défaut ou personnalisé.

En général, l'arbre des blocs peut être représenté comme dans l'image ci-dessous. Notez que les liens vers les blocs sont représentés en rouge.

Intégrité des bases de données

Dans la version actuelle de Caché, nous avons résolu les questions et problèmes les plus importants concernant les bases de données, de sorte que les risques de dégradation des bases de données sont extrêmement faibles. Cependant, nous vous recommandons toujours d'exécuter régulièrement des contrôles d'intégrité automatiques à l'aide de notre outil ^Integrity - vous pouvez le lancer dans le terminal à partir de l'espace de noms %SYS, via notre portail de gestion, sur la page Database ou via le gestionnaire de tâches. Par défaut, le contrôle d'intégrité automatique est déjà configuré et prédéfini, de sorte que la seule chose que vous devez faire est de l'activer :

 ​​​​​​​

Le contrôle d'intégrité comprend la vérification des liens aux niveaux inférieurs, la validation des types de blocs, l'analyse des bons liens et la mise en correspondance des nœuds globaux avec la séquence de collationnement appliquée. Si des erreurs sont détectées lors du contrôle d'intégrité, vous pouvez exécuter notre outil ^REPAIR à partir de l'espace de noms %SYS. Grâce à cet outil, vous pouvez visualiser n'importe quel bloc et le modifier si nécessaire, c'est-à-dire réparer votre base de données. 

Pratique

Cependant, ce n'était que de la théorie. Il est encore difficile de savoir à quoi ressemblent réellement une globale et ses blocs. Actuellement, la seule façon de visualiser les blocs est d'utiliser notre outil ^REPAIR mentionné ci-dessus. La sortie typique de ce programme est présentée ci-dessous :

 

Il y a un an, j'ai lancé un nouveau projet visant à développer un outil qui itère à travers un arbre de blocs sans risque d'endommager la base de données, qui visualise ces blocs dans une interface utilisateur Web et qui offre des options pour sauvegarder leur visualisation en SVG ou PNG. Le projet s'appelle CacheBlocksExplorer, et vous pouvez télécharger son code source sur Github.

Les fonctionnalités mises en œuvre comprennent :

  • Visualisation de toute base de données configurée ou simplement installée dans le système ;
  • Affichage des informations par bloc, type de bloc, pointeur droit, liste des nœuds avec liens ;
  • Afficher des informations détaillées sur tout nœud pointant vers un bloc inférieur ;
  • Masquer des blocs en supprimant les liens vers ceux-ci (sans aucun dommage pour les données stockées dans ces blocs).

La liste des choses à faire :

  • Affichage des liens droits : dans la version actuelle, les liens droits sont affichés dans les informations sur les blocs, mais il serait préférable de les afficher sous forme de flèches ;
  • Affichage des blocs de grandes chaînes de caractères : ils ne sont tout simplement pas affichés dans la version actuelle ;
  • Affichage de tous les blocs du catalogue global plutôt que du troisième seulement.
  • Je voudrais également afficher l'intégralité de l'arbre, mais je ne trouve toujours pas de bibliothèque capable de rendre rapidement des centaines de milliers de blocs avec leurs liens - la bibliothèque actuelle les rend dans les navigateurs web bien plus lentement que Caché ne lit la structure entière.

    Dans mon prochain article, j'essaierai de décrire plus en détail son fonctionnement, de fournir quelques exemples d'utilisation et de montrer comment récupérer de nombreuses données exploitables sur les globales et les blocs à l'aide de mon Cache Block Explorer.

    0
    0 152