Mont de Marsan, le 25 juillet 2025
Dans un précédent
article, je vous
illustrais à quel point la justesse des statistiques sur les données était
critique pour la performance de vos requêtes. Intéressons-nous aujourd’hui au
paramètre default_statistics_target
et regardons comment il peut nous aider à
améliorer les performances de nos requêtes grâce à des statistiques plus
précises.
Le principe
Pour obtenir les meilleurs plans d’exécution possibles, PostgreSQL a besoin de savoir
combien de blocs de données devront être lus et quelle est la probabilité de trouver
les données en mémoire dans les caches système et PostgreSQL (shared_buffers
).
Pour connaître la quantité de données concernée par chaque requête, PostgreSQL
doit savoir quelle est la taille de chaque ligne et combien de lignes sont
remontées. Il est nécessaire pour cela de se référer au
type des colonnes – un numeric
pèse a priori moins lourd qu’un text
ou
jsonb
– ainsi qu’à la répartition des valeurs dans les colonnes.
L’échantillonnage
Pour ce faire, nul besoin de mesurer chacune des lignes de chacune des tables. Un échantillon assez grand pris aléatoirement est suffisamment représentatif de l’ensemble. PostgreSQL se base sur cette publication de Surajit Chaudhuri, Rajeev Motwani et Vivek Narasayya “Random sampling for histogram construction: how much is enough?” de 1998 pour déterminer combien de lignes suffisent à construire cet échantillon.
Les développeurs se sont appuyés sur cette étude qui montre qu’il suffit de
prendre environ 300 × k lignes
, où k est la « cible statistiques » de
la table, définie globalement par le
paramètre default_statistics_target
. Cette valeur est codée en dur
à plusieurs endroits du code source de
analyze.c
:
std_typanalyze(VacAttrStats *stats)
{
…
stats->minrows = 300 * stats->attstattarget;
…
}
On comprend que la taille minimale de l’échantillon est donc de 300 fois la
valeur de default_statistics_target
.
PostgreSQL utilise donc par défaut un échantillon de 30 000 lignes pour
calculer ses statistiques :
postgres=# SHOW default_statistics_target; -- nous avons bien la valeur par défaut
default_statistics_target
---------------------------
100
(1 ligne)
postgres=# ANALYZE VERBOSE matable; -- confirmation de la taille de l'échantillon
INFO: analyzing "public.matable" -- "30000 rows in sample" ci-dessous
INFO: "matable": scanned 7280 of 7280 pages, containing 990000 live rows and 0 dead rows; 30000 rows in sample, 990000 estimated total rows
ANALYZE
Temps : 376,310 ms
La résolution
Pour chaque colonne, PostgreSQL calcule un certain nombre de statistiques.
Elles peuvent être consultées dans la vue
pg_stats
. Deux
d’entre elles, la liste des most common values (MCV) et
l’histogramme de valeur sont particulièrement importantes.
Par exemple, une liste MCV pour une colonne prix
d’une table produit
pourrait avoir
cette forme :
postgres=# SELECT most_common_vals, most_common_freqs
FROM pg_stats
WHERE attname='prix' \gx
-[ RECORD 1 ]-----+----------------------------------
most_common_vals | {4,99,9.99,19.99,29.99,49.99}
most_common_freqs | {0.3,0.1,0.05,0.047,0.03}
Ces cinq prix sont les plus fréquents dans la colonne prix
. 30 % des valeurs sont
égales à 4,99 et 3 % égales à 49,99.
Pour illustrer la notion d’histogramme, on peut considérer une colonne poids
d’une table produit
:
postgres=# SELECT histogram_bounds FROM pg_stats WHERE attname='poids' \gx
-[ RECORD 1 ]----+---------------------------
histogram_bounds | {0,30,200,600,1000,10000}
Avec ces 6 bornes, notre histogramme dispose de 5 intervalles. L’écart entre les bornes est ajusté afin que chaque intervalle corresponde sensiblement au même nombre de lignes. Il y a donc environ autant de produits dont le poids est compris entre 0 et 30 g que de produits compris entre 1 et 10 kg.
Mais quelle est la bonne quantité de valeurs fréquentes à retenir pour la liste
MCV ? Et combien d’intervalles doit comporter notre histogramme calculé pour les
valeurs scalaires ? En regardant dans les fonctions compute_distinct_stats()
et
compute_scalar_stats()
, nous observons ces initialisations :
// dans compute_distinct_stats()
int num_mcv = stats->attstattarget;
// dans compute_scalar_stats()
int num_mcv = stats->attstattarget;
int num_bins = stats->attstattarget;
Nous constatons que c’est de nouveau le paramètre default_statistics_target
qui est utilisé. Par défaut, pour chaque colonne de chaque table, la liste MCV
comportera donc jusqu’à 100 valeurs, et l’histogramme aura 100 intervalles.
La cible statistique sert aussi et surtout à indiquer une résolution pour les statistiques,
suffisamment élevée pour qu’elles soient précises et utiles, sans trop l’être pour
ne pas pénaliser le calcul par ANALYZE
.
Un paramètre, deux fonctions
Nous voyons donc que default_statistics_target
a deux fonctions distinctes dans
la collecte des statistiques par ANALYZE
, une pour la taille de l’échantillon,
une autre pour la résolution de certaines statistiques, listes MCV et
histogrammes.
Les études nous ont montré qu’augmenter la taille de l’échantillon n’apporte que peu d’information supplémentaire à partir d’un certain seuil. En revanche, la résolution des statistiques peut parfois être insuffisante, surtout pour l’histogramme, quand on a des valeurs avec une distribution peu régulière, voire erratique.
Grâce à un histogramme plus fin, l’optimiseur pourra se rendre compte que certains filtres sont plus sélectifs et ajuster le plan d’exécution en conséquence.
Exemple
Considérons la table mesure
suivante :
CREATE TABLE mesure (
id SERIAL PRIMARY KEY,
valeur_a INT,
valeur_b INT
);
CREATE INDEX idx_valeura ON mesure(valeur_a);
CREATE INDEX idx_valeurb ON mesure(valeur_b);
puis insérons quelques données :
-- insertion de 1000 lignes de 1 à 1000
-- dans un ordre aléatoire
INSERT INTO mesure (valeur_a, valeur_b)
SELECT x,x
FROM generate_series(1, 1000) AS x
ORDER BY random();
-- insertion de 999000 lignes d'entiers
-- entre 1 et 1000 concentré autour des
-- multiples de 10
INSERT INTO mesure (valeur_a, valeur_b)
WITH x AS ( SELECT random() AS n, random() AS r FROM generate_series(1, 999000) )
SELECT
(floor(n*100)*10 + r)::int,
(floor(n*100)*10 + r)::int
FROM x
ORDER BY random();
Ici, nous avons inséré des nombres entiers concentrés sur les dizaines, avec très peu de valeurs entre :
postgres=# SELECT count(*) FROM mesure WHERE floor(valeur_a::numeric)%10 IN (0,1);
count
--------
990200
postgres=# SELECT count(*) FROM mesure WHERE floor(valeur_a::numeric)%10 NOT IN (0,1);
count
------
800
(1 ligne)
Nous définissons une cible statistique de 100 pour la colonne valeur_a
et de 1000 pour
valeur_b
. Ceci nous permettra de voir l’impact de la résolution de l’histogramme entre
100 et 1000 intervalles.
ALTER TABLE mesure ALTER COLUMN valeur_a SET STATISTICS 100;
ALTER TABLE mesure ALTER COLUMN valeur_b SET STATISTICS 1000;
À noter que la taille d’échantillon sera de 300 000 lignes dans les deux cas :
postgres=# ANALYZE VERBOSE mesure;
INFO: analyzing "public.mesure"
INFO: "mesure": scanned 5406 of 5406 pages, containing 1000000 live rows and 0 dead rows; 300000 rows in sample, 1000000 estimated total rows
ANALYZE
Nous pouvons vérifier la taille des histogrammes :
postgres=# SELECT array_length(histogram_bounds,1) FROM pg_stats WHERE attname='valeur_a';
SELECT array_length(histogram_bounds,1) FROM pg_stats WHERE attname='valeur_b';
array_length
--------------
101
(1 ligne)
array_length
--------------
252
(1 ligne)
Le premier comporte bien 100 intervalles, (101 bornes) et le second en a autant que ANALYZE
en a jugé nécessaire, avec 1000 intervalles au maximum.
Lançons maintenant les requêtes suivantes, pour des valeurs peu fréquentes :
EXPLAIN ANALYZE SELECT * FROM mesure WHERE valeur_a IN (502, 503, 504);
EXPLAIN ANALYZE SELECT * FROM mesure WHERE valeur_b IN (502, 503, 504);
QUERY PLAN
----------------------------------------------------------------------------------
Bitmap Heap Scan on mesure (cost=58.06..5401.70 rows=4229 width=12)
(actual time=0.038..0.041 rows=3 loops=1)
Recheck Cond: (valeur_a = ANY ('{502,503,504}'::integer[]))
Heap Blocks: exact=3
-> Bitmap Index Scan on idx_valeura (cost=0.00..57.00 rows=4229 width=0)
(actual time=0.021..0.021 rows=3 loops=1)
Index Cond: (valeur_a = ANY ('{502,503,504}'::integer[]))
Planning Time: 0.164 ms
Execution Time: 0.077 ms
(7 lignes)
QUERY PLAN
----------------------------------------------------------------------------------
Bitmap Heap Scan on mesure (cost=13.35..52.19 rows=10 width=12)
(actual time=0.022..0.032 rows=3 loops=1)
Recheck Cond: (valeur_b = ANY ('{502,503,504}'::integer[]))
Heap Blocks: exact=3
-> Bitmap Index Scan on idx_valeurb (cost=0.00..13.34 rows=10 width=0)
(actual time=0.015..0.015 rows=3 loops=1)
Index Cond: (valeur_b = ANY ('{502,503,504}'::integer[]))
Planning Time: 0.092 ms
Execution Time: 0.045 ms
(7 lignes)
Ici, pour 3 lignes récupérées, l’estimation initiale de 10 est nettement
meilleure pour valeur_b
dont l’histogramme est mieux défini que pour valeur_a
à 4229.
Dans des scénarios plus complexes, cette différence peut influencer les décisions du planificateur. Il pourra choisir un parcours de table séquentiel au lieu de passer par un index par exemple, pensant que la requête n’est pas assez sélective. De la même manière, il pourra choisir une jointure nested loop adaptée aux petits ensembles au lieu de hash join pensant pouvoir économiser l’écriture d’une table de hachage.
À retenir
Augmenter default_statistics_target
permet d’obtenir des statistiques de
meilleures qualité. L’augmentation de la résolution des statistiques – liste MCV
et histogramme – permet d’avoir une meilleure estimation du nombre de lignes
retournées pour un nœud donné d’un plan d’execution et potentiellement, obtenir
de meilleures performances.
Pour aller plus loin, je vous invite à revoir la conférence Voyage au centre des statistiques de Louise Leinweber à PGDay FR 2025. Nos formations DEVPG et PERF1 incluent aussi depuis cette année un nouveau module dédié à la compréhension des statistiques utilisées par le planificateur de PostgreSQL.