31.13. Informations sur l'optimisation d'un opérateur

Une définition d'opérateur PostgreSQL peut inclure plusieurs clauses optionnelles qui donnent au système des informations utiles sur le comportement de l'opérateur. Ces clauses devraient être fournies chaque fois que c'est utile car elles peuvent considérablement accélérer l'exécution des requêtes utilisant cet opérateur. Mais si vous le faites, vous devez être sûr de leur justesse ! L'usage incorrect d'une clause d'optimisation peut entraîner un arrêt brutal du processus serveur, des sorties subtilement fausses ou d'autres effets pervers. Vous pouvez toujours abandonner une clause d'optimisation si vous n'êtes pas sûr d'elle ; la seule conséquence est un possible ralentissement des requêtes.

Des clauses additionnelles d'optimisation pourront être ajoutées dans les futures versions de PostgreSQL. Celles décrites ici sont toutes celles que cette version comprend.

31.13.1. COMMUTATOR

Si elle est fournie, la clause COMMUTATOR désigne un opérateur qui est le commutateur de l'opérateur en cours de définition. Nous disons qu'un opérateur A est le commutateur de l'opérateur B si (x A y) est égal à (y B x) pour toute valeur possible de x, y. Notez que B est aussi le commutateur de A. Par exemple, les opérateurs < et > pour un type particulier de données sont habituellement des commutateurs l'un pour l'autre, et l'opérateur + est habituellement commutatif avec lui-même. Mais l'opérateur - n'est habituellement commutatif avec rien.

Le type de l'opérande gauche d'un opérateur commuté est le même que l'opérande droit de son commutateur, et vice versa. Aussi PostgreSQL n'a besoin que du nom de l'opérateur commutateur pour consulter le commutateur, et c'est tout ce qui doit être fourni à la clause COMMUTATOR .

Vous avez juste à définir un opérateur auto-commutateur. Mais les choses sont un peu plus compliquées quand vous définissez une paire de commutateurs : comment peut-on définir la référence du premier au second alors que ce dernier n'est pas encore défini ? Il y a deux solutions à ce problème :

31.13.2. NEGATOR

La clause NEGATOR dénomme un opérateur qui est l'opérateur de négation de l'opérateur en cours de définition. Nous disons qu'un opérateur A est l'opérateur de négation de l'opérateur B si tous les deux renvoient des résultats booléens et si (x A y) est égal à NOT (x B y) pour toutes les entrées possible x, y. Notez que B est aussi l'opérateur de négation de A. Par exemple, < et >= forment une paire d'opérateurs de négation pour la plupart des types de données. Un opérateur ne peut jamais être validé comme son propre opérateur de négation .

Au contraire des commutateurs, une paire d'opérateurs unaires peut être validée comme une paire d'opérateurs de négation réciproques ; ce qui signifie que (A x) est égal à NOT (B x) pour tout x ou l'équivalent pour les opérateurs unaires à droite.

L'opérateur de négation d'un opérateur doit avoir les mêmes types d'opérandes gauche et/ou droit que l'opérateur à définir comme avec COMMUTATOR. Seul le nom de l'opérateur doit être donné dans la clause NEGATOR.

Définir un opérateur de négation est très utile pour l'optimiseur de requêtes car il permet de simplifier des expressions telles que NOT (x = y) en x <> y. Ceci arrive souvent parce que les opérations NOT peuvent être insérées à la suite d'autres réarrangements.

Des paires d'opérateurs de négation peuvent être définies en utilisant la même méthode que pour les commutateurs.

31.13.3. RESTRICT

La clause RESTRICT, si elle est invoquée, nomme une fonction d'estimation de sélectivité de restriction pour cet opérateur (notez que c'est un nom de fonction, et non pas un nom d'opérateur). Les clauses RESTRICT n'ont de sens que pour les opérateurs binaires qui renvoient un type boolean. Un estimateur de sélectivité de restriction repose sur l'idée de prévoir quelle fraction des lignes dans une table satisfera une condition de clause WHERE de la forme

colonne OP constante

pour l'opérateur courant et une valeur constante particulière. Ceci aide l'optimiseur en lui donnant une idée du nombre de lignes qui sera éliminé par les clauses WHERE qui ont cette forme (vous pouvez vous demander, qu'arrivera-t-il si la constante est à gauche ? hé bien, c'est une des choses à laquelle sert le COMMUTATOR...).

L'écriture de nouvelles fonctions d'estimation de restriction de sélectivité est éloignée des objectifs de ce chapitre mais, heureusement, vous pouvez habituellement utiliser un des estimateurs standards du système pour beaucoup de vos propres opérateurs. Voici les estimateurs standards de restriction :

eqsel pour =
neqsel pour <>
scalarltsel pour < ou <=
scalargtsel pour > ou >=

Ces catégories peuvent sembler un peu curieuses mais cela prend un sens si vous y réfléchissez. = acceptera typiquement une petite fraction des lignes d'une table ; <> rejettera typiquement seulement une petite fraction des lignes de la table. < acceptera une fraction des lignes en fonction de la situation de la constante donnée dans la gamme de valeurs de la colonne pour cette table (ce qui est justement l'information collectée par la commande ANALYZE et rendue disponible pour l'estimateur de sélectivité). <= acceptera une fraction légèrement plus grande que < pour la même constante de comparaison mais elles sont assez proches pour ne pas valoir la peine d'être distinguées puisque nous ne risquons pas de toute façon de faire mieux qu'une grossière estimation. La même remarque s'applique à > et >=.

Vous pouvez fréquemment vous en sortir à bon compte en utilisant soit eqsel ou neqsel pour des opérateurs qui ont une très grande ou une très faible sélectivité, même s'ils ne sont pas réellement égalité ou inégalité. Par exemple, les opérateurs géométriques d'égalité approchée utilisent eqsel en supposant habituellement qu'ils ne correspondent qu'à une petite fraction des entrées dans une table.

Vous pouvez utiliser scalarltsel et scalargtsel pour des comparaisons de types de données qui possèdent un moyen de conversion en scalaires numériques pour les comparaisons de rang. Si possible, ajoutez le type de données à ceux acceptés par la fonction convert_to_scalar() dans src/backend/utils/adt/selfuncs.c (finalement, cette fonction devrait être remplacée par des fonctions pour chaque type de données identifié grâce à une colonne du catalogue système pg_type ; mais cela n'a pas encore été fait). Si vous ne faites pas ceci, les choses fonctionneront mais les estimations de l'optimiseur ne seront pas aussi bonnes qu'elles pourraient l'être.

D'autres fonctions d'estimation de sélectivité conçues pour les opérateurs géométriques sont placées dans src/backend/utils/adt/geo_selfuncs.c : areasel, positionsel et contsel. Lors de cette rédaction, ce sont seulement des fragments mais vous pouvez vouloir les utiliser (ou mieux les améliorer).

31.13.4. JOIN

La clause JOIN, si elle est invoquée, nomme une fonction d'estimation de sélectivité de jointure pour l'opérateur (notez que c'est un nom de fonction, et non pas un nom d'opérateur). Les clauses JOIN n'ont de sens que pour les opérateurs binaires qui renvoient un type boolean. Un estimateur de sélectivité de jointure repose sur l'idée de prévoir quelle fraction des lignes dans une paire de tables satisfera une condition de clause WHERE de la forme

table1.colonne1 OP table2.colonne2

pour l'opérateur courant. Comme pour la clause RESTRICT, ceci aide considérablement l'optimiseur en lui indiquant parmi plusieurs séquences de jointure possibles laquelle prendra vraisemblablement le moins de travail.

Comme précédemment, ce chapitre n'essaiera pas d'expliquer comment écrire une fonction d'estimation de sélectivité de jointure mais suggérera simplement d'utiliser un des estimateurs standard s'il est applicable :

eqjoinsel pour =
neqjoinsel pour <>
scalarltjoinsel pour < ou <=
scalargtjoinsel pour > ou >=
areajoinsel pour des comparaisons basées sur une aire 2D
positionjoinsel pour des comparaisons basées sur une position 2D
contjoinsel pour des comparaisons basées sur un appartenance 2D

31.13.5. HASHES

La clause HASHES indique au système qu'il est permis d'utiliser la méthode de jointure-découpage pour une jointure basée sur cet opérateur. HASHES n'a de sens que pour un opérateur binaire qui renvoie un boolean et en pratique l'opérateur égalité serait mieux approprié pour certains types de données

La jointure-découpage repose sur l'hypothèse que l'opérateur de jointure peut seulement renvoyer la valeur vrai pour des paires de valeurs droite et gauche qui correspondent au même code de découpage. Si deux valeurs sont placées dans deux différents paquets (<< buckets >>), la jointure ne pourra jamais les comparer avec la supposition implicite que le résultat de l'opérateur de jointure doit être faux. Ainsi, il n'y a aucun sens à spécifier HASHES pour des opérateurs qui ne représentent pas l'égalité.

Pour être marqué HASHES, l'opérateur de jointure doit apparaître dans une classe d'opérateurs d'index de découpage. Ceci n'est pas rendu obligatoire quand vous créez l'opérateur, puisque évidemment la classe référençant l'opérateur peut ne pas encore exister. Mais les tentatives d'utilisation de l'opérateur dans les jointure-découpage échoueront à l'exécution si une telle classe d'opérateur n'existe pas. Le système a besoin de la classe d'opérateur pour définir la fonction de découpage spécifique au type de données d'entrée de l'opérateur. Bien sûr, vous devez également fournir une fonction de découpage appropriée avant de pouvoir créer la classe d'opérateur.

On doit apporter une grande attention à la préparation des fonctions de découpage parce qu'il y a des processus dépendants de la machine qui peuvent ne pas faire les choses correctement. Par exemple, si votre type de données est une structure dans laquelle peuvent se trouver des bits de remplissage sans intérêt, vous ne pouvez pas simplement passer la structure complète à la fonction hash_any (à moins d'écrire vos autres opérateurs et fonctions de façon à s'assurer que les bits inutilisés sont toujours zéro, ce qui est la stratégie recommandée). Un autre exemple est fourni sur les machines qui respectent le standard de virgule-flottante IEEE, le zéro négatif et le zéro positif sont des valeurs différentes (les motifs de bit sont différents) mais ils sont définis pour être égaux. Si une valeur flottante peut contenir un zéro négatif, alors une étape supplémentaire est nécessaire pour s'assurer qu'elle génère la même valeur de découpage qu'un zéro positif.

Note : La fonction sous-jacente à un opérateur de jointure-découpage doit être marquée immuable ou stable. Si elle est volatile, le système n'essaiera jamais d'utiliser l'opérateur pour une jointure hachage.

Note : Si un opérateur de jointure-hachage a une fonction sous-jacente marquée stricte, la fonction doit également être complète : cela signifie qu'elle doit renvoyer TRUE ou FALSE, jamais NULL, pour n'importe quelle double entrée non NULL. Si cette règle n'est pas respectée, l'optimisation de découpage des opérations IN peut générer des résultats faux (spécifiquement, IN devrait renvoyer FALSE quand la réponse correcte devrait être NULL ; ou bien il devrait renvoyer une erreur indiquant qu'il ne s'attendait pas à un résultat NULL).

31.13.6. MERGES (SORT1, SORT2, LTCMP, GTCMP)

La clause MERGES, si elle est présente, indique au système qu'il est permis d'utiliser la méthode de jointure-union pour une jointure basée sur cet opérateur. MERGES n'a de sens que pour un opérateur binaire qui renvoie un boolean et, en pratique, cet opérateur doit représenter l'égalité pour des types de données ou des paires de types de données.

La jointure-union est fondée sur le principe d'ordonner les tables gauche et droite et ensuite de les comparer en parallèle. Ainsi, les deux types de données doivent être capable d'être pleinement ordonnées, et l'opérateur de jointure doit pouvoir réussir seulement pour des paires de valeurs tombant à la << même place >> dans l'ordre de tri. En pratique, cela signifie que l'opérateur de jointure doit se comporter comme l'opérateur égalité. Mais contrairement à la jointure-hachage, où il vaut mieux que les types de données droite et gauche sont les mêmes (ou au moins soient bitwise équivalent), il est possible de faire une jointure-union sur deux types de données distincts tant qu'ils sont logiquement compatibles. Par exemple, l'opérateur d'égalité smallint-contre-integer est susceptible d'opérer une jointure-union. Nous avons seulement besoin d'opérateurs de tri qui organisent les deux types de données en séquences logiquement comparables.

L'exécution d'une jointure-union exige que le système soit capable d'identifier quatre opérateurs rattachés à l'opérateur de jointure-union : la comparaison less-than pour le type de donnée de l'opérande gauche, la comparaison less-than pour le type de donnée de l'opérande droit, la comparaison less-than entre les deux types de donnée et la comparaison greater-than entre les deux types de donnée (il y a en fait quatre opérateurs distincts si l'opérateur de jointure-union a deux types de données d'opérande différents ; mais quand les types d'opérande sont les mêmes, les trois opérateurs less-than sont tous le même opérateur). Il est possible de spécifier ces opérateurs individuellement par leur nom, comme les options respectives SORT1, SORT2, LTCMP et GTCMP. Le système remplira respectivement par défaut les noms <, <, <, > si n'importe lequel d'entre eux est omis quand MERGES est spécifié. De même, MERGES sera supposé être indiqué si n'importe laquelle de ces quatre options apparaît, il est donc possible de seulement spécifier quelques-unes de ces options et de laisser le système compléter le reste.

Les types de données des opérandes des quatre opérateurs de comparaison peuvent être déduits des types d'opérandes de l'opérateur de jointure-union, aussi, exactement comme avec COMMUTATOR, seuls les noms d'opérateurs ont besoin d'être donnés dans ces clauses. À moins que vous ne fassiez des choix particuliers de noms d'opérateurs, il suffit d'écrire MERGES et laisser le système remplir les détails (comme avec COMMUTATOR et NEGATOR, le système est capable de faire des entrées d'opérateur fictives si il vous arrive de définir l'opérateur égalité avant les autres).

Il existe des restrictions additionnelles sur les opérateurs que vous marquez comme jointure-union. Ces restrictions ne sont pas actuellement contrôlées par la commande CREATE OPERATOR mais des erreurs peuvent intervenir lors de l'utilisation de l'opérateur si un des points suivants n'est pas vérifié :

Note : La fonction sous-jacente à un opérateur de jointure-union doit être marquée immuable ou stable. Si elle est volatile, le système n'essaiera jamais d'utiliser l'opérateur pour une jointure union.

Note : Dans les versions de PostgreSQL antérieures à la 7.3, MERGES n'était pas disponible : pour faire un opérateur de jointure union, on devait explicitement écrire SORT1 et SORT2. De plus, les options LTCMP et GTCMP n'existaient pas ; les noms de ces opérateurs ont été rattachés respectivement à < et >.