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 être la cause de requêtes lentes, 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.
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 :
Une façon d'opérer est d'omettre la clause commutator
dans
le premier opérateur que vous définissez et ensuite d'en insérer une
dans la définition du second opérateur. Puisque
postgresql sait que les opérateurs
commutatifs vont par paire, quand il voit la seconde définition, il
retourne instantanément remplir la clause commutator
manquante dans la première définition.
L'autre façon, plus directe, est de simplement inclure les clauses
commutator
dans les deux définitions. quand
postgresql traite la première définition et
réalise que la clause commutator
se réfère à un opérateur
inexistant, le système va créer une entrée provisoire pour cet opérateur
dans le catalogue système. Cette entrée sera pourvue seulement de
données valides pour le nom de l'opérateur, les types d'opérande droit
et gauche et le type du résultat, puisque c'est tout ce que
postgresql peut déduire à ce point. la
première entrée du catalogue pour l'opérateur sera liée à cette entrée
provisoire. Plus tard, quand vous définirez le second opérateur, le
système mettra à jour l'entrée provisoire avec les informations
additionnelles fournies par la seconde définition. Si vous essayez
d'utiliser l'opérateur provisoire avant qu'il ne soit complété, vous
aurez juste un message d'erreur.
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.
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 < |
scalarlesel pour <= |
scalargtsel pour > |
scalargesel pour >= |
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
, scalarlesel
,
scalargtsel
et scalargesel
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).
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 < |
scalarlejoinsel pour <= |
scalargtjoinsel pour > |
scalargejoinsel pour >= |
areajoinsel pour des comparaisons basées sur une aire 2D |
positionjoinsel pour des comparaisons basées sur des positions 2D |
contjoinsel pour des comparaisons basées sur un appartenance 2D |
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é doit
représenter l'égalité pour certains types de données ou paire de type 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 une certaine forme d'égalité. Dans la plupart des cas, il
est seulement pratique de supporter le hachage pour les opérateurs qui
prennent le même type de données sur chaque côté. Néanmoins, quelque
fois, il est possible de concevoir des fonctions de hachage compatibles
pour deux type de données, voire plus ; c'est-à-dire pour les
fonctions qui généreront les mêmes codes de hachage pour des valeurs
égales même si elles ont des représentations différentes. Par exemple,
il est assez simple d'arranger cette propriété lors du hachage d'entiers
de largeurs différentes.
Pour être marqué hashes
, l'opérateur de jointure doit
apparaître dans une famille 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 famille d'opérateur n'existe pas. Le
système a besoin de la famille 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 créer des fonctions de découpage appropriées avant de
pouvoir créer la famille 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.
Un opérateur joignable par hachage doit avoir un commutateur (lui-même si les types de données des deux opérandes sont identiques, ou un opérateur d'égalité relatif dans le cas contraire) qui apparaît dans la même famille d'opérateur. Si ce n'est pas le cas, des erreurs du planificateur pourraient apparaître quand l'opérateur est utilisé. De plus, une bonne idée (mais pas obligatoire) est qu'une famille d'opérateur de hachage supporte les tupes de données multiples pour fournir des opérateurs d'égalité pour chaque combinaison des types de données ; cela permet une meilleure optimisation.
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.
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).
MERGES
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 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.
Pour être marqué MERGES
, l'opérateur de jointure doit
apparaître en tant que membre d'égalité d'une famille opérateur d'index
btree
. Ceci n'est pas forcé quand vous créez l'opérateur puisque, bien
sûr, la famille d'opérateur référente n'existe pas encore. Mais
l'opérateur ne sera pas utilisé pour les jointures de fusion sauf si
une famille d'opérateur correspondante est trouvée. L'option
MERGES
agit en fait comme une aide pour le planificateur
lui indiquant qu'il est intéressant de chercher une famille d'opérateur
correspondant.
Un opérateur joignable par fusion doit avoir un commutateur (lui-même si les types de données des deux opérateurs sont identiques, ou un opérateur d'égalité en relation dans le cas contraire) qui apparaît dans la même famille d'opérateur. Si ce n'est pas le cas, des erreurs du planificateur pourraient apparaître quand l'opérateur est utilisé. De plus, une bonne idée (mais pas obligatoire) est qu'une famille d'opérateur de hachage supporte les tupes de données multiples pour fournir des opérateurs d'égalité pour chaque combinaison des types de données ; cela permet une meilleure optimisation.
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.