Cela arrive à tout le monde tôt ou tard. L’espace disque du serveur se remplit, l’alarme se déclenche et la question est toujours la même. Quelle base de données occupe tout cet espace ?

Je m’y suis retrouvé confronté après une optimisation. J’avais lancé OPTIMIZE TABLE en espérant récupérer de l’espace, mais le système de fichiers ne baissait pas comme prévu. Sans un « avant » à comparer à l’« après », impossible de savoir si l’optimisation avait fonctionné. Impossible aussi de savoir quelle base était responsable, et si l’espace disque manquant n’était pas carrément ailleurs.
Les outils dont je disposais ne répondaient qu’à la moitié du problème :
dumontre les fichiers sur le disque, mais ne dit pas à quelle table ils appartiennent ;information_schema(oupg_classsur PostgreSQL) donne les tailles logiques, mais ne voit ni les binlogs, niibdata1, ni les undo logs, ni les WAL ;- tous deux photographient un seul instant : la comparaison dans le temps reste un travail manuel, fait de copier-coller et d’un tableur.
Il me fallait quelque chose de simple : photographier l’état de toutes les bases à un moment donné, refaire la photo plus tard et me dire ce qui a changé, en classant les différences de la plus grande à la plus petite. Le coupable saute ainsi aux yeux.
Il en est sorti un unique script Bash, sans dépendances au-delà du client de base de données et des outils standard de Linux (awk, du, df). Il fonctionne avec MySQL/MariaDB et avec PostgreSQL. Je le partage ici dans l’espoir d’épargner à quelqu’un le même après-midi de frustration.
Ce qu’il fait
Le script a quatre commandes :
snapshotenregistre l’état actuel (bases, tables, fichiers du répertoire de données, espace du système de fichiers) ;comparecompare deux instantanés et montre les différences, les plus grandes en tête ;reportmontre les bases et tables les plus grosses d’un seul instantané ;listliste les instantanés enregistrés.
La comparaison est divisée en quatre sections, pour ne laisser aucun angle mort :
- taille de chaque base (données + index) ;
- espace libre ou récupérable (
data_freesur MySQL, estimation des tuples morts sur PostgreSQL) ; - taille de chaque table ;
- fichiers réels du répertoire de données : dossiers des bases,
ibdata1, binlogs, undo,pg_wal.
La dernière section est souvent celle qui résout le mystère. Bien des fois, l’espace « anormal » n’appartient à aucune base, mais aux journaux binaires ou aux fichiers partagés d’InnoDB.
Prérequis
- Linux avec Bash 4 ou supérieur,
awk,du,df(le tout en standard) ; - le client
mysqloupsql, avec un utilisateur capable de lire les métadonnées de toutes les bases ; - les droits root (ou ceux de l’utilisateur
mysql/postgres) si vous voulez aussi mesurer les fichiers sur disque ; sans eux, cette section est ignorée avec un avertissement.
Installation
Enregistrez le script (vous le trouverez à la fin de l’article) dans un fichier et rendez-le exécutable :
nano db_space_compare.sh
chmod +x db_space_compare.sh
Identifiants
Pour MySQL/MariaDB, le plus propre est un fichier ~/.my.cnf contenant les identifiants, afin qu’ils n’apparaissent pas dans l’historique des commandes :
[client]
user=root
password=LA_TUA_PASSWORD
Vous pouvez aussi passer les options via une variable d’environnement :
export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
Pour PostgreSQL, ~/.pgpass, les variables PG* ou PG_OPTS fonctionnent :
export PG_OPTS="-U postgres -h localhost"
Comment l’utiliser
Le déroulement type est avant, optimisation, après, comparaison :
# 1. fotografia prima dell'ottimizzazione
sudo ./db_space_compare.sh snapshot prima
# 2. esegui l'ottimizzazione (OPTIMIZE TABLE, VACUUM FULL, ...)
# 3. fotografia dopo
sudo ./db_space_compare.sh snapshot dopo
# 4. confronto
sudo ./db_space_compare.sh compare prima dopo
Pour PostgreSQL, il suffit d’ajouter l’option -e postgres :
sudo ./db_space_compare.sh -e postgres snapshot prima
sudo ./db_space_compare.sh -e postgres snapshot dopo
sudo ./db_space_compare.sh -e postgres compare prima dopo
Les options disponibles sont :
-e mysql|postgreschoisit le moteur (par défaut :mysql) ;-d DIRchange le dossier où sont enregistrés les instantanés (par défaut :./db_snapshots) ;-n Nfixe le nombre de lignes affichées par section (par défaut : 20).
Si vous n’indiquez pas d’étiquette, l’instantané prend pour nom la date et l’heure. C’est pratique pour surveiller la croissance dans le temps.
Exemple de sortie
Voici une comparaison sur des données de test, construites exprès pour montrer un cas typique. La base shop diminue après l’optimisation, mais la base log grossit et les binlogs doublent.
Confronto 'prima' (2026-10-11 20:00:00) -> 'dopo' (2026-10-11 22:00:00)
Filesystem /var: usato 46.6 GiB -> 48.4 GiB (+1.9 GiB)
=== Database - dimensione totale ===
NOME PRIMA DOPO DIFFERENZA NOTE
log 21.4 GiB 27.5 GiB +6.1 GiB +28.3%
shop 5.6 GiB 3.6 GiB -2.0 GiB -35.0%
=== Tabelle - dimensione (dati + indici) ===
NOME PRIMA DOPO DIFFERENZA NOTE
log.eventi 21.4 GiB 27.5 GiB +6.1 GiB +28.3%
shop.ordini 5.6 GiB 3.6 GiB -2.0 GiB -35.0%
log.nuova 0 B 1.0 KiB +1.0 KiB NUOVO
=== File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL) ===
NOME PRIMA DOPO DIFFERENZA NOTE
log 21.4 GiB 27.5 GiB +6.1 GiB +28.3%
mysql-bin.* 3.7 GiB 8.4 GiB +4.7 GiB +125.0%
shop 5.7 GiB 3.6 GiB -2.0 GiB -36.1%
ibdata1 11.2 GiB 11.2 GiB 0 B
Ici le coupable est évident. La table log.eventi a grossi de plus de 6 GiB. Les fichiers mysql-bin.* sont passés de 3,7 à 8,4 GiB. L’optimisation sur shop a fonctionné, mais le gain d’espace disque a été englouti par le reste.
Comment lire les résultats
- En haut de chaque section se trouvent les éléments qui ont le plus changé. Une base ou une table qui ne diminue pas (voire grossit) après l’optimisation est le premier suspect.
- Si les tables diminuent mais pas le système de fichiers, l’espace disque est ailleurs. Regardez la section sur les fichiers du répertoire de données (binlogs,
ibdata1, undo,pg_wal). - MySQL/MariaDB :
ibdata1ne rétrécit jamais, pas même avecOPTIMIZE TABLE. Avecinnodb_file_per_table=OFF, la valeur dedata_freeest partagée et peu indicative. - PostgreSQL : un simple
VACUUMne rend pas d’espace au système d’exploitation, il fautVACUUM FULL(oupg_repack). L’espace « récupérable » affiché est une estimation fondée sur les tuples morts.
Pour PostgreSQL, la documentation officielle sur l’utilisation du disque explique comment mesurer l’espace occupé.
Surveiller l’espace disque dans le temps
Si le problème n’est pas lié à une seule optimisation mais à une croissance progressive, mieux vaut automatiser les instantanés avec cron, un par heure, puis comparer deux moments quelconques :
# crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot >/dev/null 2>&1
Sans étiquette, chaque instantané prend pour nom la date et l’heure (par exemple 20261011-140000). Avec list vous voyez ceux qui existent et avec compare vous en comparez deux. En quelques jours, on voit quelle base grossit de façon anormale.
./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000
Le script complet
Voici le code. Copiez-le dans un fichier nommé db_space_compare.sh, rendez-le exécutable et vous êtes prêt.
#!/usr/bin/env bash
#
# db_space_compare.sh
# Fotografa l'occupazione di spazio di tutti i database (MySQL/MariaDB o
# PostgreSQL) e confronta due fotografie (es. prima/dopo l'ottimizzazione)
# per capire quale database, tabella o file occupa spazio in modo anomalo.
#
# Uso:
# db_space_compare.sh [opzioni] snapshot [etichetta] (default: data-ora)
# db_space_compare.sh [opzioni] compare <prima> <dopo>
# db_space_compare.sh [opzioni] report <etichetta>
# db_space_compare.sh [opzioni] list
#
# Opzioni:
# -e mysql|postgres motore del database (default: mysql)
# -d DIR cartella delle fotografie (default: ./db_snapshots)
# -n N righe mostrate per sezione (default: 20)
#
# Credenziali:
# MySQL/MariaDB: ~/.my.cnf oppure MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
# PostgreSQL: ~/.pgpass, variabili PG* oppure PG_OPTS="-U postgres -h localhost"
# (non mettere -d in PG_OPTS: lo script si collega a ogni database)
#
# Per misurare anche i file su disco (cartelle dei DB, ibdata1, binlog, undo,
# pg_wal) va eseguito come root o come utente mysql/postgres; altrimenti
# quella parte viene saltata con un avviso.
#
# Esempio:
# sudo ./db_space_compare.sh snapshot prima
# ... ottimizzazione (OPTIMIZE TABLE / VACUUM FULL / ...) ...
# sudo ./db_space_compare.sh snapshot dopo
# sudo ./db_space_compare.sh compare prima dopo
set -euo pipefail
export LC_ALL=C
ENGINE=mysql
OUTDIR=./db_snapshots
TOPN=20
SNAP_IN_PROGRESS=""
die() { echo "ERRORE: $*" >&2; exit 1; }
warn() { echo "ATTENZIONE: $*" >&2; }
usage() { sed -n '2,/^$/{s/^# \{0,1\}//;p}' "$0"; exit "${1:-0}"; }
cleanup() {
if [[ -n $SNAP_IN_PROGRESS ]]; then
rm -rf -- "$SNAP_IN_PROGRESS"
echo "Fotografia incompleta rimossa: $SNAP_IN_PROGRESS" >&2
fi
}
trap cleanup EXIT
while getopts ":e:d:n:h" opt; do
case $opt in
e) ENGINE=$OPTARG ;;
d) OUTDIR=$OPTARG ;;
n) TOPN=$OPTARG ;;
h) usage 0 ;;
*) usage 1 ;;
esac
done
shift $((OPTIND - 1))
[[ $ENGINE == mysql || $ENGINE == postgres ]] || die "motore non valido: '$ENGINE' (usa mysql o postgres)"
[[ $TOPN =~ ^[0-9]+$ ]] || die "-n deve essere un numero"
read -r -a MYSQL_ARGS <<< "${MYSQL_OPTS:-}"
read -r -a PG_ARGS <<< "${PG_OPTS:-}"
my() { mysql "${MYSQL_ARGS[@]+"${MYSQL_ARGS[@]}"}" --batch --skip-column-names "$@"; }
pg() { psql "${PG_ARGS[@]+"${PG_ARGS[@]}"}" -X -q -A -t -F $'\t' -v ON_ERROR_STOP=1 "$@"; }
# Funzioni awk comuni: formattazione in KiB/MiB/GiB, chiave e valore di riga.
# nk = numero di colonne che formano il nome (1 = database, 2 = db.tabella)
# vc = colonne da sommare per il valore (es. "4,5" = dati + indici)
AWK_LIB='
function h(b, a, i, u) {
split("B KiB MiB GiB TiB PiB", u, " ")
a = (b < 0) ? -b : b; i = 1
while (a >= 1024 && i < 6) { a /= 1024; i++ }
if (i == 1) return sprintf("%s%d B", (b < 0 ? "-" : ""), a)
return sprintf("%s%.1f %s", (b < 0 ? "-" : ""), a, u[i])
}
function hs(b) { return (b > 0 ? "+" : "") h(b) }
function key( k, i) { k = $1; for (i = 2; i <= nk; i++) k = k "." $i; return k }
function val( n, c, i, s) { n = split(vc, c, ","); s = 0; for (i = 1; i <= n; i++) s += $(c[i]); return s }
'
meta_get() { awk -F= -v k="$2" '$1 == k { sub(/^[^=]*=/, ""); print; exit }' "$1/meta.txt"; }
# ---------------------------------------------------------------- raccolta dati
# Occupazione reale su disco delle voci di primo livello della data directory:
# cartelle dei database, ibdata1, binlog, undo, redo log, pg_wal, base, ...
# I file numerati (mysql-bin.000123, relay-log.000045, ...) sono sommati in una voce.
collect_disk() {
local d=$1 out=$2
: > "$out"
if [[ -z $d || ! -r $d || ! -x $d ]]; then
warn "data directory '${d:-?}' non leggibile: salto i file su disco (esegui come root per includerli)"
return 0
fi
{ du -sb -- "$d"/* 2>/dev/null || true; } |
awk -F'\t' -v p="$d/" '
{ n = $2
if (index(n, p) == 1) n = substr(n, length(p) + 1)
if (n ~ /\.[0-9][0-9][0-9][0-9][0-9][0-9]$/) sub(/[0-9]+$/, "*", n)
s[n] += $1 }
END { for (n in s) printf "%s\t%.0f\n", n, s[n] }' > "$out"
}
write_meta() {
local dir=$1 datadir=$2 version=$3
{
echo "timestamp=$(date '+%Y-%m-%d %H:%M:%S')"
echo "engine=$ENGINE"
echo "host=$(hostname)"
echo "version=$version"
echo "datadir=$datadir"
if [[ -n $datadir && -e $datadir ]]; then
{ df -P -B1 -- "$datadir" 2>/dev/null || true; } |
awk 'NR == 2 { print "fs_mount=" $6; print "fs_used=" $3; print "fs_avail=" $4 }'
fi
} > "$dir/meta.txt"
}
# File prodotti da ogni fotografia (separati da TAB):
# tables.tsv db, tabella, engine, dati, indici, libero/recuperabile, righe
# databases.tsv db, dati, indici, libero/recuperabile, totale
# disk.tsv voce della data directory, byte su disco
# meta.txt data, host, versione, filesystem
snapshot_mysql() {
local dir=$1 pre="" datadir version
command -v mysql >/dev/null 2>&1 || die "client 'mysql' non trovato"
my -e "SELECT 1" >/dev/null || die "connessione a MySQL/MariaDB fallita (controlla ~/.my.cnf o MYSQL_OPTS)"
# MySQL 8 tiene in cache le statistiche di information_schema fino a 24 ore:
# senza questo, i valori "dopo" l'ottimizzazione potrebbero essere quelli vecchi.
if [[ -n $(my -e "SHOW VARIABLES LIKE 'information_schema_stats_expiry'") ]]; then
pre="SET SESSION information_schema_stats_expiry = 0;"
fi
my -e "$pre
SELECT table_schema, table_name, IFNULL(engine, '-'),
IFNULL(data_length, 0), IFNULL(index_length, 0), IFNULL(data_free, 0), IFNULL(table_rows, 0)
FROM information_schema.TABLES
WHERE table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
AND table_schema NOT IN ('information_schema', 'performance_schema', 'sys')" > "$dir/tables.tsv"
my -e "$pre
SELECT s.schema_name,
IFNULL(SUM(t.data_length), 0), IFNULL(SUM(t.index_length), 0), IFNULL(SUM(t.data_free), 0),
IFNULL(SUM(t.data_length), 0) + IFNULL(SUM(t.index_length), 0)
FROM information_schema.SCHEMATA s
LEFT JOIN information_schema.TABLES t
ON t.table_schema = s.schema_name
AND t.table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
WHERE s.schema_name NOT IN ('information_schema', 'performance_schema', 'sys')
GROUP BY s.schema_name" > "$dir/databases.tsv"
datadir=$(my -e "SELECT @@datadir"); datadir=${datadir%/}
version=$(my -e "SELECT VERSION()")
collect_disk "$datadir" "$dir/disk.tsv"
write_meta "$dir" "$datadir" "$version"
}
snapshot_postgres() {
local dir=$1 maindb=${PGDATABASE:-postgres} datadir version db
command -v psql >/dev/null 2>&1 || die "client 'psql' non trovato"
pg -d "$maindb" -c "SELECT 1" >/dev/null || die "connessione a PostgreSQL fallita (controlla PG_OPTS, PGUSER, ~/.pgpass)"
pg -d "$maindb" -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY 1" > "$dir/.dblist"
# Per ogni database: dimensione di ogni tabella (catalogo incluso, es. pg_largeobject)
# e stima dello spazio occupato da tuple morte (bloat recuperabile con VACUUM FULL).
: > "$dir/tables.tsv"
while IFS= read -r db; do
[[ -n $db ]] || continue
pg -d "$db" -c "
SELECT current_database(), s.schemaname || '.' || s.relname, 'table',
pg_table_size(s.relid), pg_indexes_size(s.relid),
CASE WHEN s.n_live_tup + s.n_dead_tup > 0
THEN (pg_table_size(s.relid)::numeric * s.n_dead_tup / (s.n_live_tup + s.n_dead_tup))::bigint
ELSE 0 END,
s.n_live_tup
FROM pg_stat_all_tables s
WHERE s.schemaname <> 'information_schema'
AND s.schemaname NOT LIKE 'pg_toast%'" < /dev/null >> "$dir/tables.tsv" \
|| warn "impossibile leggere le tabelle del database '$db'"
done < "$dir/.dblist"
pg -d "$maindb" -c "SELECT datname, pg_database_size(oid) FROM pg_database WHERE datallowconn AND NOT datistemplate" > "$dir/.dbsize"
awk -F'\t' '
FILENAME == ARGV[1] { d[$1] += $4; i[$1] += $5; f[$1] += $6; next }
{ printf "%s\t%.0f\t%.0f\t%.0f\t%s\n", $1, d[$1], i[$1], f[$1], $2 }' \
"$dir/tables.tsv" "$dir/.dbsize" > "$dir/databases.tsv"
datadir=$(pg -d "$maindb" -c "SHOW data_directory" 2>/dev/null || true)
version=$(pg -d "$maindb" -c "SHOW server_version")
collect_disk "$datadir" "$dir/disk.tsv"
# Dettaglio di base/<oid> con il nome del database al posto dell'oid
if [[ -n $datadir && -r $datadir/base && -x $datadir/base ]]; then
pg -d "$maindb" -c "SELECT oid, datname FROM pg_database" > "$dir/.oidmap"
{ du -sb -- "$datadir"/base/* 2>/dev/null || true; } |
awk -F'\t' '
FILENAME == ARGV[1] { name[$1] = $2; next }
{ o = $2; sub(/.*\//, "", o); printf "base/%s\t%s\n", ((o in name) ? name[o] : o), $1 }' \
"$dir/.oidmap" - >> "$dir/disk.tsv"
fi
write_meta "$dir" "$datadir" "$version"
}
# --------------------------------------------------------------- presentazione
print_fs() {
local d=$1 used avail mount
used=$(meta_get "$d" fs_used); avail=$(meta_get "$d" fs_avail); mount=$(meta_get "$d" fs_mount)
[[ -n $used ]] || return 0
awk -v u="$used" -v a="$avail" -v m="$mount" "$AWK_LIB"'
BEGIN { printf "Filesystem %s: usato %s, libero %s\n", m, h(u), h(a) }'
}
top_section() {
local f=$1 nk=$2 vc=$3 title=$4
echo
echo "=== $title ==="
if [[ ! -s $f ]]; then echo " (nessun dato)"; return 0; fi
awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
{ v = val(); printf "%.0f\t%s\t%s\n", v, key(), h(v) }' "$f" |
sort -t $'\t' -k1,1nr -k2,2 |
awk -F'\t' -v top="$TOPN" 'NR <= top && $1 > 0 { printf " %-60s %12s\n", $2, $3 }'
}
compare_section() {
local fa=$1 fb=$2 nk=$3 vc=$4 title=$5
[[ -f $fa ]] || fa=/dev/null
[[ -f $fb ]] || fb=/dev/null
echo
echo "=== $title ==="
if [[ ! -s $fa && ! -s $fb ]]; then echo " (nessun dato)"; return 0; fi
printf ' %-50s %12s %12s %13s %s\n' "NOME" "PRIMA" "DOPO" "DIFFERENZA" "NOTE"
awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
FILENAME == ARGV[1] { k = key(); A[k] = val(); S[k] = 1; next }
{ k = key(); B[k] = val(); S[k] = 1 }
END {
for (k in S) {
inA = (k in A); inB = (k in B)
a = inA ? A[k] : 0; b = inB ? B[k] : 0; d = b - a
if (!inA) note = "NUOVO"
else if (!inB) note = "RIMOSSO"
else if (a > 0 && d != 0) note = sprintf("%+.1f%%", 100 * d / a)
else note = ""
printf "%.0f\t%s\t%s\t%s\t%s\t%s\n", (d < 0 ? -d : d), k, h(a), h(b), hs(d), note
}
}' "$fa" "$fb" |
sort -t $'\t' -k1,1nr -k2,2 |
awk -F'\t' -v top="$TOPN" 'NR <= top { printf " %-50s %12s %12s %13s %s\n", $2, $3, $4, $5, $6 }'
}
# -------------------------------------------------------------------- comandi
cmd_snapshot() {
local label=${1:-$(date '+%Y%m%d-%H%M%S')}
[[ $label =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die "etichetta non valida: usa solo lettere, numeri, . _ -"
local dir=$OUTDIR/$label
[[ ! -e $dir ]] || die "la fotografia '$label' esiste già ($dir)"
mkdir -p -- "$dir"
SNAP_IN_PROGRESS=$dir
echo "Fotografia '$label' ($ENGINE) in corso..."
"snapshot_$ENGINE" "$dir"
rm -f -- "$dir/.dblist" "$dir/.dbsize" "$dir/.oidmap"
SNAP_IN_PROGRESS=""
echo "Salvata in $dir ($(wc -l < "$dir/tables.tsv") tabelle)"
print_fs "$dir"
top_section "$dir/databases.tsv" 1 5 "Database più grandi"
}
cmd_report() {
local label=${1:-}
[[ -n $label ]] || die "uso: report <etichetta>"
local d=$OUTDIR/$label
[[ -f $d/meta.txt ]] || die "fotografia '$label' non trovata in $OUTDIR"
echo "Fotografia '$label' del $(meta_get "$d" timestamp) - $(meta_get "$d" engine) $(meta_get "$d" version) su $(meta_get "$d" host)"
print_fs "$d"
top_section "$d/databases.tsv" 1 5 "Database più grandi (dati + indici)"
top_section "$d/tables.tsv" 2 4,5 "Tabelle più grandi (dati + indici)"
top_section "$d/tables.tsv" 2 6 "Tabelle con più spazio recuperabile (data_free / tuple morte stimate)"
top_section "$d/disk.tsv" 1 2 "Voci più grandi nella data directory"
}
cmd_compare() {
local a=${1:-} b=${2:-}
[[ -n $a && -n $b ]] || die "uso: compare <etichetta_prima> <etichetta_dopo>"
[[ $a != "$b" ]] || die "le due etichette sono uguali"
local da=$OUTDIR/$a db=$OUTDIR/$b
[[ -f $da/meta.txt ]] || die "fotografia '$a' non trovata in $OUTDIR"
[[ -f $db/meta.txt ]] || die "fotografia '$b' non trovata in $OUTDIR"
[[ $(meta_get "$da" engine) == "$(meta_get "$db" engine)" ]] || warn "le due fotografie sono di motori diversi"
echo "Confronto '$a' ($(meta_get "$da" timestamp)) -> '$b' ($(meta_get "$db" timestamp))"
local ua ub m
ua=$(meta_get "$da" fs_used); ub=$(meta_get "$db" fs_used); m=$(meta_get "$db" fs_mount)
if [[ -n $ua && -n $ub ]]; then
awk -v a="$ua" -v b="$ub" -v m="$m" "$AWK_LIB"'
BEGIN { printf "Filesystem %s: usato %s -> %s (%s)\n", m, h(a), h(b), hs(b - a) }'
fi
compare_section "$da/databases.tsv" "$db/databases.tsv" 1 5 "Database - dimensione totale"
compare_section "$da/databases.tsv" "$db/databases.tsv" 1 4 "Database - spazio libero/recuperabile"
compare_section "$da/tables.tsv" "$db/tables.tsv" 2 4,5 "Tabelle - dimensione (dati + indici)"
compare_section "$da/disk.tsv" "$db/disk.tsv" 1 2 "File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL)"
cat <<'EOF'
Come leggere il risultato:
- In cima a ogni sezione ci sono le voci cambiate di più: un database o una
tabella che dopo l'ottimizzazione non cala, o addirittura cresce, è il
primo sospetto.
- Se le tabelle calano ma il filesystem no, lo spazio sta altrove: guarda la
sezione "File nella data directory" (binlog, ibdata1, undo, pg_wal).
- MySQL/MariaDB: ibdata1 non si restringe mai, nemmeno con OPTIMIZE; con
innodb_file_per_table=OFF il data_free è condiviso e poco indicativo.
- PostgreSQL: VACUUM semplice non restituisce spazio al disco, serve
VACUUM FULL (o pg_repack); lo spazio "recuperabile" è una stima.
EOF
}
cmd_list() {
if [[ ! -d $OUTDIR ]]; then echo "Nessuna fotografia in $OUTDIR"; return 0; fi
local d found=0
for d in "$OUTDIR"/*/; do
[[ -f $d/meta.txt ]] || continue
found=1
printf '%-24s %s %-8s %s\n' "$(basename "$d")" "$(meta_get "$d" timestamp)" \
"$(meta_get "$d" engine)" "$(meta_get "$d" host)"
done
[[ $found -eq 1 ]] || echo "Nessuna fotografia in $OUTDIR"
}
cmd=${1:-}
shift || true
case $cmd in
snapshot) cmd_snapshot "$@" ;;
compare) cmd_compare "$@" ;;
report) cmd_report "$@" ;;
list) cmd_list ;;
""|help) usage 0 ;;
*) die "comando sconosciuto: '$cmd' (usa snapshot, compare, report, list)" ;;
esac
Notes finales
- Le script est en lecture seule sur les bases : il n’exécute que
SELECTsur les métadonnées. Il n’écrit que dans le dossier des instantanés. - Sur MySQL 8, il définit
information_schema_stats_expiry = 0dans la session. Sinon, les statistiques en cache (jusqu’à 24 heures) pourraient afficher d’« anciennes » valeurs même après l’optimisation. - Si un instantané échoue à mi-parcours, il est supprimé automatiquement pour ne pas laisser de données incomplètes.
- Avant de l’utiliser en production, essayez-le dans un environnement de test et vérifiez les droits de l’utilisateur de la base.
Si MySQL ne démarre plus après une mise à jour, voyez l’article sur Debian 12.
Si vous l’utilisez pour surveiller l’espace disque et trouvez un cas qu’il ne couvre pas, écrivez-moi dans les commentaires. Vos idées pour l’étendre sont aussi bienvenues (autres moteurs, export CSV, alertes par e-mail).
Laisser un commentaire