venerdì 15 maggio 2009

Vacuum

Una cosa che molto spesso lascia perplessi gli utenti che si avvicinano a PostgreSQL è l'uso di vacuum per l'analisi di una tabella e la sua "pulizia" da tuple morte. La necessità dell'utilizzo di vacuum deriva dal sistema di storage non-overwriting che PostgreSQL ha adottato fin dalle prime versioni, e che poi oggi è parte integrante del sistema MVCC. Senza dilungarsi troppo sull'argomento, l'idea è che ogni tupla modificata (ossia una tupla sottoposta a scrittura come UPDATE o DELETE) non viene aggiornata in loco, ma viene appesa in fondo alla tabella come nuova. La vecchia tupla viene poi invalidata (coerentemente con le transazioni in corso) e solo la nuova rimane (se correttamente committata). Mediante questa tecnica il database riesce ad implementare un buon isolamento fra le transazioni senza richiedere troppi lock.
Questo però richiede che ogni tanto venga reclamato lo spazio occupato da tuple morte, e questo è uno dei compiti di vacuum. Oltre a questo, vacuum si occupa di aggiornare le statistiche di sistema per poter fornire dati precisi all'ottimizzatore.
La buona notizia è che vacuum è solitamente attivo in modalità automatica in tutte le installazioni recenti, quindi non dovrete eseguirlo a mano.

Per mostrare l'uso di vacuum noi dovremo prima di tutto disabilitare l'aggiornamento automatico delle statistiche, agendo sui parametri track_count e track_activities e impostandoli ad off.
Successivamente possiamo creare una tabella di prova con cui giocare:

pgdaydb=# create table test_table(id serial, descrizione varchar);
NOTICE: CREATE TABLE will create implicit sequence "test_table_id_seq"
for serial column "test_table.id"
CREATE TABLE
pgdaydb=# insert into test_table(descrizione) values('Prima riga');
INSERT 0 1
pgdaydb=# insert into test_table(descrizione) values('Seconda riga');
INSERT 0 1
pgdaydb=# insert into test_table(descrizione) values('Terza riga');
INSERT 0 1
pgdaydb=# insert into test_table(descrizione) values('Quarta riga');
INSERT 0 1

Se ora chiediamo le statistiche sulla tabella otteniamo che la tabella risulta essere ancora vuota (ossia per il sistema non ci sono tuple inserite):

pgdaydb=# select relname, reltype, relfilenode, relpages, reltuples
from pg_class where relname = 'test_table';
relname | reltype | relfilenode | relpages | reltuples
------------+---------+-------------+----------+-----------
test_table | 26147 | 26145 | 0 | 0

Se guardiamo su disco la dimensione del file corrispondente alla tabella, troviamo che esso occupa 8 KB, ossia lo spazio di esattamente una pagina (le quattro tuple infatti sono abbastanza piccole per ricadere in una pagina dati). Ricordiamoci infatti che le tabelle sono organizzate a pagine di 8KB, e quindi il sistema alloca sempre multipli di 8KB:

ls -lh 26145
-rw------- 1 postgres postgres 8.0K 2009-05-15 08:08 26145
Ora possiamo popolare la nostra tabella forzando delle insert fino ad arrivare a circa 2 milioni di record; è sufficiente ripetere qualche volta la query seguente:

INSERT INTO test_table(descrizione) SELECT descrizione FROM test_table;
A questo punto lo spazio disco occupato dalla tabella è notevolmente cresciuto:

ls -lh 26145
-rw------- 1 postgres postgres 93M 2009-05-15 08:12 26145
Siccome le statistiche non vengono aggiornate in automatico, il sistema è ancora convinto che non vi siano tuple nella tabella, e quindi una eventuale query porterebbe a percorsi sbagliati (ossia uno scan sequenziale quando magari un indice sarebbe piu' opportuno):

select relname, reltype, relfilenode, relpages, reltuples
from pg_class where relname = 'test_table';
relname | reltype | relfilenode | relpages | reltuples
------------+---------+-------------+----------+-----------
test_table | 26147 | 26145 | 0 | 0


Cosa succede se ora facciamo un update di tutte le tuple? Vengono generate 2 milioni di nuove tuple che invalidano le precedenti, e infatti lo spazio disco occupato dalla tabella cresce di circa il doppio:

pgdaydb=# update test_table set descrizione='Nuova descrizione';
UPDATE 2097152

ls -lh 26145
-rw------- 1 postgres postgres 197M 2009-05-15 08:14 26145

Chiediamo a vacuum di aggiornare le statistiche di sistema:

pgdaydb=# vacuum analyze test_table;
VACUUM
pgdaydb=# select relname, reltype, relfilenode, relpages, reltuples
from pg_class where relname = 'test_table';
relname | reltype | relfilenode | relpages | reltuples
------------+---------+-------------+----------+-------------
test_table | 26147 | 26145 | 25206 | 2.09715e+06


ls -lh 26145
-rw------- 1 postgres postgres 197M 2009-05-15 08:14 26145
Quello che succede è che vacuum scorre la tabella per vedere quante pagine dati sono presenti, quante tuple sono valide nelle pagine dati e aggiorna le statistiche di sistema. Notare che le tuple valide sono effettivamente 2 milioni e che le pagine sono 25206, che forniscono uno spazio disco di circa 200 MB, come infatti risulta guardando lo spazio occupato su disco.

Siccome sappiamo che la tabella contiene 4 milioni di tuple, di cui solo 2 milioni sono valide, possiamo richiedere il vacuum della tabella e ottenere la riduzione di spazio.


pgdaydb=# vacuum full verbose test_table;
INFO: vacuuming "public.test_table"
INFO: "test_table": found 0 removable, 2097152 nonremovable row versions in 25206 pages
DETAIL: 0 dead row versions cannot be removed yet.
Nonremovable row versions range from 46 to 46 bytes long.
There were 2097152 unused item pointers.
Total free space (including removable row versions) is 88442096 bytes.
11848 pages are or will become empty, including 0 at the end of the table.
11850 pages containing 88388672 free bytes are potential move destinations.
CPU 0.06s/0.22u sec elapsed 0.28 sec.
INFO: "test_table": moved 1836489 row versions, truncated 25206 to 13509 pages
DETAIL: CPU 0.95s/4.95u sec elapsed 18.32 sec.
INFO: vacuuming "pg_toast.pg_toast_26145"
INFO: "pg_toast_26145": found 0 removable, 0 nonremovable row versions in 0 pages
DETAIL: 0 dead row versions cannot be removed yet.
Nonremovable row versions range from 0 to 0 bytes long.
There were 0 unused item pointers.
Total free space (including removable row versions) is 0 bytes.
0 pages are or will become empty, including 0 at the end of the table.
0 pages containing 0 free bytes are potential move destinations.
CPU 0.00s/0.00u sec elapsed 0.00 sec.
INFO: index "pg_toast_26145_index" now contains 0 row versions in 1 pages
DETAIL: 0 index pages have been deleted, 0 are currently reusable.
CPU 0.00s/0.00u sec elapsed 0.00 sec.
VACUUM
Notate che il sistema analizza la tabella, decide di troncare le pagine dati muovendo le tuple e compattando lo spazio (truncated 25206 to 13509 page) e infatti, osservando lo spazio disco si ha che questo si è quasi dimezzato:

ls -lh 26145
-rw------- 1 postgres postgres 106M 2009-05-15 08:16 26145

Cosa si conclude da tutto ciò?
Vacuum è uno strumento indispensabile, consente sia di aggiornare le statistiche di sistema (indispensabili per l'ottimizzatore) che di ricompattare lo spazio non usato. Comprendere la sua necessità, dovuta anche all'esistenza del sistema di storage non overwriting e a MVCC è la base per ogni DBA PostgreSQL.
Ma come è già stato scritto, il sistema attiva l'autovacuum per default e anche le statistiche vengono solitamente aggiornate automaticamente, quindi ci sono buone probabilità che non dobbiate mai intervenire manualmente!

Nessun commento: