31.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.

31.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 bien au-delà du sujet de ce chapitre.

Les routines d'une méthode d'indexation ne connaissent pas directement les types de données sur lesquels opère la méthode. Au lieu de cela, une classe d'opérateur identifie l'ensemble des 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 parcours 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 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.

31.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 31-2.

Tableau 31-2. Stratégies B-tree

OpérationNuméro de stratégie
plus petit que1
plus petit ou égal2
égal3
plus grand ou égal4
plus grand que5

Les index de découpage expriment seulement une égalité bit à bit et utilisent ainsi une seule stratégie exposée dans le Tableau 31-3.

Tableau 31-3. Stratégies de découpage

OpérationNuméro de stratégie
égal à1

Les index R-tree expriment des relations d'appartenance à un rectangle. Ils utilisent huit stratégies, exposées dans le Tableau 31-4.

Tableau 31-4. Stratégies R-tree

OpérationNuméro de stratégie
à gauche de1
à gauche ou imbriqué2
imbriqué3
à droite ou imbriqué4
à droite de5
identique6
contient7
contenu par8

Les index GiST sont encore plus flexibles : ils n'ont pas du tout d'ensemble fixe de stratégies. En lieu et place, la routine d'appui de cohérence de chaque classe d'opérateur GiST particulière interprète les numéros de stratégie comme elle l'entend.

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 >>.

31.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 R-tree doit être capable de calculer les intersections, unions et dimensions des rectangles. 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 31-5.

Tableau 31-5. Fonctions d'appui de B-tree

FonctionNumé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 31-6.

Tableau 31-6. Fonctions d'appui pour découpage

FonctionNuméro d'appui
Calculer la valeur de découpage pour une clé1

Les index R-tree requièrent trois fonctions d'appui exposées dans le Tableau 31-7.

Tableau 31-7. Fonctions d'appui pour R-tree

FonctionNuméro d'appui
union1
intersection2
taille3

Les index GiST requièrent sept fonctions d'appui exposées dans le Tableau 31-8.

Tableau 31-8. Fonctions d'appui GiST

FonctionNuméro d'appui
cohérence1
union2
compression3
décompression4
coût5
séparation6
égalité7

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é.

31.14.4. Exemple

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 31.12. Pour une classe d'opérateur sur les B-trees, nous avons besoin des opérateurs :

Le code C de l'opérateur d'égalité ressemble à ceci :

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

Les quatre autres opérateurs sont très similaires. Vous pouvez trouver leur code dans src/tutorial/complex.c et src/tutorial/complex.sql dans la distribution des sources.

Maintenant, déclarons les fonctions et les opérateurs basés sur ces fonctions :

CREATE FUNCTION complex_abs_eq(complex, complex) RETURNS boolean
    AS 'nom_fichier', 'complex_abs_eq'
    LANGUAGE C;

CREATE OPERATOR = (
    leftarg = complex,
    rightarg = complex,
    procedure = complex_abs_eq,
    restrict = eqsel,
    join = eqjoinsel
);

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 :

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.

31.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.

31.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.

31.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 R-tree 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 la méthode d'indexation GiST supporte un type STORAGE qui soit différent du type de donnée de la colonne. Les routines d'appui de GiST pour la compression et la décompression doivent s'occuper de la conversion du type de donnée quand STORAGE est utilisé.