¿Qué base de datos ocupa espacio en disco? El script para MySQL y PostgreSQL

Le pasa a todo el mundo tarde o temprano. El espacio en disco del servidor se llena, salta la alarma y la pregunta es siempre la misma. ¿Qué base de datos está ocupando todo este espacio?

Espacio en disco por base de datos: gráfico de anillo en cuatro sectores con tres barras de leyenda a la derecha

Yo me encontré con esto después de una optimización. Había lanzado OPTIMIZE TABLE con la esperanza de recuperar espacio, pero el sistema de archivos no bajaba como esperaba. Sin un «antes» con el que comparar el «después», era imposible saber si la optimización había funcionado. No sabía qué base de datos era la verdadera responsable. Ni si el espacio que faltaba estaba incluso en otra parte.

Las herramientas que tenía a mano solo respondían a la mitad del problema.

  • du muestra los archivos en disco, pero no dice a qué tabla pertenecen.
  • information_schema (o pg_class en PostgreSQL) da los tamaños lógicos, pero no ve los binlogs, ibdata1, los undo logs ni el WAL.
  • ambos fotografían un solo instante. La comparación en el tiempo sigue siendo un trabajo manual, hecho con copiar y pegar y una hoja de cálculo.

Necesitaba algo sencillo: fotografiar el estado de todas las bases de datos en un momento dado, volver a hacerlo más tarde y que me dijera qué ha cambiado, ordenando las diferencias de mayor a menor. Así el culpable salta enseguida a la vista.

Salió un único script de Bash. Solo necesita el cliente de la base de datos y las herramientas estándar de Linux (awk, du, df). Funciona con MySQL/MariaDB y con PostgreSQL. Lo comparto aquí con la esperanza de que le ahorre a alguien la misma tarde de frustración.

Qué hace: medir el espacio en disco

El script tiene cuatro comandos.

  • snapshot guarda el estado actual (bases de datos, tablas, archivos del directorio de datos, espacio del sistema de archivos).
  • compare compara dos instantáneas y muestra las diferencias, las mayores arriba.
  • report muestra las bases de datos y tablas más grandes de una sola instantánea.
  • list lista las instantáneas guardadas.

La comparación se divide en cuatro secciones, para no dejar rincones oscuros.

  • tamaño de cada base de datos (datos + índices).
  • espacio libre o recuperable (data_free en MySQL, estimación de tuplas muertas en PostgreSQL).
  • tamaño de cada tabla.
  • archivos reales en el directorio de datos: carpetas de las bases de datos, ibdata1, binlogs, undo, pg_wal.

La última sección suele ser la que resuelve el misterio. Muchas veces el espacio «anómalo» no pertenece a ninguna base de datos. Pertenece a los logs binarios o a los archivos compartidos de InnoDB.

Requisitos

  • Linux con Bash 4 o superior, awk, du, df (todo de serie).
  • el cliente mysql o psql, con un usuario que pueda leer los metadatos de todas las bases de datos.
  • permisos de root (o del usuario mysql/postgres) si quieres medir también los archivos en disco. Sin ellos, esa sección se omite con un aviso.

Instalación

Guarda el script (lo encontrarás al final del artículo) en un archivo y hazlo ejecutable.

nano db_space_compare.sh
chmod +x db_space_compare.sh

Credenciales

Para MySQL/MariaDB lo más limpio es un archivo ~/.my.cnf con las credenciales, así no aparecen en el historial de comandos.

[client]
user=root
password=LA_TUA_PASSWORD

Como alternativa puedes pasar las opciones con una variable de entorno.

export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"

Para PostgreSQL funcionan ~/.pgpass, las variables PG* o PG_OPTS.

export PG_OPTS="-U postgres -h localhost"

Cómo se usa

El flujo típico es antes, optimización, después, comparación.

# 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

Para PostgreSQL basta con añadir la opción -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

Las opciones disponibles son:

  • -e mysql|postgres elige el motor (por defecto: mysql).
  • -d DIR cambia la carpeta donde se guardan las instantáneas (por defecto: ./db_snapshots).
  • -n N fija cuántas filas mostrar por sección (por defecto: 20).

Si no indicas una etiqueta, la instantánea toma como nombre la fecha y la hora. Es cómodo para monitorizar el crecimiento en el tiempo.

Ejemplo de salida

Aquí tienes una comparación con datos de prueba, construidos a propósito para mostrar un caso típico. La base de datos shop se reduce tras la optimización, pero la base log crece y los binlogs se duplican.

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

Aquí el culpable es evidente: la tabla log.eventi ha crecido más de 6 GiB, y los archivos mysql-bin.* han pasado de 3,7 a 8,4 GiB. La optimización de shop funcionó, pero la ganancia se la comió el resto.

Cómo leer los resultados del espacio en disco

  • Arriba de cada sección están los elementos que más han cambiado. Una base de datos o una tabla que no baja (o incluso crece) tras la optimización es la primera sospechosa.
  • Si las tablas bajan pero el sistema de archivos no, el espacio está en otra parte. Mira la sección sobre los archivos del directorio de datos (binlogs, ibdata1, undo, pg_wal).
  • MySQL/MariaDB: ibdata1 nunca se encoge, ni siquiera con OPTIMIZE TABLE. Con innodb_file_per_table=OFF el valor de data_free es compartido y poco indicativo.
  • PostgreSQL: un simple VACUUM no devuelve espacio al sistema operativo, hace falta VACUUM FULL (o pg_repack). El espacio «recuperable» que se muestra es una estimación basada en las tuplas muertas.

Para PostgreSQL, la documentación sobre monitorizar el uso del disco explica cómo se calculan los tamaños de tablas e índices.

Monitorizar el espacio en disco en el tiempo

Si el problema no está ligado a una sola optimización sino a un crecimiento gradual. Conviene automatizar las instantáneas con cron, una por hora, y luego comparar dos momentos cualesquiera.

# crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot >/dev/null 2>&1

Sin etiqueta, cada instantánea toma como nombre la fecha y la hora (por ejemplo 20261011-140000). Con list ves las disponibles y con compare comparas dos. En pocos días se ve qué base de datos crece de forma anómala.

./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000

El script completo

Aquí está el código. Cópialo en un archivo llamado db_space_compare.sh, hazlo ejecutable y listo.

#!/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

Notas finales sobre el espacio en disco

  • El script es de solo lectura sobre las bases de datos. Solo ejecuta SELECT sobre los metadatos. Escribe únicamente en la carpeta de las instantáneas.
  • En MySQL 8 establece information_schema_stats_expiry = 0 en la sesión. De lo contrario, las estadísticas en caché (hasta 24 horas) podrían mostrar los valores «viejos» incluso después de la optimización.
  • Si una instantánea falla a medias, se elimina automáticamente para no dejar datos incompletos.
  • Antes de usarlo en producción, pruébalo en un entorno de pruebas y comprueba los permisos del usuario de la base de datos.

Si MySQL deja de arrancar tras una actualización, aquí tienes otro caso: MySQL no arranca tras actualizar a Debian 12.

Si lo usas y encuentras un caso que no cubre, o tienes ideas para ampliarlo (otros motores, exportación a CSV, avisos por correo), escríbeme en los comentarios.

Comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Aviso sobre Cookies en WordPress por Real Cookie Banner