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.

Alain Lesage

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.


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.