33.14. Interfacer des extensions d'index
Les procédures décrites jusqu'à maintenant permettent de définir de
nouveaux types, de nouvelles fonctions et de nouveaux opérateurs.
Néanmoins, nous ne pouvons pas encore définir un index sur une
colonne d'un nouveau type de données. Pour cela, nous devons définir
une classe d'opérateur pour le nouveau
type de données. Plus loin dans cette section, nous illustrerons ce
concept avec un exemple : une nouvelle classe d'opérateur pour la
méthode d'indexation B-tree qui enregistre et trie des nombres
complexes dans l'ordre ascendant des valeurs absolues.
Note
Avant PostgreSQL™ version
7.3, il était nécessaire de faire manuellement quelques ajouts
aux catalogues système pg_amop,
pg_amproc et pg_opclass afin de créer une classe d'opérateur
définie par l'utilisateur. Cette approche est maintenant
obsolète, au bénéfice de la commande CREATE
OPERATOR CLASS, qui est beaucoup plus simple et permet
d'éviter les erreurs lors de la création nécessaire des entrées
de ces catalogues.
33.14.1. Méthodes d'indexation et classes d'opérateurs
La table pg_am contient une ligne pour
chaque méthode d'indexation (connue en interne comme méthode
d'accès). Le support pour l'accès normal aux tables est implémenté
dans PostgreSQL™ mais toutes
les méthodes d'index sont décrites dans pg_am. Il est possible d'ajouter une nouvelle
méthode d'indexation en définissant les routines d'interface
nécessaires et en créant ensuite une ligne dans la table pg_am -- mais ceci est au-delà du sujet de ce
chapitre (voir le
Chapitre 49, Définition de l'interface des méthodes d'accès
aux index).
Les routines pour une méthode d'indexation n'ont pas à connaître
directement les types de données sur lesquels opère la méthode
d'indexation. Au lieu de cela, une classe
d'opérateur identifie l'ensemble d'opérations que la méthode
d'indexation doit utiliser pour fonctionner avec un type
particulier de données. Les classes d'opérateurs sont ainsi
dénommées parce qu'une de leur tâche est de spécifier l'ensemble
des opérateurs de la clause WHERE
utilisables avec un index (c'est-à-dire, qui peuvent être
requalifiés en balayage d'index). Une classe d'opérateur peut
également spécifier des procédures
d'appui, nécessaires pour les opérations internes de la
méthode d'indexation mais sans correspondance directe avec un
quelconque opérateur de clause WHERE
pouvant être utilisé avec l'index.
Il est possible de définir plusieurs classes d'opérateurs pour le
même type de données et la même méthode d'indexation. Ainsi, de
multiples ensembles de sémantiques d'indexation peuvent être
définis pour un seul type de données. Par exemple, un index B-tree
exige qu'un tri ordonné soit défini pour chaque type de données
auquel il peut s'appliquer. Il peut être utile pour un type de
donnée de nombre complexe de disposer d'une classe d'opérateur
B-tree qui trie les données selon la valeur absolue complexe, une
autre selon la partie réelle, etc. Typiquement, une des classes
d'opérateur sera considérée comme plus utile et sera marquée comme
l'opérateur par défaut pour ce type de données et cette méthode
d'indexation.
Le même nom de classe d'opérateur peut être utilisé pour plusieurs
méthodes d'indexation différentes (par exemple, les méthodes
d'index B-tree et hash ont toutes les deux des classes d'opérateur
nommées int4_ops) mais chacune de ces
classes est une entité indépendante et doit être définie
séparément.
33.14.2. Stratégies des méthode d'indexation
Les opérateurs associés à une classe d'opérateur sont identifiés
par des « numéros de
stratégie », servant à identifier la sémantique de
chaque opérateur dans le contexte de sa classe d'opérateur. Par
exemple, les B-trees imposent un classement strict selon les clés,
du plus petit au plus grand. Ainsi, des opérateurs comme
« plus petit que » et
« plus grand que » sont
intéressants pour un B-tree. Comme PostgreSQL™ permet à l'utilisateur de
définir des opérateurs, PostgreSQL™ ne peut pas rechercher le
nom d'un opérateur (par exemple, < ou
>=) et rapporter de quelle comparaison
il s'agit. Au lieu de cela, la méthode d'indexation définit un
ensemble de « stratégies »,
qui peuvent être comprises comme des opérateurs généralisés. Chaque
classe d'opérateur spécifie l'opérateur effectif correspondant à
chaque stratégie pour un type de donnée particulier et pour une
interprétation de la sémantique d'index.
La méthode d'indexation B-tree définit cinq stratégies, qui sont
exposées dans le Tableau 33.2,
« Stratégies B-tree ».
Tableau 33.2. Stratégies B-tree
|
Opération
|
Numéro de stratégie
|
|
plus petit que
|
1
|
|
plus petit ou égal
|
2
|
|
égal
|
3
|
|
plus grand ou égal
|
4
|
|
plus grand que
|
5
|
Les index de découpage expriment seulement une égalité bit à bit et
utilisent ainsi une seule stratégie exposée dans le Tableau 33.3,
« Stratégies de découpage ».
Tableau 33.3. Stratégies de découpage
|
Opération
|
Numéro de stratégie
|
|
égal à
|
1
|
Les index GiST sont encore plus flexibles : ils n'ont pas du tout
un ensemble fixe de stratégies. À la place, la routine de support
de « cohérence » de chaque
classe d'opérateur GiST interprète les numéros de stratégie comme
elle l'entend. Comme exemple, plusieurs des classes d'opérateurs
GiST indexe les objets géométriques à deux dimensions fournissant
les stratégies « R-tree »
affichées dans
Tableau 33.4, « Stratégies « R-tree » pour GiST à deux
dimensions ». Quatre d'entre elles sont des vrais tests à
deux dimensions (surcharge, identique, contient, contenu par) ;
quatre autres considèrent seulement la direction X ; et les quatre
dernières fournissent les mêmes tests dans la direction Y.
Tableau 33.4. Stratégies « R-tree » pour GiST à deux dimensions
|
Opération
|
Numéro de stratégie
|
|
strictement à gauche de
|
1
|
|
ne s'étend pas à droite de
|
2
|
|
surcharge
|
3
|
|
ne s'étend pas à gauche de
|
4
|
|
strictement à droite de
|
5
|
|
identique
|
6
|
|
contient
|
7
|
|
contenu par
|
8
|
|
ne s'étend pas au dessus
|
9
|
|
strictement en dessous
|
10
|
|
strictement au dessus
|
11
|
|
ne s'étend pas en dessous
|
12
|
Les index GIN sont similaires aux index GiST en flexibilité : ils
n'ont pas un ensemble fixe de stratégie. À la place, les routines
de support de chaque classe d'opérateur interprètent les numéros de
stratégie suivant la définition du classe d'opérateur. Comme
exemple, les numéros des stratégies utilisés par les classes
d'opérateur sur des tableaux sont affichés dans Tableau 33.5,
« Stratégies des tableaux GIN ».
Tableau 33.5. Stratégies des tableaux GIN
|
Opération
|
Numéro de stratégie
|
|
surcharge
|
1
|
|
contient
|
2
|
|
est contenu par
|
3
|
|
identique
|
4
|
Notez que tous les opérateurs de stratégie renvoient des valeurs de
type booléen. Dans la pratique, tous les opérateurs définis comme
stratégies de méthode d'indexation doivent renvoyer un type
boolean puisqu'ils doivent apparaître au
plus haut niveau d'une clause WHERE pour
être utilisés avec un index.
À ce propos, la colonne
amorderstrategy
dans pg_am indique si la méthode d'indexation supporte
les balayages ordonnés. Zéro indique qu'elle ne les supporte pas ;
si elle les supporte,
amorderstrategy
est le numéro de
stratégie qui correspond à l'opérateur de classement. Par exemple,
B-tree a
amorderstrategy
= 1,
qui est son numéro de stratégie pour « plus petit que ».
33.14.3. Routines d'appui des méthodes d'indexation
Généralement, les stratégies n'apportent pas assez d'informations
au système pour indiquer comment utiliser un index. Dans la
pratique, les méthodes d'indexation demandent des routines d'appui
additionnelles pour fonctionner. Par exemple, les méthodes d'index
B-tree doivent être capables de comparer deux clés et de déterminer
laquelle est supérieure, égale ou inférieure à l'autre. De la même
façon, la méthode d'indexation hash doit être capable de calculer
les codes de hachage pour les valeurs de clés. Ces opérations ne
correspondent pas à des opérateurs utilisés dans les commandes SQL
; ce sont des routines administratives utilisées en interne par des
méthodes d'index.
Comme pour les stratégies, la classe d'opérateur énumère les
fonctions spécifiques et le rôle qu'elles doivent jouer pour un
type de donnée donné et une interprétation sémantique donnée. La
méthode d'indexation définit l'ensemble des fonctions dont elle a
besoin et la classe d'opérateur identifie les fonctions exactes à
utiliser en les assignant aux « numéros de
fonction d'appui ».
Les B-trees demandent une seule fonction d'appui, exposée dans le
Tableau 33.6,
« Fonctions d'appui de B-tree ».
Tableau 33.6. Fonctions d'appui de B-tree
|
Fonction
|
Numéro d'appui
|
|
Comparer deux clés et renvoyer un entier inférieur à
zéro, zéro ou supérieure à zéro indiquant si la première
clé est inférieure, égale ou supérieure à la deuxième.
|
1
|
De façon identique, les index de découpage requièrent une fonction
d'appui exposée dans le Tableau 33.7,
« Fonctions d'appui pour découpage ».
Tableau 33.7. Fonctions d'appui pour découpage
|
Fonction
|
Numéro d'appui
|
|
Calculer la valeur de découpage pour une clé
|
1
|
Les index GiST requièrent sept fonctions d'appui exposées dans le
Tableau 33.8,
« Fonctions d'appui pour GiST ».
Tableau 33.8. Fonctions d'appui pour GiST
|
Fonction
|
Numéro d'appui
|
|
consistency - détermine si la clé satisfait le qualifiant
de la requête
|
1
|
|
union - calcule l'union d'un ensemble de clés
|
2
|
|
compress - calcule une représentation compressée d'une
clé ou d'une valeur à indexer
|
3
|
|
decompress - calcule une représentation décompressée
d'une clé compressée
|
4
|
|
penality - calcule la pénalité pour l'insertion d'une
nouvelle clé dans un sous-arbre avec la clé du sous-arbre
indiqué
|
5
|
|
picksplit - détermine les entrées d'une page qui sont à
déplacer vers la nouvelle page et calcule les clés
d'union pour les pages résultantes
|
6
|
|
equal - compare deux clés et renvoie true si elles sont
identiques
|
7
|
Les index GIN requièrent quatre fonctions d'appui exposées dans le
Tableau 33.9,
« Fonctions d'appui GIN ».
Tableau 33.9. Fonctions d'appui GIN
|
Fonction
|
Numéro d'appui
|
|
compare - Compare deux clés et renvoie un entier plus
petit que zéro, zéro ou plus grand que zéro, indiquant si
la première clé est plus petit, égal à ou plus grand que
la seconde.
|
1
|
|
extractValue - extrait les clés à partir d'une condition
de requête
|
2
|
|
extractQuery - extrait les clés à partir d'une condition
de requête
|
3
|
|
consistent - détermine la valeur correspondant à la
condition de requête
|
4
|
Contrairement aux opérateurs de stratégie, les fonctions d'appui
renvoient le type de donnée, quelqu'il soit, que la méthode
d'indexation particulière attend, par exemple, dans le cas de la
fonction de comparaison des B-trees, un entier signé.
Maintenant que nous avons vu les idées, voici l'exemple promis de
création d'une nouvelle classe d'opérateur. Cette classe
d'opérateur encapsule les opérateurs qui trient les nombres
complexes selon l'ordre de la valeur absolue, aussi avons-nous
choisi le nom de complex_abs_ops. En
premier lieu, nous avons besoin d'un ensemble d'opérateurs. La
procédure pour définir des opérateurs a été discutée dans la
Section 33.12,
« Opérateurs définis par l'utilisateur ». Pour une
classe d'opérateur sur les B-trees, nous avons besoin des
opérateurs :
- valeur absolue less-than (stratégie 1) ;
- valeur absolue less-than-or-equal (stratégie 2) ;
- valeur absolue equal (stratégie 3) ;
- valeur absolue greater-than-or-equal (stratégie 4) ;
- valeur absolue greater-than (stratégie 5) ;
Le plus simple moyen de définie un ensemble d'opérateurs de
comparaison est d'écrire en premier la fonction de comparaison
B-tree, puis d'écrire les autres fonctions en tant que wrapper de
la fonction de support. Ceci réduit les risques de résultats
incohérents pour les cas spécifiques. En suivant cette approche,
nous devons tout d'abord écrire :
#define Mag(c) ((c)->x*(c)->x + (c)->y*(c)->y)
bool
complex_abs_eq(Complex *a, Complex *b)
{
double amag = Mag(a), bmag = Mag(b);
return (amag == bmag);
}
Maintenant, la fonction plus-petit-que ressemble à ceci :
PG_FUNCTION_INFO_V1(complex_abs_lt);
Datum
complex_abs_lt(PG_FUNCTION_ARGS)
{
Complex *a = (Complex *) PG_GETARG_POINTER(0);
Complex *b = (Complex *) PG_GETARG_POINTER(1);
PG_RETURN_BOOL(complex_abs_cmp_internal(a, b) < 0);
}
Les quatre autres fonctions diffèrent seulement sur la façon dont
ils comparent le résultat de la fonction interne au zéro.
Maintenant, déclarons en SQL les fonctions et les opérateurs basés
sur ces fonctions :
CREATE FUNCTION complex_abs_lt(complex, complex) RETURNS bool
AS 'nom_fichier', 'complex_abs_lt'
LANGUAGE C IMMUTABLE STRICT;
CREATE OPERATOR < (
leftarg = complex, rightarg = complex, procedure = complex_abs_lt,
commutator = > , negator = >= ,
restrict = scalarltsel, join = scalarltjoinsel
);
Il est important de spécifier les fonctions de sélectivité de
restriction et de jointure, sinon l'optimiseur sera incapable de
faire un usage effectif de l'index. Notez que les cas 'less-than',
'equal' et 'greater-than' doivent utiliser des fonctions
différentes de sélectivité.
Voici d'autres choses importantes à noter :
-
Il ne peut y avoir qu'un seul opérateur nommé, disons,
= et acceptant un type complex pour ses deux opérandes. Dans le cas
présent, nous n'avons aucun autre opérateur = pour complex mais,
si nous construisons un type de donnée fonctionnel, nous
aurions certainement désiré que =
soit l'opération ordinaire d'égalité pour les nombres
complexes (et non pour l'égalité de leurs valeurs absolues).
Dans ce cas, nous aurions eu besoin d'utiliser un autre nom
d'opérateur pour notre fonction complex_abs_eq.
-
Bien que PostgreSQL™
puisse se débrouiller avec des fonctions ayant le même nom
SQL, tant qu'elles ont en argument des types de données
différents, en C il ne peut exister qu'une fonction globale
pour un nom donné. Aussi ne devons-nous pas donner un nom
simple comme abs_eq.
Habituellement, inclure le nom du type de données dans le nom
de la fonction C est une bonne habitude pour ne pas provoquer
de conflit avec des fonctions pour d'autres types de donnée.
-
Nous aurions pu faire de abs_eq le
nom SQL de la fonction, en laissant à PostgreSQL™ le soin de la
distinguer de toute autre fonction SQL de même nom par les
types de données en argument. Pour la simplicité de
l'exemple, nous donnerons à la fonction le même nom au niveau
de C et au niveau de SQL.
La prochaine étape est l'enregistrement de la routine d'appui
nécessaire pour les B-trees. Le code exemple C qui implémente ceci
est dans le même fichier qui contient les fonctions d'opérateur.
Voici comment déclarer la fonction :
CREATE FUNCTION complex_abs_cmp(complex, complex)
RETURNS integer
AS 'filename'
LANGUAGE C;
Maintenant que nous avons les opérateurs requis et la routine
d'appui, nous pouvons enfin créer la classe d'opérateur.
CREATE OPERATOR CLASS complex_abs_ops
DEFAULT FOR TYPE complex USING btree AS
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 complex_abs_cmp(complex, complex);
Et c'est fait ! Il devrait être possible maintenant de créer et
d'utiliser les index B-tree sur les colonnes complex.
Nous aurions pu écrire les entrées de l'opérateur de façon plus
explicite comme dans
OPERATOR 1 < (complex, complex) ,
mais il n'y a pas besoin de faire ainsi quand les opérateurs
prennent le même type de donnée que celui pour lequel la classe
d'opérateur a été définie.
Les exemples ci-dessus supposent que vous voulez que cette nouvelle
classe d'opérateur soit la classe d'opérateur B-tree par défaut
pour le type de donnée complex. Si vous
ne voulez pas, supprimez simplement le mot DEFAULT.
33.14.5. Classes d'opérateur inter-type
Jusqu'à maintenant, nous avons supposé implicitement qu'une classe
d'opérateur s'occupe d'un seul type de données. Bien qu'il ne peut
y avoir qu'un seul type de données dans une colonne d'index
particulière, il est souvent utile d'indexer les opérations qui
comparent une colonne indexée à une valeur d'un type de données
différent. Ceci est actuellement supporté par les méthodes
d'indexation B-tree et GiST.
Les index B-trees requièrent que l'opérande côté gauche de chaque
opérateur soit le type de donnée indexé mais l'opérande côté droit
peut être d'un type différent. Il doit exister une fonction de
support disposant d'une signature correspondante. Par exemple, la
classe d'opérateur intégrée pour le type bigint (int8) permet les
comparaisons inter-type vers int4 et
int2. Cela pourrait être dupliqué par
cette définition :
CREATE OPERATOR CLASS int8_ops
DEFAULT FOR TYPE int8 USING btree AS
-- comparaisons int8 standard
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 btint8cmp(int8, int8) ,
-- comparaisons entre types vers int2 (smallint)
OPERATOR 1 < (int8, int2) ,
OPERATOR 2 <= (int8, int2) ,
OPERATOR 3 = (int8, int2) ,
OPERATOR 4 >= (int8, int2) ,
OPERATOR 5 > (int8, int2) ,
FUNCTION 1 btint82cmp(int8, int2) ,
-- comparaisons entre types vers int4 (integer)
OPERATOR 1 < (int8, int4) ,
OPERATOR 2 <= (int8, int4) ,
OPERATOR 3 = (int8, int4) ,
OPERATOR 4 >= (int8, int4) ,
OPERATOR 5 > (int8, int4) ,
FUNCTION 1 btint84cmp(int8, int4) ;
Notez que cette définition « surcharge » la stratégie de l'opérateur et
supporte les numéros de fonction. Ceci est autorisé (seulement pour
les classes d'opérateur B-tree) tant que chaque instance d'un
nombre particulier a un type de données côté droit différent. Les
instances qui ne sont pas inter-type sont la valeur par défaut ou
les opérateurs principaux de la classe d'opérateur.
Les index GiST n'autorisent pas le surchargement de stratégie ou
les nombres de fonctions de support mais il est toujours possible
d'obtenir l'effet d'un support de plusieurs types de données sur le
côté droit en affectant un numéro de stratégie distinct à chaque
opérateur qui a besoin d'être supporté. La fonction de support
cohérente doit déterminer ce qu'elle a
besoin de faire suivant le numéro de stratégie et doit être
préparée à accepter des valeurs de comparaisons des types de
données appropriés.
33.14.6. Dépendances du système pour les classes d'opérateur
PostgreSQL™ utilise les
classe d'opérateur pour inférer les propriétés des opérateurs de
plusieurs autres façons que le seul usage avec les index. Donc,
vous pouvez créer des classes d'opérateur même si vous n'avez pas
l'intention d'indexer une quelconque colonne de votre type de
donnée.
En particulier, il existe des caractéristiques de SQL telles que
ORDER BY et DISTINCT qui requièrent la comparaison et le tri des
valeurs. Pour implémenter ces caractéristiques sur un type de
donnée défini par l'utilisateur, PostgreSQL™ recherche la classe
d'opérateur B-tree par défaut pour le type de donnée. Le membre
« equals » de cette classe
d'opérateur définit pour le système la notion d'égalité des valeurs
pour GROUP BY et DISTINCT, et le tri ordonné imposé par la classe
d'opérateur définit le ORDER BY par
défaut.
La comparaison des tableaux de types définis par l'utilisateur
repose sur les sémantiques définies par la classe d'opérateur
B-tree par défaut.
S'il n'y a pas de classe d'opérateur B-tree par défaut pour le type
de donnée, le système cherchera une classe d'opérateur de
découpage. Mais puisque cette classe d'opérateur ne fournit que
l'égalité, c'est en pratique seulement suffisant pour établir
l'égalité de tableau.
Quand il n'y a pas de classe d'opérateur par défaut pour un type de
donnée, vous obtenez des erreurs telles que « could not identify an ordering operator » si
vous essayez d'utiliser ces caractéristiques SQL avec le type de
donnée.
Note
Dans les versions de PostgreSQL™ antérieures à la 7.4,
les opérations de tri et de groupement utilisaient
implicitement les opérateurs nommés =,
< et >.
Le nouveau comportement qui repose sur les classes d'opérateurs
par défaut évite d'avoir à faire une quelconque supposition sur
le comportement des opérateurs avec des noms particuliers.
33.14.7. Caractéristiques spéciales des classes d'opérateur
Il y a deux caractéristiques spéciales des classes d'opérateur dont
nous n'avons pas encore parlées, essentiellement parce qu'elles ne
sont pas utiles avec les méthodes d'index les plus communément
utilisées.
Normalement, déclarer un opérateur comme membre d'une classe
d'opérateur signifie que la méthode d'indexation peut retrouver
exactement l'ensemble de lignes qui satisfait la condition
WHERE utilisant cet opérateur. Par
exemple,
SELECT * FROM table WHERE colonne_entier < 4;
peut être accompli exactement par un index B-tree sur la colonne
entière. Mais il y a des cas où un index est utile comme un guide
inexact vers la colonne correspondante. Par exemple, si un index
GiST enregistre seulement les rectangles limite des objets, alors
il ne peut pas exactement satisfaire une condition WHERE qui teste le chevauchement entre des objets
non rectangulaires comme des polygones. Cependant, nous pourrions
utiliser l'index pour trouver des objets dont les rectangles
limites chevauchent les limites de l'objet cible. Dans ce cas,
l'index est dit être à perte pour l'opérateur, et nous ajoutons
RECHECK à la clause OPERATOR dans la commande
CREATE OPERATOR CLASS
. RECHECK est valide si l'index garantie de retourner
toutes les lignes requises, plus peut-être des lignes
supplémentaires pouvant être éliminées par le recours à l'opérateur
original.
Considérons à nouveau la situation où nous gardons seulement dans
l'index le rectangle délimitant un objet complexe comme un
polygone. Dans ce cas, il n'est pas très intéressant de conserver
le polygone entier dans l'index - nous pouvons aussi bien conserver
seulement un objet simple du type box.
Cette situation est exprimée par l'option STORAGE dans la commande
CREATE OPERATOR CLASS
: nous
aurons à écrire quelque chose comme
CREATE OPERATOR CLASS polygon_ops
DEFAULT FOR TYPE polygon USING gist AS
...
STORAGE box;
Actuellement, seule les méthodes d'indexation GIN et GiST
supportent un type STORAGE qui soit
différent du type de donnée de la colonne. Les routines d'appui de
GiST pour la compression (compress) et la
décompression (decompress) doivent
s'occuper de la conversion du type de donnée quand STORAGE est utilisé. Avec GIN, le type STORAGE identifie le type des valeurs
« key », qui est normalement
différent du type de la colonne indexée -- par exemple, une classe
d'opérateur pour des colonnes de tableaux d'entiers pourrait avoir
des clés qui sont seulement des entiers. Les routines de support
GIN extractValue et extractQuery sont responsables de l'extraction des
clés à partir des valeurs indexées.