Comme nous l'avons vu dans la section précédente, le planificateur de requêtes a besoin d'estimer le nombre de lignes récupérées par une requête pour faire les bons choix dans ses plans de requêtes. Cette section fournit un aperçu sur les statistiques que le système utilise pour ces estimations.
Un élément des statistiques est le nombre total d'entrées dans chaque
table et index, ainsi que le nombre de blocs disque occupés par chaque table
et index. Cette information est conservée dans la table
pg_class
sur les colonnes reltuples
et
relpages
. Nous pouvons la regarder avec des
requêtes comme celle-ci :
SELECT relname, relkind, reltuples, relpages FROM pg_class WHERE relname LIKE 'tenk1%'; relname | relkind | reltuples | relpages ----------------------+---------+-----------+---------- tenk1 | r | 10000 | 358 tenk1_hundred | i | 10000 | 30 tenk1_thous_tenthous | i | 10000 | 30 tenk1_unique1 | i | 10000 | 30 tenk1_unique2 | i | 10000 | 30 (5 rows)
Ici, nous pouvons voir que tenk1
contient 10000
lignes, comme pour ses index, mais que les index sont bien plus petits que la
table (ce qui n'est pas surprenant).
Pour des raisons d'efficacité, reltuples
et
relpages
ne sont pas mises à jour en temps réel, et
contiennent alors souvent des valeurs un peu obsolètes. Elles
sont mises à jour par les commandes VACUUM
, ANALYZE
et quelques commandes DDL comme CREATE INDEX
.
Une opération VACUUM
ou ANALYZE
qui ne parcourt pas la table entièrement
(ce qui est le cas le plus fréquent) augmentera de façon incrémentale
la valeur de reltuples
sur la base de la partie
de la table qu'elle a parcourue, résultant en une valeur approximative.
Dans tous les cas, le planificateur mettra à l'échelle
les valeurs qu'il aura trouvées dans pg_class
pour
correspondre à la taille physique de la table, obtenant ainsi une
approximation plus proche de la réalité.
La plupart des requêtes ne récupèrent qu'une fraction des lignes dans une
table à cause de clauses WHERE
qui restreignent les lignes à
examiner. Du coup, le planificateur a besoin d'une estimation de la
sélectivité des clauses WHERE
, c'est-à-dire la
fraction des lignes qui correspondent à chaque condition de la clause
WHERE
. L'information utilisée pour cette tâche est stockée dans
le catalogue système pg_statistic
.
Les entrées de pg_statistic
sont mises à jour par
les commandes ANALYZE
et VACUUM ANALYZE
et sont toujours approximatives même si elles ont été mises à jour récemment.
Plutôt que de regarder directement dans
pg_statistic
, il vaut mieux voir sa vue
pg_stats
lors d'un examen manuel des statistiques.
pg_stats
est conçue pour être plus facilement
lisible. De plus, pg_stats
est lisible par tous
alors que pg_statistic
n'est lisible que par un
superutilisateur (ceci empêche les utilisateurs non privilégiés d'apprendre
certaines choses sur le contenu des tables appartenant à d'autres personnes à
partir des statistiques. La vue pg_stats
est restreinte
pour n'afficher que les lignes des tables lisibles par l'utilisateur courant).
Par exemple, nous pourrions lancer :
SELECT attname, inherited, n_distinct, array_to_string(most_common_vals, E'\n') as most_common_vals FROM pg_stats WHERE tablename = 'road'; attname | inherited | n_distinct | most_common_vals ---------+-----------+------------+------------------------------------ name | f | -0.363388 | I- 580 Ramp+ | | | I- 880 Ramp+ | | | Sp Railroad + | | | I- 580 + | | | I- 680 Ramp name | t | -0.284859 | I- 880 Ramp+ | | | I- 580 Ramp+ | | | I- 680 Ramp+ | | | I- 580 + | | | State Hwy 13 Ramp (2 rows)
Notez que deux lignes sont affichées pour la même colonne, une correspondant
à la hiérarchie d'héritage complète commençant à la table
road
(inherited
=t
),
et une autre incluant seulement la table road
elle-même
(inherited
=f
).
Les informations stockées dans
pg_statistic
par ANALYZE
,
en particulier le nombre maximum
d'éléments dans les tableaux most_common_vals
et
histogram_bounds
pour chaque colonne, peuvent être
définies colonne par colonne en utilisant la commande ALTER
TABLE SET STATISTICS
ou globalement en initialisant la variable de
configuration default_statistics_target. La limite par
défaut est actuellement de 100 entrées. Augmenter la limite pourrait
permettre des estimations plus précises du planificateur, en particulier
pour les colonnes ayant des distributions de données irrégulières, au prix
d'un plus grand espace consommé dans pg_statistic
et
d'un temps plus long pour calculer les estimations. En revanche, une limite
plus basse pourrait être suffisante pour des colonnes avec des distributions de
données simples.
Le Chapitre 71 donne plus de détails sur l'utilisation des statistiques par le planificateur.
Il est habituel de voir des requêtes lentes tourner avec de mauvais plans d'exécution, car plusieurs colonnes utilisées dans les clauses de la requête sont corrélées. L'optimiseur part normalement du principe que toutes les conditions sont indépendantes les unes des autres, ce qui est faux quand les valeurs des colonnes sont corrélées. Les statistiques classiques, du fait qu'il s'agit par nature de statistiques sur une seule colonne, ne peuvent pas capturer d'information sur la corrélation entre colonnes. Toutefois, PostgreSQL a la possibilité de calculer des statistiques multivariées, qui peuvent capturer une telle information.
Comme le nombre de combinaisons de colonnes est très important, il n'est pas possible de calculer les statistiques multivariées automatiquement. À la place, des objets statistiques étendus, plus souvent appelés simplement objets statistiques, peuvent être créés pour indiquer au serveur qu'il faut obtenir des statistiques sur un ensemble intéressant de colonnes.
Les objets statistiques sont créés en utilisant la commande CREATE STATISTICS. La création de tels objets crée seulement une entrée dans le
catalogue pour exprimer l'intérêt dans cette statistique. La vraie
récupération de données est effectuée par ANALYZE
(soit
une commande manuelle, soit une analyse automatique en tâche de fond). Les
valeurs collectées peuvent être examinées dans le catalogue pg_statistic_ext_data
.
ANALYZE
calcule des statistiques étendues basées sur le
même ensemble de lignes de la table qu'il utilise pour calculer les
statistiques standard sur une seule colonne. Puisque la taille
d'échantillon peut être augmentée en augmentant la cible de statistiques de
la table ou de n'importe laquelle de ses colonnes (comme décrit dans
la section précédente), une plus grande cible de statistiques donnera
normalement des statistiques étendues plus précises, mais nécessitera
également plus de temps pour les calculer.
La section suivante décrit les types de statistiques étendues qui sont actuellement supportées.
Le type le plus simple de statistiques étendues trace les
dépendances fonctionnelles , un concept utilisé
dans les définitions des formes normales des bases de données. On dit qu'une colonne
b
est fonctionnellement dépendante d'une
colonne a
si la connaissance de la valeur de
a
est suffisante pour déterminer la valeur de
b
, et donc qu'il n'existe pas deux lignes ayant
la même valeur de a
avec des valeurs
différentes de b
. Dans une base de données
complètement normalisée, les dépendances fonctionnelles ne devraient
exister que sur la clé primaire et les superclés. Toutefois, dans la
pratique, beaucoup d'ensembles de données ne sont pas totalement normalisés
pour de nombreuses raisons ; une dénormalisation intentionnelle pour des
raisons de performances est un exemple courant. Même dans une base de
données totalement normalisée, il peut y avoir une corrélation partielle
entre des colonnes, qui peuvent être exprimées comme une dépendance
fonctionnelle partielle.
L'existence de dépendances fonctionnelles a un impact direct sur la précision de l'estimation pour certaines requêtes. Si une requête contient des conditions à la fois sur des colonnes indépendantes et sur des colonnes dépendantes, les conditions sur les colonnes dépendantes ne réduisent plus la taille du résultat ; mais sans la connaissance de cette dépendance fonctionnelle, l'optimiseur de requêtes supposera que les conditions sont indépendantes, avec pour résultat une taille de résultat sous-estimée.
Pour informer l'optimiseur des dépendances fonctionnelles,
ANALYZE
peut collecter des mesures sur des dépendances
entre colonnes. Évaluer le degré de dépendance entre tous les
ensembles de colonnes aurait un coût prohibitif, c'est pourquoi la
collecte de données est limitée aux groupes de colonnes apparaissant
ensemble dans un objet statistique défini avec l'option
dependencies
. Il est conseillé de ne créer des
dépendences
statistiques que pour des groupes de
colonnes fortement corrélées, pour éviter un surcoût à la fois
dans ANALYZE
et plus tard lors de la planification de
requête.
Voici un exemple de collecte de statistiques fonctionnellement dépendantes :
CREATE STATISTICS stts (dependencies) ON city, zip FROM zipcodes; ANALYZE zipcodes; SELECT stxname, stxkeys, stxddependencies FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid) WHERE stxname = 'stts'; stxname | stxkeys | stxddependencies ---------+---------+------------------------------------------ stts | 1 5 | {"1 => 5": 1.000000, "5 => 1": 0.423130} (1 row)
On peut voir ici que la colonne 1 (zip code) détermine complètement la colonne 5 (city) et que donc le coefficient est 1.0, alors que la ville ne détermine le code postal qu'environ 42% du temps, ce qui veut dire qu'il y a beaucoup de villes (58%) qui sont représentées par plus d'un seul code postal.
Lors du calcul de la sélectivité d'une requête impliquant des colonnes fonctionnellement dépendantes, le planificateur ajoute l'estimation de sélectivité par condition en utilisant les coefficients de dépendance afin de ne pas produire de résultats sous-estimés.
Les dépendances fonctionnelles sont pour le moment uniquement appliquées
pour les conditions sur une simple égalité entre une colonne et une
valeur constante et des clauses IN
contenant des
valeurs constantes. Elles ne sont pas utilisées pour améliorer
l'estimation sur les conditions d'égalité entre deux colonnes ou la
comparaison d'une colonne avec une expression ni pour les clauses
d'intervalle, LIKE
ou tout autre type de condition.
Lors d'une estimation avec des dépendances fonctionnelles, l'optimiseur part du principe que les conditions sur les colonnes impliquées sont compatibles et donc redondantes. Si elles sont incompatibles, l'estimation correcte devrait être zéro ligne, mais cette possibilité n'est pas envisagée. Par exemple, dans une requête telle que
SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '94105';
l'optimiseur négligera la clause city
puisqu'elle ne changera pas la sélectivité, ce qui est correct. Par
contre, il fera la même supposition pour
SELECT * FROM zipcodes WHERE city = 'San Francisco' AND zip = '90210';
bien qu'il n'y ait en réalité aucune ligne satisfaisant cette requête. Toutefois, les statistiques de dépendances fonctionnelles ne fournissent pas suffisamment d'information pour en arriver à cette conclusion.
Pour beaucoup de situations pratiques, cette supposition est généralement correcte ; par exemple, l'application pourrait contenir une interface graphique qui n'autorise que la sélection de villes et codes postaux compatibles pour l'utilisation dans une requête. Mais si ce n'est pas le cas, les dépendances fonctionnelles pourraient ne pas être une solution viable.
Les statistiques sur une seule colonne stockent le nombre de valeurs
distinctes pour chaque colonne. Les estimations du nombre de valeurs
distinctes combinant plus d'une colonne (par exemple, pour
GROUP BY a, b
) sont souvent fausses quand l'optimiseur
ne dispose que de données statistiques par colonne, avec pour conséquence
le choix de mauvais plans.
Afin d'améliorer de telles estimations, ANALYZE
peut
collecter des statistiques n-distinct pour des groupes de colonne. Comme
précédemment, il n'est pas envisageable de le faire pour tous les
regroupements possibles, ainsi les données ne sont collectées
que pour ceux apparaissant ensemble dans un objet
statistique défini avec l'option ndistinct
. Des
données seront collectées pour chaque combinaison possible de deux
colonnes ou plus dans l'ensemble de colonnes listées.
En continuant avec l'exemple précédent, le nombre n-distinct dans une table de code postaux pourrait ressembler à ceci :
CREATE STATISTICS stts2 (ndistinct) ON city, state, zip FROM zipcodes; ANALYZE zipcodes; SELECT stxkeys AS k, stxdndistinct AS nd FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid) WHERE stxname = 'stts2'; -[ RECORD 1 ]-------------------------------------------------------- k | 1 2 5 nd | {"1, 2": 33178, "1, 5": 33178, "2, 5": 27435, "1, 2, 5": 33178} (1 row)
Cela indique qu'il y a trois combinaisons de colonnes qui ont 33178 valeurs distinctes : le code postal et l'état; le code postal et la ville; et le code postal, la ville et l'état (le fait qu'ils soient tous égaux est attendu puisque le code postal seul est unique dans cette table). D'un autre côté, la combinaison de la ville et de l'état n'a que 27435 valeurs distinctes.
Il est conseillé de créer des objets statistiques
ndistinct
uniquement sur les combinaisons de colonnes
réellement utilisées pour des regroupements, et pour lesquelles
les mauvaises estimations du nombre de groupe a pour conséquence de
mauvais plans. Sinon le temps consommé par ANALYZE
serait gaspillé.
Un autre type de statistiques enregistrées pour chaque colonne est les listes des valeurs les plus communes. Ceci permet des estimations très précises pour les colonnes individuelles, mais pourrait résulter en des estimations significativement mauvaises pour les requêtes ayant des filtres sur plusieurs colonnes.
Pour améliorer ces estimations, ANALYZE
peut récupérer
des listes MCV sur des combinaisons de colonnes. De façon similaire aux
dépendances fonctionnelles et coefficients de valeurs distinctes, il
n'est pas possible de le faire pour chaque regroupement de colonnes. Ceci
est encore plus vrai dans ce cas, car la liste MCV (contrairement aux
dépendances fonctionnelles et coefficients de valeurs distinctes),
enregistre les valeurs les plus communes. Donc les données ne sont
récupérées que pour les groupes de colonnes apparaissant dans un objet
statistique défini avec l'option mcv
option.
En continuant sur l'exemple précédent, la liste MCV pour une table de codes ZIP pourrait ressembler à ce qui suit (contrairement aux types plus simples de statistiques, une fonction est requise pour inspecter le contenu du MCV) :
CREATE STATISTICS stts3 (mcv) ON city, state FROM zipcodes; ANALYZE zipcodes; SELECT m.* FROM pg_statistic_ext join pg_statistic_ext_data on (oid = stxoid), pg_mcv_list_items(stxdmcv) m WHERE stxname = 'stts3'; index | values | nulls | frequency | base_frequency -------+------------------------+-------+-----------+---------------- 0 | {Washington, DC} | {f,f} | 0.003467 | 2.7e-05 1 | {Apo, AE} | {f,f} | 0.003067 | 1.9e-05 2 | {Houston, TX} | {f,f} | 0.002167 | 0.000133 3 | {El Paso, TX} | {f,f} | 0.002 | 0.000113 4 | {New York, NY} | {f,f} | 0.001967 | 0.000114 5 | {Atlanta, GA} | {f,f} | 0.001633 | 3.3e-05 6 | {Sacramento, CA} | {f,f} | 0.001433 | 7.8e-05 7 | {Miami, FL} | {f,f} | 0.0014 | 6e-05 8 | {Dallas, TX} | {f,f} | 0.001367 | 8.8e-05 9 | {Chicago, IL} | {f,f} | 0.001333 | 5.1e-05 ... (99 rows)
Ceci indique que la combinaison la plus commune des colonnes city et state est Washington DC, avec la fréquence réelle (dans cet exemple) de 0,35 %. La fréquence de base de la combinaison (telle qu'elle est calculée par les fréquences par mono colonne) est seulement de 0,0027 %, résultant en une sous-estimation très forte.
Il est préférable de créer des objets statistiques MCV
uniquement sur les combinaisons de colonnes réellement utilisées ensemble
dans des filtres et pour lesquelles la mauvaise estimation du nombre de
groupes a pour conséquence de mauvais plans. Dans le cas contraire, le
ANALYZE
et le temps de planification sont juste
gâchés.