It happens to everyone sooner or later. The server disk fills up, the alarm goes off and the question is always the same. Which database is taking up all this space? Measuring database disk space properly turns out to be harder than it looks.

I ran into it after an optimisation. I had run OPTIMIZE TABLE hoping to reclaim space, but the filesystem didn’t shrink as expected. Without a “before” to compare with the “after”, it was impossible to tell whether the optimisation had worked. Nor could I tell which database was really responsible, or whether the missing space was somewhere else entirely.
The tools I had answered only half of the problem:
dushows the files on disk, but doesn’t say which table they belong to;information_schema(orpg_classon PostgreSQL) gives the logical sizes, but doesn’t see binlogs,ibdata1, undo logs or WAL;- both photograph a single instant: comparing over time remains manual work, done with copy and paste and a spreadsheet.
I needed something simple: take a snapshot of the state of all databases at a given moment, take another later and have it tell me what changed, sorting the differences from the largest to the smallest. That way the culprit jumps out immediately.
The result is a single Bash script. It has no dependencies beyond the database client and the standard Linux tools (awk, du, df). It works with MySQL/MariaDB and with PostgreSQL. I’m sharing it here in the hope it saves someone the same afternoon of frustration.
What the database disk space script does
The script has four commands:
snapshotsaves the current state (databases, tables, files in the data directory, filesystem space);comparecompares two snapshots and shows the differences, the largest on top;reportshows the largest databases and tables in a single snapshot;listlists the saved snapshots.
The comparison is split into four sections, so as not to leave any blind spots:
- size of each database (data + indexes);
- free or reclaimable space (
data_freeon MySQL, an estimate of dead tuples on PostgreSQL); - size of each table;
- actual files in the data directory: database folders,
ibdata1, binlogs, undo,pg_wal.
The last section is often the one that solves the mystery. Many times the “anomalous” space doesn’t belong to any database, but to the binary logs or to InnoDB’s shared files.
Requirements
- Linux with Bash 4 or later,
awk,du,df(all standard); - the
mysqlorpsqlclient, with a user who can read the metadata of all databases; - root permissions (or those of the
mysql/postgresuser) if you also want to measure the files on disk; without them, that section is skipped with a warning.
Installation
Save the script (you will find it at the end of the article) to a file and make it executable:
nano db_space_compare.sh
chmod +x db_space_compare.sh
Credentials
For MySQL/MariaDB the cleanest way is a ~/.my.cnf file with the credentials, so they don’t appear in the command history:
[client]
user=root
password=LA_TUA_PASSWORD
Alternatively you can pass the options through an environment variable:
export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
For PostgreSQL, ~/.pgpass, the PG* variables or PG_OPTS work:
export PG_OPTS="-U postgres -h localhost"
How to use it
The typical flow is before, optimisation, after, comparison:
# 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
For PostgreSQL just add the -e postgres option:
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
The available options are:
-e mysql|postgreschooses the engine (default:mysql);-d DIRchanges the folder where snapshots are saved (default:./db_snapshots);-n Nsets how many rows to show per section (default: 20).
If you don’t give a label, the snapshot is named after the date and time. This is handy for monitoring growth over time.
Example output
Here is a comparison on test data, built on purpose to show a typical case. The shop database shrinks after the optimisation, but the log database grows and the binlogs double.
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
Here the culprit is obvious: the log.eventi table has grown by more than 6 GiB. The mysql-bin.* files went from 3.7 to 8.4 GiB. The optimisation on shop worked, but the gain was eaten up by the rest.
How to read the database disk space results
- At the top of each section are the items that changed the most. A database or table that doesn’t shrink (or even grows) after the optimisation is the first suspect.
- If the tables shrink but the filesystem doesn’t, the space is elsewhere. Look at the section on files in the data directory (binlogs,
ibdata1, undo,pg_wal). - MySQL/MariaDB:
ibdata1never shrinks, not even withOPTIMIZE TABLE. Withinnodb_file_per_table=OFFthe value ofdata_freeis shared and not very informative. - PostgreSQL: a plain
VACUUMdoes not give space back to the operating system, you needVACUUM FULL(orpg_repack). The “reclaimable” space shown is an estimate based on dead tuples. The PostgreSQL documentation on monitoring disk usage covers the underlying functions.
Monitoring database disk space over time
If the problem isn’t tied to a single optimisation but to gradual growth, it is worth automating snapshots with cron, one an hour, and then comparing any two moments:
# crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot >/dev/null 2>&1
Without a label, every snapshot is named after the date and time (for example 20261011-140000). With list you see the available ones and with compare you compare two. Within a few days it becomes clear which database is growing abnormally.
./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000
The complete script
Here is the code. Copy it into a file called db_space_compare.sh, make it executable and you are ready.
#!/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
Final notes on database disk space
- The script is read-only on the databases: it only runs
SELECTon the metadata. It writes only to the snapshots folder. - On MySQL 8 it sets
information_schema_stats_expiry = 0in the session. Otherwise the cached statistics (up to 24 hours) could show “old” values even after the optimisation. - If a snapshot fails halfway, it is removed automatically so as not to leave incomplete data.
- Before using it in production, try it in a test environment and check the database user’s permissions.
If you use it and find a case it doesn’t cover, write to me in the comments. The same goes for ideas to extend it (other engines, CSV export, email alerts). If the problem is a MySQL that won’t start rather than a full disk, see MySQL won’t start after a Debian 12 upgrade.
Leave a Reply