Das passiert früher oder später jedem: Die Festplatte des Servers läuft voll, der Alarm geht los und die Frage ist immer dieselbe. Welche Datenbank belegt den ganzen Platz? Beim Speicherplatz von MySQL gibt es dafür selten eine schnelle Antwort.

Ich stand davor nach einer Optimierung. Ich hatte OPTIMIZE TABLE ausgeführt in der Hoffnung, Platz zurückzugewinnen, aber das Dateisystem schrumpfte nicht wie erwartet. Ohne ein „Vorher“, mit dem man das „Nachher“ vergleichen konnte, ließ sich nicht sagen, ob die Optimierung gewirkt hatte, welche Datenbank wirklich verantwortlich war und ob der fehlende Platz vielleicht ganz woanders steckte.
Die Werkzeuge, die ich hatte, beantworteten nur die Hälfte des Problems.
duzeigt die Dateien auf der Platte, sagt aber nicht, zu welcher Tabelle sie gehören.information_schema(oderpg_classbei PostgreSQL) liefert die logischen Größen, sieht aber weder Binlogs nochibdata1, Undo-Logs oder WAL.- beide zeigen nur einen einzigen Augenblick: Der Vergleich über die Zeit bleibt Handarbeit mit Copy-and-paste und Tabellenkalkulation.
Ich brauchte etwas Einfaches: den Zustand aller Datenbanken zu einem bestimmten Zeitpunkt festhalten, später erneut festhalten und mir sagen lassen, was sich geändert hat, die Unterschiede sortiert vom größten zum kleinsten. So fällt der Übeltäter sofort auf.
Herausgekommen ist ein einzelnes Bash-Skript ohne Abhängigkeiten außer dem Datenbank-Client und den üblichen Linux-Werkzeugen (awk, du, df). Es funktioniert mit MySQL/MariaDB und mit PostgreSQL. Ich teile es hier in der Hoffnung, jemandem denselben frustrierenden Nachmittag zu ersparen.
Was es tut
Das Skript hat vier Befehle.
snapshotspeichert den aktuellen Zustand (Datenbanken, Tabellen, Dateien im Datenverzeichnis, Platz des Dateisystems).comparevergleicht zwei Momentaufnahmen und zeigt die Unterschiede, die größten zuerst.reportzeigt die größten Datenbanken und Tabellen einer einzelnen Momentaufnahme.listlistet die gespeicherten Momentaufnahmen auf.
Der Vergleich ist in vier Abschnitte unterteilt, damit keine blinden Flecken bleiben.
- Größe jeder Datenbank (Daten + Indizes).
- freier oder rückgewinnbarer Platz (
data_freebei MySQL, Schätzung der toten Tupel bei PostgreSQL). - Größe jeder Tabelle.
- tatsächliche Dateien im Datenverzeichnis: Datenbankordner,
ibdata1, Binlogs, Undo,pg_wal.
Der letzte Abschnitt löst das Rätsel oft. Viele Male gehört der „auffällige“ Platz zu keiner Datenbank, sondern zu den Binärlogs oder zu den gemeinsamen InnoDB-Dateien.
Voraussetzungen
- Linux mit Bash 4 oder höher,
awk,du,df(alles Standard). - der Client
mysqloderpsql, mit einem Benutzer, der die Metadaten aller Datenbanken lesen darf. - Root-Rechte (oder die des Benutzers
mysql/postgres), wenn Sie auch die Dateien auf der Platte messen wollen; ohne sie wird dieser Abschnitt mit einer Warnung übersprungen.
Installation
Speichern Sie das Skript (am Ende des Artikels) in einer Datei und machen Sie es ausführbar.
nano db_space_compare.sh
chmod +x db_space_compare.sh
Zugangsdaten
Für MySQL/MariaDB ist der sauberste Weg eine Datei ~/.my.cnf mit den Zugangsdaten, damit sie nicht im Befehlsverlauf auftauchen.
[client]
user=root
password=LA_TUA_PASSWORD
Alternativ können Sie die Optionen über eine Umgebungsvariable übergeben.
export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
Für PostgreSQL funktionieren ~/.pgpass, die Variablen PG* oder PG_OPTS.
export PG_OPTS="-U postgres -h localhost"
So verwenden Sie es
Der typische Ablauf ist vorher, Optimierung, nachher, Vergleich.
# 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
Für PostgreSQL genügt es, die Option -e postgres hinzuzufügen.
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
Die verfügbaren Optionen sind.
-e mysql|postgreswählt die Engine (Standard:mysql).-d DIRändert den Ordner, in dem die Momentaufnahmen gespeichert werden (Standard:./db_snapshots).-n Nlegt fest, wie viele Zeilen pro Abschnitt angezeigt werden (Standard: 20).
Geben Sie keine Bezeichnung an, erhält die Momentaufnahme Datum und Uhrzeit als Namen. Das ist praktisch, um das Wachstum über die Zeit zu überwachen.
Beispielausgabe
Hier ein Vergleich mit Testdaten, extra gebaut, um einen typischen Fall zu zeigen: Die Datenbank shop schrumpft nach der Optimierung, aber die Datenbank log wächst und die Binlogs verdoppeln sich.
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
Hier ist der Übeltäter offensichtlich: Die Tabelle log.eventi ist um mehr als 6 GiB gewachsen, und die Dateien mysql-bin.* sind von 3,7 auf 8,4 GiB gestiegen. Die Optimierung von shop hat funktioniert, aber der Gewinn wurde vom Rest aufgefressen.
Wie man die Ergebnisse zum Speicherplatz von MySQL liest
- Am Anfang jedes Abschnitts stehen die Einträge mit der größten Änderung. Eine Datenbank oder Tabelle, die nach der Optimierung nicht schrumpft (oder sogar wächst), ist der erste Verdächtige.
- Schrumpfen die Tabellen, das Dateisystem aber nicht, liegt der Platz woanders: Sehen Sie sich den Abschnitt über die Dateien im Datenverzeichnis an (Binlogs,
ibdata1, Undo,pg_wal). - MySQL/MariaDB:
ibdata1schrumpft nie, auch nicht mitOPTIMIZE TABLE. Mitinnodb_file_per_table=OFFist der Wert vondata_freegemeinsam genutzt und wenig aussagekräftig. - PostgreSQL: Ein einfaches
VACUUMgibt keinen Platz an das Betriebssystem zurück, dafür braucht manVACUUM FULL(oderpg_repack). Der angezeigte „rückgewinnbare“ Platz ist eine Schätzung auf Basis der toten Tupel. Wie PostgreSQL den Platz selbst berechnet, erklärt die Dokumentation zur Plattenbelegung.
Wachstum über die Zeit überwachen
Hängt das Problem nicht an einer einzelnen Optimierung, sondern an schrittweisem Wachstum, lohnt es sich, die Momentaufnahmen mit cron zu automatisieren, eine pro Stunde, und dann zwei beliebige Zeitpunkte zu vergleichen.
# crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot >/dev/null 2>&1
Ohne Bezeichnung heißt jede Momentaufnahme nach Datum und Uhrzeit (zum Beispiel 20261011-140000). Mit list sehen Sie die vorhandenen, mit compare vergleichen Sie zwei. In wenigen Tagen zeigt sich, welche Datenbank ungewöhnlich wächst.
./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000
Das vollständige Skript
Hier ist der Code. Kopieren Sie ihn in eine Datei namens db_space_compare.sh, machen Sie sie ausführbar, und Sie sind startklar.
#!/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
Abschließende Hinweise
- Das Skript liest den Speicherplatz von MySQL nur aus und ist auf den Datenbanken schreibgeschützt: Es führt nur
SELECTauf den Metadaten aus. Es schreibt ausschließlich in den Ordner der Momentaufnahmen. - Unter MySQL 8 setzt es
information_schema_stats_expiry = 0in der Sitzung, weil sonst die zwischengespeicherten Statistiken (bis zu 24 Stunden) auch nach der Optimierung „alte“ Werte zeigen könnten. - Schlägt eine Momentaufnahme auf halbem Weg fehl, wird sie automatisch entfernt, um keine unvollständigen Daten zu hinterlassen.
- Bevor Sie es in der Produktion einsetzen, probieren Sie es in einer Testumgebung aus und prüfen die Berechtigungen des Datenbankbenutzers.
Wenn Sie es nutzen und einen Fall finden, den es nicht abdeckt, oder Ideen zur Erweiterung haben (andere Engines, CSV-Export, E-Mail-Benachrichtigungen), schreiben Sie mir in die Kommentare. Läuft MySQL nach einem Upgrade gar nicht mehr, hilft dieser Artikel: MySQL startet nach dem Debian-12-Upgrade nicht mehr.
Schreibe einen Kommentar