samedi 12 avril 2008
Intérêt du VACUUM FULL lors d'UPDATE fréquents - Exemple concret
Par david techer, samedi 12 avril 2008 à 12:12 :: PostGIS et PostgreSQL
Souvent on me pose la question à  quoi sert l'option FULL du VACUUM? Je ne vais pas décortiquer ici tout ce que fais le FULL mais juste montrer un exemple pratique d'un UPDATE sur toute une table.
Pour se faire, je vais partir d'un exemple pris du bouquin de référence (que j'avais déjà  introduis dans un précédent billet)...
CHARGEMENT DES DONNEES
On va commencer par télécharger les sources en faisantwget http://www.conjectrix.com/pgbook/source2/bookdata.tar.gzOn les décompresse ensuite
tar xvzf bookdata.tar.gzPour rester en conformité avec les données, on va se créer une base
performance
et un utilisateur korry
postgres@bremko:~$ createdb performance CREATE DATABASE postgres@bremko:~$ createuser -s korry CREATE USEROn se rend dans le sous-répertoire chapter04 qui contient un fichier sql
recalls.sql
contenant la structure de la table recalls qui va nous servir. On en profite par la même occasion de charger le fichier. Au passage, on notera que le fichier fait 236Mo
postgres@bremko:~/chapter04$Au passage, on se connecte à  la base performance pour la suite, on obtient le descriptif suivantdu -hs recalls.sql
236M recalls.sql postgres@bremko:~/chapter04$psql -d performance -f recalls.sql
You are now connected to database "performance" as user "korry". CREATE TABLE CREATE INDEX CREATE INDEX postgres@bremko:~/chapter04$
performance=# \d recalls Table "public.recalls" Column | Type | Modifiers -------------+-------------------------+----------- record_id | numeric(9,0) | campno | character(9) | maketxt | character(25) | modeltxt | character(25) | yeartxt | character(4) | mfgcampno | character(10) | compdesc | character(75) | mgftxt | character(30) | bgman | character(8) | endman | character(8) | vet | character(1) | potaff | numeric(9,0) | ndate | character(8) | odate | character(8) | influenced | character(4) | mfgname | character(30) | rcdate | character(8) | datea | character(8) | rpno | character(3) | fmvss | character(3) | desc_defect | character varying(2000) | con_defect | character varying(2000) | cor_action | character varying(2000) | Indexes: "recall_potaff" btree (potaff) "recall_record_id" btree (record_id)
CAS D'UN SEUL UPDATE ET ETUDE
Examinons maintenant la taille de la table
performance=# SELECT relname,reltuples,relpages,pg_relation_size(relname) FROM pg_class WHERE relname='recalls';
relname | reltuples | relpages | pg_relation_size
---------+-----------+----------+------------------
recalls | 39241 | 4400 | 36044800
(1 row)
Une page pour PostgreSQL est de 8192 bytes. Ce que nous confirme la commande suivante
performance=# show block_size ; block_size ------------ 8192 (1 row)La table a 39241 tuples et 4400 pages. Ce qui donne donc 4400x8192=36044800.Ce que confirme la fonctionnalité
pg_relation_size()
. Ici bien sà »r, je n'ai pas tenu compte de la place qu'occupait les index. On peut aussi le vérifier sur le disque mais pour celà  , il faut remonter l'OID associé à  la table recalls. Ce qui s'obtient en faisant
performance=# SELECT relname,relfilenode FROM pg_class WHERE relname='recalls';
relname | relfilenode
---------+-------------
recalls | 188438
(1 row)
Par la suite, j'ai
Maintenant si l'option stats_row_level est active depuis le démarrage du serveur (à  ne pas faire dans un environnement de prod...), on a aussipostgres@bremko:~/chapter04$ ls -al /mnt/pgdata/base/188437/188438 -rw------- 1 postgres postgres36044800
2008-04-12 10:50 /mnt/pgdata/base/188437/188438
performance=# \x
Expanded display is on.
performance=# Select * from pg_stat_user_tables ;
-[ RECORD 1 ]----+--------
relid | 188438
schemaname | public
relname | recalls
seq_scan | 2
seq_tup_read | 78482
idx_scan | 0
idx_tup_fetch | 0
n_tup_ins | 39241
n_tup_upd | 0
n_tup_del | 0
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
Le champs n_tup_ins
indique le nombre de tuples inséré depuis le démarrage du serveur soit 39241 tuples.
On va maintenant modifier la table en faisant un simple UPDATE sur le champs potaff
performance=# START TRANSACTION ;
START TRANSACTION
performance=# UPDATE recalls SET potaff = potaff +1;
UPDATE 39241
performance=# END TRANSACTION ;
COMMIT
Revenons à  une de nos précédentes requêtes
performance=# SELECT relname,reltuples,relpages,pg_relation_size(relname) FROM pg_class WHERE relname='recalls';
relname | reltuples | relpages | pg_relation_size
---------+-----------+----------+------------------
recalls | 39241 | 4400 | 71868416
(1 row)
On s'apercoit qu'un simple UPDATE sur la table entière à  suffit pour doubler la taille de la table! Ce que confirme aussi la commande suivante
postgres@bremko:~/chapter04$ ls -al /mnt/pgdata/base/188437/188438
-rw------- 1 postgres postgres 71868416
2008-04-12 10:50 /mnt/pgdata/base/188437/188438
En revanche le nombre de pages n'est pas à  jour. Un simple ANALYZE sur la table suffira pour le mettre à  jour
performance=# ANALYZE recalls ; ANALYZE performance=# SELECT relname,reltuples,relpages,pg_relation_size(relname) FROM pg_class WHERE relname='recalls'; relname | reltuples | relpages | pg_relation_size ---------+-----------+----------+------------------ recalls | 39493 |Le nombre de pages a aussi quasiment doublé! C'est maintenant qu'intervient le FULL! Bien sà »r, on supposera aussi qu'aucune autre activité n'a lieu pendant ce temps sur la base. PostgreSQL en fait garde une trace des lignes qui sont sujet à  modification et le rend non accessibles. Tout celà  repose en grande partie sur le principe du MVCC et isolation de transaction qu'à  choisi PostgreSQL qui par défaut est en REAL COMMITTED. D'ailleurs PostgreSQL ne supporte que deux niveaux d'isolation8773
|71868416
(1 row)
performance=# show transaction_isolation ;
transaction_isolation
-----------------------
read committed
(1 row)
So let's go now for notre FULL de la mort qui tue le diable
performance=# VACUUM FULL recalls ; VACUUM performance=# SELECT relname,reltuples,relpages,pg_relation_size(relname) FROM pg_class WHERE relname='recalls'; relname | reltuples | relpages | pg_relation_size ---------+-----------+----------+------------------ recalls | 39241 | 4248 | 34799616 (1 row)Or mon remarquera que par rapport à  la taille initiale on a 34799616 < 36044800 ce qui est tout à  fait normal car le FULL essait de "compresser/réagencer au mieux" l'espace disque sur les blocks de la table (déplace etc...), un peu comme le principe de la défragmentation sous Windaube. Dès fois le gain n'est pas important mais parfois il peut le devenir en fonction de ce qui a dans la base.
Donc des updates fréquents sur une table font grossir la table . Voilà  une des raisons pour laquelle un FULL s'avère efficace pour récupérer l'espace disque lorsque ce dernier arrive à  moins de 10 à  20 % de sa capacité.
Actuellement j'ai donc en utilisantpg_size_pretty ()
et pg_total_relation_size()
performance=# SELECT relname,reltuples,relpages,pg_size_pretty(pg_total_relation_size(relname)) FROM pg_class WHERE relname='recalls'; relname | reltuples | relpages | pg_size_pretty ---------+-----------+----------+---------------- recalls | 39241 | 4248 | 39 MB (1 row)Ma table occupe donc en y incluant (les index etc...) 39 MB sur le disque. Imaginons maintenant 10 UPDATES successifs comme suit
...ET POUR 10 UPDATES IDENTIQUES???
START TRANSACTION; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; UPDATE recalls SET potaff=potaff+1; END TRANSACTION;Dans un environnement de production, si le serveur tient la charge, on peut imaginer que celà  correspond à  10 connexions au serveur de manière séquentiels (ce qui est encore l'un des meilleurs cas par rapport à  ce accès concurrents faisant monté en charge le CPU) répétant la même requête...Essayez d'imaginer la taille de votre table par la suite! Sans faire de FULL au bout de 6 UPDATES, on est passé de 39 M à  plus de 260M. :(. Et encore on en se plaindra pas car il s'agit ici d'un table de taille très raisonnable.
Faisons donc les tests! On en profitera pour créer une table qui stockera au fur et à  mesure l'avancée suite à  un requête de mise jour
START TRANSACTION; CREATE TABLE stats_size(id serial,dtsize numeric); INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); UPDATE recalls SET potaff=potaff+1; INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); END TRANSACTION;Ce qui donne donc
performance=# select * from stats_size; id | dtsize ----+-------- 1 | 36 2 | 74 3 | 111 4 | 147 5 | 185 6 | 222 7 | 258 8 | 295 9 | 333 10 | 369 11 | 407 (11 rows)En effectuant 10 fois de suite la même requête on est passé de 36 à  407 soit un facteur de 10 pour la totalité de la table qui augmente de manière quasi linéraire comme le montre le graphique suivant

Taille de la table suite à  10 UPDATE successifs
performance=# VACUUM FULL ANALYZE recalls ; VACUUM performance=# INSERT INTO stats_size(dtsize) VALUES ((SELECT trim(both ' MB' from pg_size_pretty(pg_total_relation_size('recalls')))::numeric)); INSERT 0 1Et on obtient

Taille de la table suite à  10 UPDATE successifs + VACUUM FULL ANALYZE