Traiter la fragmentation
Créer une table t3
avec une colonne id
de
type integer
.
CREATE TABLE t3(id integer );
Désactiver l’autovacuum pour la table t3
.
ALTER TABLE t3 SET (autovacuum_enabled = false );
La désactivation de l’autovacuum ici a un but uniquement pédagogique.
En production, c’est une très mauvaise idée !
Insérer un million de lignes dans la table t3
avec la
fonction generate_series
.
INSERT INTO t3 SELECT generate_series (1 , 1000000 );
Récupérer la taille de la table t3
.
SELECT pg_size_pretty (pg_table_size('t3 ' ));
pg_size_pretty
----------------
35 MB
Supprimer les 500 000 premières lignes de la table
t3
.
DELETE FROM t3 WHERE id <= 500000 ;
Récupérer la taille de la table t3
. Que faut-il en
déduire ?
SELECT pg_size_pretty (pg_table_size('t3 ' ));
pg_size_pretty
----------------
35 MB
DELETE
seul ne permet pas de regagner de la place sur le
disque. Les lignes supprimées sont uniquement marquées comme étant
mortes. Comme l’autovacuum est ici désactivé, PostgreSQL n’a pas encore
nettoyé ces lignes.
Exécuter un VACUUM VERBOSE
sur la table t3
.
Quelle est l’information la plus importante ?
INFO: vacuuming "public.t3"
INFO: "t3": removed 500000 row versions in 2213 pages
INFO: "t3": found 500000 removable, 500000 nonremovable row versions
in 4425 out of 4425 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 3815272
There were 0 unused item pointers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.09 s, system: 0.00 s, elapsed: 0.10 s.
VACUUM
L’indication :
removed 500000 row versions in 2213 pages
indique 500 000 lignes ont été nettoyées dans 2213 blocs (en gros, la
moitié des blocs de la table).
Pour compléter, l’indication suivante :
found 500000 removable, 500000 nonremovable row versions in 4425 out of 4425 pages
reprend l’indication sur 500 000 lignes mortes, et précise que
500 000 autres ne le sont pas. Les 4425 pages parcourues correspondent
bien à la totalité des 35 Mo de la table complète. C’est la première
fois que VACUUM
passe sur cette table, il est normal
qu’elle soit intégralement parcourue.
Récupérer la taille de la table t3
. Que faut-il en
déduire ?
SELECT pg_size_pretty (pg_table_size('t3 ' ));
pg_size_pretty
----------------
35 MB
VACUUM
ne permet pas non plus de gagner en espace
disque. Principalement, il renseigne la structure FSM (free space
map ) sur les emplacements libres dans les fichiers des tables.
Exécuter un VACUUM FULL VERBOSE
sur la table
t3
.
INFO: vacuuming "public.t3"
INFO: "t3": found 0 removable, 500000 nonremovable row versions in 4425 pages
DÉTAIL : 0 dead row versions cannot be removed yet.
CPU: user: 0.10 s, system: 0.01 s, elapsed: 0.21 s.
VACUUM
Récupérer la taille de la table t3
. Que faut-il en
déduire ?
SELECT pg_size_pretty (pg_table_size('t3 ' ));
pg_size_pretty
----------------
17 MB
Là, par contre, nous gagnons en espace disque. Le
VACUUM FULL
reconstruit la table et la fragmentation
disparaît.
Créer une table t4
avec une colonne id
de
type integer
.
CREATE TABLE t4(id integer );
Désactiver l’autovacuum pour la table t4
.
ALTER TABLE t4 SET (autovacuum_enabled = false );
Insérer un million de lignes dans la table t4
avec
generate_series
.
INSERT INTO t4(id ) SELECT generate_series (1 , 1000000 );
Récupérer la taille de la table t4
.
SELECT pg_size_pretty (pg_table_size('t4 ' ));
pg_size_pretty
----------------
35 MB
Supprimer les 500 000 dernières lignes de la table
t4
.
DELETE FROM t4 WHERE id > 500000 ;
Récupérer la taille de la table t4
. Que faut-il en
déduire ?
SELECT pg_size_pretty (pg_table_size('t4 ' ));
pg_size_pretty
----------------
35 MB
Là aussi, nous n’avons rien perdu.
Exécuter un VACUUM
sur la table t4
.
Récupérer la taille de la table t4
. Que faut-il en
déduire ?
SELECT pg_size_pretty (pg_table_size('t4 ' ));
pg_size_pretty
----------------
17 MB
En fait, il existe un cas où il est possible de gagner de l’espace
disque suite à un VACUUM
simple : quand l’espace récupéré
se trouve en fin de table et qu’il est possible de prendre rapidement un
verrou exclusif sur la table pour la tronquer. C’est assez peu fréquent
mais c’est une optimisation intéressante.
Détecter la fragmentation
Créer une table t5
avec deux colonnes : c1
de type integer
et c2
de type
text
.
CREATE TABLE t5 (c1 integer , c2 text );
Désactiver l’autovacuum pour la table t5
.
ALTER TABLE t5 SET (autovacuum_enabled= false );
Insérer un million de lignes dans la table t5
avec
generate_series
.
INSERT INTO t5(c1, c2) SELECT i, 'Ligne ' || i FROM generate_series (1 , 1000000 ) AS i;
CREATE EXTENSION pg_freespacemap;
Cette extension installe une fonction nommée
pg_freespace
, dont la version la plus simple ne demande que
la table en argument, et renvoie l’espace libre dans chaque bloc, en
octets, connu de la Free Space Map .
SELECT count (blkno), sum (avail) FROM pg_freespace('t5 ' :: regclass);
count | sum
-------+-----
6274 | 0
et donc 6274 blocs (soit 51,4 Mo) sans aucun espace vide.
Modifier exactement 200 000 lignes de la table
t5
.
Que rapporte pg_freespace
quant à
l’espace libre de la table t5
?
UPDATE t5 SET c2 = upper (c2) WHERE c1 <= 200000 ;
SELECT count (blkno), sum (avail) FROM pg_freespace('t5 ' :: regclass);
count | sum
-------+-----
7451 | 32
La table comporte donc 20 % de blocs en plus, où sont stockées les
nouvelles versions des lignes modifiées. Le champ avail indique
qu’il n’y a quasiment pas de place libre. (Ne pas prendre la valeur de
32 octets au pied de la lettre, la Free Space Map ne cherche
pas à fournir une valeur précise.)
Exécuter un VACUUM
sur la table t5
.
INFO: vacuuming "public.t5"
INFO: "t5": removed 200000 row versions in 1178 pages
INFO: "t5": found 200000 removable, 1000000 nonremovable row versions
in 7451 out of 7451 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 8685974
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.11 s, system: 0.03 s, elapsed: 0.33 s.
INFO: vacuuming "pg_toast.pg_toast_4160544"
INFO: index "pg_toast_4160544_index" now contains 0 row versions in 1 pages
DÉTAIL : 0 index row versions were removed.
0 index pages have been deleted, 0 are currently reusable.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
INFO: "pg_toast_4160544": found 0 removable, 0 nonremovable row versions in 0 out of 0 pages
DÉTAIL : 0 dead row versions cannot be removed yet, oldest xmin: 8685974
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
VACUUM
Que rapporte pg_freespace
quant à l’espace libre de la
table t5
?
SELECT count (blkno), sum (avail) FROM pg_freespace('t5 ' :: regclass);
count | sum
-------+---------
7451 | 8806816
Il y a toujours autant de blocs, mais environ 8,8 Mo sont à présent
repérés comme libres.
Il faut donc bien exécuter un VACUUM
pour que PostgreSQL
nettoie les blocs et mette à jour la structure FSM, ce qui nous permet
de déduire le taux de fragmentation de la table.
Récupérer la taille de la table t5
.
SELECT pg_size_pretty (pg_table_size('t5 ' ));
pg_size_pretty
----------------
58 MB
Exécuter un VACUUM (FULL, VERBOSE)
sur la table
t5
.
VACUUM (FULL , VERBOSE ) t5;
INFO: vacuuming "public.t5"
INFO: "t5": found 200000 removable, 1000000 nonremovable row versions in 7451 pages
DÉTAIL : 0 dead row versions cannot be removed yet.
CPU: user: 0.49 s, system: 0.19 s, elapsed: 1.46 s.
VACUUM
Récupérer la taille de la table t5
et l’espace libre
rapporté par pg_freespacemap
. Que faut-il en déduire ?
SELECT count (blkno),sum (avail)FROM pg_freespace('t5 ' :: regclass);
count | sum
-------+-----
6274 | 0
SELECT pg_size_pretty (pg_table_size('t5 ' ));
pg_size_pretty
----------------
49 MB
VACUUM FULL
a réécrit la table sans les espaces morts,
ce qui nous a fait gagner entre 8 et 9 Mo. La taille de la table
maintenant correspond bien à celle de l’ancienne table, moins la place
prise par les lignes mortes.
Gestion de l’autovacuum
Créer une table t6
avec une colonne id
de
type integer
.
CREATE TABLE t6 (id integer ) ;
Insérer un million de lignes dans la table t6
:
INSERT INTO t6(id ) SELECT generate_series (1 , 1000000 ) ;
INSERT INTO t6(id ) SELECT generate_series (1 , 1000000 ) ;
Que contient la vue pg_stat_user_tables
pour la
table t6
? Il faudra peut-être attendre une minute. (Si la
version de PostgreSQL est antérieure à la 13, il faudra lancer un
VACUUM t6
.)
SELECT * FROM pg_stat_user_tables WHERE relname = 't6 ' ;
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 0
seq_tup_read | 0
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 0
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 1000000
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:42:43.612269+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:42:43.719195+01
vacuum_count | 0
autovacuum_count | 1
analyze_count | 0
autoanalyze_count | 1
Les deux dates last_autovacuum
et
last_autoanalyze
sont renseignées. Il faudra peut-être
attendre une minute que l’autovacuum passe sur la table (voire plus sur
une instance chargée par ailleurs).
Le seuil de déclenchement de l’autoanalyze est :
autovacuum_analyze_scale_factor
× nombre de lignes
+ autovacuum_analyze_threshold
soit par défaut 10 % × 0 + 50 = 50. Quand il n’y a que des insertions,
le seuil pour l’autovacuum est :
autovacuum_vacuum_insert_scale_factor
× nombre de
lignes
+ autovacuum_vacuum_insert_threshold
soit 20 % × 0 + 1000 = 1000.
Avec un million de nouvelles lignes, les deux seuils sont
franchis.
Avec PostgreSQL 12 ou antérieur, seule la ligne
last_autoanalyze
sera remplie. S’il n’y a que des
insertions, le démon autovacuum ne lance un VACUUM
spontanément qu’à partir de PostgreSQL 13.
Jusqu’en PostgreSQL 12, il faut donc lancer manuellement :
Vérifier le nombre de lignes dans
pg_class.reltuples
.
Vérifions que le nombre de lignes est à jour dans
pg_class
:
SELECT * FROM pg_class WHERE relname = 't6 ' ;
-[ RECORD 1 ]-------+--------
oid | 4160608
relname | t6
relnamespace | 2200
reltype | 4160610
reloftype | 0
relowner | 10
relam | 2
relfilenode | 4160608
reltablespace | 0
relpages | 4425
reltuples | 1e+06
...
L’autovacuum se base entre autres sur cette valeur pour décider s’il
doit passer ou pas. Si elle n’est pas encore à jour, il faut lancer
manuellement :
ce qui est d’ailleurs généralement conseillé après un gros
chargement.
Modifier 60 000 lignes supplémentaires de la table t6
avec :
UPDATE t6 SET id = 1 WHERE id > 940000 ;
Attendre une minute.
Que contient la vue pg_stat_user_tables
pour la
table t6
?
Que faut-il en déduire ?
UPDATE t6 SET id = 0 WHERE id <= 150000 ;
Le démon autovacuum ne se déclenche pas instantanément après
les écritures, attendons un peu :
SELECT * FROM pg_stat_user_tables WHERE relname = 't6 ' ;
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 1
seq_tup_read | 1000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 150000
n_tup_del | 0
n_tup_hot_upd | 0
n_live_tup | 1000000
n_dead_tup | 150000
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:42:43.612269+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:43:43.561288+01
vacuum_count | 0
autovacuum_count | 1
analyze_count | 0
autoanalyze_count | 2
Seul last_autoanalyze
a été modifié, et il reste entre
150 000 lignes morts (n_dead_tup
). En effet, le démon
autovacuum traite séparément l’ANALYZE
(statistiques sur
les valeurs des données) et le VACUUM
(recherche des
espaces morts). Si l’on recalcule les seuils de déclenchement, on trouve
pour l’autoanalyze :
autovacuum_analyze_scale_factor
× nombre de lignes
+ autovacuum_analyze_threshold
soit par défaut 10 % × 1 000 000 + 50 = 100 050, dépassé ici.
Pour l’autovacuum, le seuil est de :
autovacuum_vacuum_insert_scale_factor
× nombre de
lignes
+ autovacuum_vacuum_insert_threshold
soit 20 % × 1 000 000 + 50 = 200 050, qui n’est pas atteint.
Modifier 60 000 lignes supplémentaires de la table t6
avec :
UPDATE t6 SET id = 1 WHERE id > 940000 ;
Attendre une minute.
Que contient la vue pg_stat_user_tables
pour la
table t6
?
Que faut-il en déduire ?
UPDATE t6 SET id = 1 WHERE id > 940000 ;
L’autovacuum ne passe pas tout de suite, les 210 000 lignes mortes au
total sont bien visibles :
SELECT * FROM pg_stat_user_tables WHERE relname = 't6 ' ;
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 3
seq_tup_read | 3000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 210000
n_tup_del | 0
n_tup_hot_upd | 65
n_live_tup | 1000000
n_dead_tup | 210000
n_mod_since_analyze | 60000
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:42:43.612269+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:43:43.561288+01
vacuum_count | 0
autovacuum_count | 1
analyze_count | 0
autoanalyze_count | 2
Mais comme le seuil de 200 050 lignes modifiées à été franchi, le
démon lance un VACUUM
:
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 3
seq_tup_read | 3000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 210000
n_tup_del | 0
n_tup_hot_upd | 65
n_live_tup | 896905
n_dead_tup | 0
n_mod_since_analyze | 60000
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:47:43.740962+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:43:43.561288+01
vacuum_count | 0
autovacuum_count | 2
analyze_count | 0
autoanalyze_count | 2
Noter que n_dead_tup
est revenu à 0.
last_auto_analyze
indique qu’un nouvel ANALYZE
n’a pas été exécuté : seules 60 000 lignes ont été modifiées (voir
n_mod_since_analyze
), en-dessous du seuil de 100 050.
Descendre le facteur d’échelle de la table t6
à 10 %
pour le VACUUM
.
ALTER TABLE t6 SET (autovacuum_vacuum_scale_factor= 0.1 );
Modifier encore 200 000 autres lignes de la table
t6
:
UPDATE t6 SET id = 1 WHERE id > 740000 ;
Attendre une minute.
Que contient la vue pg_stat_user_tables
pour la
table t6
?
Que faut-il en déduire ?
UPDATE t6 SET id = 1 WHERE id > 740000 ;
SELECT * FROM pg_stat_user_tables WHERE relname= 't6 ' ;
-[ RECORD 1 ]-------+------------------------------
relid | 4160608
schemaname | public
relname | t6
seq_scan | 4
seq_tup_read | 4000000
idx_scan | ¤
idx_tup_fetch | ¤
n_tup_ins | 1000000
n_tup_upd | 410000
n_tup_del | 0
n_tup_hot_upd | 65
n_live_tup | 1000000
n_dead_tup | 0
n_mod_since_analyze | 0
n_ins_since_vacuum | 0
last_vacuum | ¤
last_autovacuum | 2021-02-22 17:53:43.563671+01
last_analyze | ¤
last_autoanalyze | 2021-02-22 17:53:43.681023+01
vacuum_count | 0
autovacuum_count | 3
analyze_count | 0
autoanalyze_count | 3
Le démon a relancé un VACUUM
et un ANALYZE
.
Avec un facteur d’échelle à 10 %, il ne faut plus attendre que la
modification de 100 050 lignes pour que le VACUUM
soit
déclenché par le démon. C’était déjà le seuil pour
l’ANALYZE
.