<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>script bash - Matteo Cisilino</title>
	<atom:link href="https://www.cisilino.com/fr/tag/script-bash-fr/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cisilino.com/fr/</link>
	<description>Guide su WordPress, server e sicurezza</description>
	<lastBuildDate>Sat, 31 Oct 2026 10:03:59 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>Rotation des identifiants de base de données après une faille ISPConfig</title>
		<link>https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/</link>
					<comments>https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 10:03:56 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[gestion de serveurs]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[réponse aux incidents]]></category>
		<category><![CDATA[rotation des identifiants]]></category>
		<category><![CDATA[script bash]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1416</guid>

					<description><![CDATA[<p>Après une faille ISPConfig, je lance la rotation des identifiants de base de données avec reset_DB_password.sh : détection du rôle, sauvegardes automatiques et dry-run obligatoire.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/">Rotation des identifiants de base de données après une faille ISPConfig</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Je lance la rotation des identifiants dès que je suspecte une faille. Pas après l&rsquo;avoir confirmée. La rotation des identifiants de base de données sur un serveur ISPConfig a l&rsquo;air d&rsquo;une maintenance de routine. Elle cesse de l&rsquo;être le jour où vous trouvez un web shell sur un site hébergé. Ou un fichier de configuration que vous ne savez pas expliquer, posé dans une sauvegarde publique. À ce moment-là, chaque mot de passe lié à cette base de données doit être nouveau. Vérifié. Documenté. Pas seulement supposé correct.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/database-credential-rotation-ispconfig.webp" alt="rotation des identifiants de base de données : cadenas et clé au-dessus d'ISPConfig" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Quand la rotation des identifiants cesse-t-elle d&rsquo;être optionnelle ?</h2>



<p class="wp-block-paragraph">Quelques scénarios reviennent assez souvent pour que je garde un plan de rotation prêt à l&rsquo;avance. Je préfère ne pas en improviser un sous pression.</p>



<p class="wp-block-paragraph">Un plugin ou un CMS sur un site hébergé se fait compromettre. J&rsquo;ai raconté un cas réel sur ce type de serveur dans <a href="https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/">PHP2Shell : comment j&rsquo;ai protégé mes serveurs en attendant le correctif</a>, une vulnérabilité PHP qui permettait à un attaquant de déposer un web shell avant qu&rsquo;un correctif n&rsquo;existe. Une fois qu&rsquo;un web shell tourne sous un utilisateur d&rsquo;hébergement, il peut souvent lire le config.inc.php propre à ce site. Sur un serveur ISPConfig partagé, ce fichier se trouve juste à côté des identifiants de la base de données du panneau de contrôle lui-même. Pas seulement celui que l&rsquo;attaquant visait au départ.</p>



<p class="wp-block-paragraph">Un fichier de configuration se retrouve là où il ne devrait pas être. Une archive de sauvegarde oubliée dans un dossier public. Un dépôt poussé avec un fichier égaré dedans. Un journal plus lisible que prévu. config.inc.php et mysql_clientdb.conf n&rsquo;ont rien à faire dans aucun de ces endroits. Les erreurs arrivent malgré tout, y compris les miennes.</p>



<p class="wp-block-paragraph">Un administrateur système quitte l&rsquo;équipe. Quel que soit le ton du départ, les identifiants de base de données partagés qu&rsquo;il détenait ne m&rsquo;inspirent plus une confiance totale. La confiance ne se transmet pas automatiquement quand quelqu&rsquo;un passe la porte.</p>



<p class="wp-block-paragraph">Une intrusion suspectée, même non confirmée. C&rsquo;est le cas le plus difficile à expliquer à qui n&rsquo;a jamais géré un serveur sous suspicion d&rsquo;attaque. On n&rsquo;attend pas la preuve avant d&rsquo;agir. On présume la compromission et on lance la rotation. Une rotation inutile coûte un après-midi. Une rotation sautée, après une faille réelle, peut coûter tous les sites hébergés par le serveur.</p>



<h2 class="wp-block-heading">Pourquoi la rotation manuelle des identifiants est si sujette aux erreurs</h2>



<p class="wp-block-paragraph">La première fois que j&rsquo;ai fait la rotation des identifiants d&rsquo;ISPConfig à la main, j&rsquo;ai sous-estimé le nombre de fichiers concernés. Ils doivent tous changer ensemble, sinon quelque chose casse discrètement quelques heures plus tard.</p>



<p class="wp-block-paragraph">Il y a config.inc.php côté serveur, et une seconde copie côté interface. Les deux doivent recevoir les mêmes nouvelles valeurs. Il y a mysql_clientdb.conf, que ISPConfig lit pour gérer la base de données de chaque site hébergé, pas seulement son propre ménage interne. Dovecot, Pure-FTPd et Postfix conservent chacun leur propre copie de certains de ces identifiants. Le mail et le FTP peuvent cesser de s&rsquo;authentifier silencieusement si vous mettez à jour l&rsquo;utilisateur de la base de données et oubliez un seul de ces trois services.</p>



<p class="wp-block-paragraph">Il y a ensuite la topologie Master/Slave de certaines de mes installations. Un nœud Slave s&rsquo;authentifie auprès de la base de données de son Master via le réseau, avec un mot de passe distinct. Ce mot de passe doit être tourné sur le Master lui-même, pas seulement en local. Si l&rsquo;ordre est mauvais, un Slave peut se retrouver verrouillé hors du Master même qu&rsquo;il est censé répliquer.</p>



<p class="wp-block-paragraph">Rien de tout cela n&rsquo;est conceptuellement difficile. C&rsquo;est juste beaucoup de fichiers. Plusieurs services à redémarrer, dans un ordre sensé. Presque aucune tolérance pour une faute de frappe, surtout sous pression, juste après avoir trouvé quelque chose que vous ne vouliez pas trouver. Cette combinaison explique pourquoi la rotation des identifiants paraissait toujours plus risquée à la main que la tâche elle-même ne devrait l&rsquo;être. C&rsquo;est pour cette raison que j&rsquo;ai écrit reset_DB_password.sh, après avoir vécu ce problème moi-même plus d&rsquo;une fois.</p>



<h2 class="wp-block-heading">Ce que reset_DB_password.sh fait réellement</h2>



<p class="wp-block-paragraph">reset_DB_password.sh détecte si le nœud est Standalone, Master ou Slave. Il exécute ensuite uniquement les phases que ce rôle exige.</p>



<p class="wp-block-paragraph">La phase A tourne le mot de passe root local de MySQL. C&rsquo;est le mot de passe qu&rsquo;ISPConfig utilise lui-même pour gérer la base de données de chaque site hébergé. Le script met à jour mysql_clientdb.conf avec la nouvelle valeur.</p>



<p class="wp-block-paragraph">La phase B tourne le mot de passe de l&rsquo;utilisateur de contrôle ispconfig. Elle met à jour config.inc.php côté serveur et côté interface. Puis elle met à jour les identifiants correspondants dans Dovecot, Pure-FTPd et Postfix, pour que le mail et le FTP continuent de s&rsquo;authentifier sans passer manuellement par trois fichiers de configuration supplémentaires.</p>



<p class="wp-block-paragraph">La phase C ne tourne que sur un Slave. Elle se connecte à distance à la base de données du Master. Elle tourne là le mot de passe de l&rsquo;utilisateur Slave, puis met à jour dbmaster_password en local, pour que la réplication continue de s&rsquo;authentifier correctement avec la nouvelle valeur.</p>



<p class="wp-block-paragraph">Après la phase B, le script propose de redémarrer Dovecot, Pure-FTPd et Postfix pour vous. Je dis presque toujours oui. Une rotation d&rsquo;identifiants qui laisse un service mail tourner avec l&rsquo;ancien mot de passe n&rsquo;est pas terminée, elle est juste silencieusement cassée. Redémarrer dans la même exécution, juste après le changement des fichiers de configuration, garde tout cela dans une seule courte fenêtre de maintenance au lieu de trois fenêtres séparées découvertes plus tard.</p>



<h2 class="wp-block-heading">Des sauvegardes avant tout changement</h2>



<p class="wp-block-paragraph">Avant de toucher le moindre fichier, le script fait une copie rsync complète de /usr/local/ispconfig/ dans un dossier horodaté. Il tente aussi un mysqldump non bloquant des bases mysql et dbispconfig. Non bloquant volontairement : je préfère terminer une rotation d&rsquo;identifiants plutôt qu&rsquo;attendre un dump bloqué pour une raison qui n&rsquo;a rien à voir. Chaque fichier de configuration qu&rsquo;il modifie reçoit sa propre copie .bak avant qu&rsquo;une seule chaîne à l&rsquo;intérieur ne soit remplacée.</p>



<p class="wp-block-paragraph">Rien de tout cela ne remplace une stratégie de sauvegarde indépendante. Je prends toujours un snapshot séparé, ou une sauvegarde complète, avant de lancer ceci sur un serveur qui compte vraiment. Le propre avertissement du script dit la même chose, de façon plus directe que je ne viens de le faire.</p>



<h2 class="wp-block-heading">Toujours un dry run d&rsquo;abord</h2>



<p class="wp-block-paragraph">Chaque phase demande une confirmation avant de toucher à quoi que ce soit. &#8211;dry-run simule une exécution complète sans toucher un seul fichier. J&rsquo;utilise dry-run en premier sur chaque serveur, chaque fois. Même sur ceux dont j&rsquo;ai déjà tourné les identifiants, et que je crois bien connaître. &#8211;skip-local-root saute la phase A, pour quand seul le mot de passe de l&rsquo;utilisateur de contrôle doit changer. &#8211;role=master ou &#8211;role=slave remplace la détection automatique, pour le cas rare où elle ne lit pas correctement la topologie d&rsquo;un serveur.</p>



<p class="wp-block-paragraph">Les mots de passe qu&rsquo;il génère comptent 32 caractères, lettres et chiffres uniquement. Assez longs pour résister à une tentative de deviner. Assez simples pour que les parseurs de MySQL et de PHP ne trébuchent jamais sur un symbole égaré dans une chaîne de connexion. J&rsquo;ai testé le script sur ISPConfig 3.3.1p1. Je ne l&rsquo;ai pas lancé sur des branches plus anciennes ou plus récentes. Je ne présumerais pas un comportement identique là-bas, sans vérifier moi-même d&rsquo;abord.</p>



<p class="wp-block-paragraph">Le rôle d&rsquo;un nœud n&rsquo;est pas toujours évident au premier coup d&rsquo;œil, surtout sur un serveur dont vous avez hérité plutôt que celui que vous avez construit vous-même. C&rsquo;est pour cela que &#8211;role existe, et pourquoi la sortie du dry-run vaut la peine d&rsquo;être lue ligne par ligne la première fois, pas survolée. Je préfère passer deux minutes à confirmer qu&rsquo;un Slave est vraiment un Slave plutôt que de le découvrir à la dure en plein milieu de la rotation.</p>



<h2 class="wp-block-heading">Ce que je vous dirais avant de l&rsquo;exécuter</h2>



<p class="wp-block-paragraph"><strong>Avertissement</strong> : le script est fourni tel quel, à titre informatif. Son utilisation se fait entièrement à vos risques : je n&rsquo;assume aucune responsabilité pour une perte de données, une interruption de service, ou toute erreur causée par un usage incorrect ou par un contexte différent de celui dans lequel je l&rsquo;utilise moi-même. Gardez toujours une sauvegarde indépendante ou un snapshot de VM avant de l&rsquo;exécuter en production, quoi que la propre étape de sauvegarde du script capture déjà pour vous.</p>



<p class="wp-block-paragraph">C&rsquo;est un logiciel libre. Vous pouvez le redistribuer et le modifier, avec attribution à moi comme auteur original. Le code source de reset_DB_password.sh, et le reste de la collection ispconfig-fast-tools, est sur GitHub à l&rsquo;adresse <a href="https://github.com/matteocisilino/ispconfig-fast-tools" target="_blank" rel="noopener">github.com/matteocisilino/ispconfig-fast-tools</a>.</p>



<h2 class="wp-block-heading">La rotation des identifiants de base de données est une habitude, pas une tâche de nettoyage</h2>



<p class="wp-block-paragraph">La rotation des identifiants de base de données après une faille confirmée, c&rsquo;est simplement une réponse aux incidents compétente. La plupart des administrateurs le feraient sans qu&rsquo;on ait à le leur répéter. L&rsquo;habitude la plus difficile, c&rsquo;est de le faire après une faille simplement suspectée, avant d&rsquo;avoir la moindre preuve. C&rsquo;est la seule version du plan qui limite vraiment les dégâts, tant qu&rsquo;ils sont encore limitables. Un script qui exécute les bonnes phases selon le rôle d&rsquo;un serveur, prend ses propres sauvegardes d&rsquo;abord, et me laisse faire un dry-run complet avant de toucher à quoi que ce soit de réel. Voilà ce qui rend cette habitude praticable un mardi soir, plutôt que théorique dans un article de blog.</p>



<p class="wp-block-paragraph">ISPConfig, MySQL, PHP, Dovecot, Pure-FTPd, Postfix et les autres noms mentionnés sont des marques déposées de leurs propriétaires respectifs. Ils sont utilisés uniquement pour identifier les produits.</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/">Rotation des identifiants de base de données après une faille ISPConfig</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Quelle base de données occupe l&#8217;espace disque ? Un script pour MySQL et PostgreSQL</title>
		<link>https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/</link>
					<comments>https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 07:53:00 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[espace disque]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[script bash]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=260</guid>

					<description><![CDATA[<p>Le disque du serveur se remplit et vous ne savez pas quelle base en est responsable ? Un script pour mesurer l'espace de MySQL et PostgreSQL et le comparer dans le temps.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/">Quelle base de données occupe l’espace disque ? Un script pour MySQL et PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Cela arrive à tout le monde tôt ou tard. L&rsquo;espace disque du serveur se remplit, l&rsquo;alarme se déclenche et la question est toujours la même. <strong>Quelle base de données occupe tout cet espace ?</strong></p>
<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/espace-disque-bases-mysql-postgresql.webp" alt="Espace disque : graphique en anneau divisé en quatre secteurs de couleurs, avec trois barres de légende à droite" width="1200" height="630"/></figure>
<p>Je m&rsquo;y suis retrouvé confronté après une optimisation. J&rsquo;avais lancé <code>OPTIMIZE TABLE</code> en espérant récupérer de l&rsquo;espace, mais le système de fichiers ne baissait pas comme prévu. Sans un « avant » à comparer à l&rsquo;« après », impossible de savoir si l&rsquo;optimisation avait fonctionné. Impossible aussi de savoir quelle base était responsable, et si l&rsquo;espace disque manquant n&rsquo;était pas carrément ailleurs.</p>
<p>Les outils dont je disposais ne répondaient qu&rsquo;à la moitié du problème :</p>
<ul class="wp-block-list">
<li><code>du</code> montre les fichiers sur le disque, mais ne dit pas à quelle table ils appartiennent ;</li>
<li><code>information_schema</code> (ou <code>pg_class</code> sur PostgreSQL) donne les tailles logiques, mais ne voit ni les binlogs, ni <code>ibdata1</code>, ni les undo logs, ni les WAL ;</li>
<li>tous deux photographient un seul instant : la comparaison dans le temps reste un travail manuel, fait de copier-coller et d&rsquo;un tableur.</li>
</ul>
<p>Il me fallait quelque chose de simple : <strong>photographier l&rsquo;état de toutes les bases à un moment donné, refaire la photo plus tard et me dire ce qui a changé</strong>, en classant les différences de la plus grande à la plus petite. Le coupable saute ainsi aux yeux.</p>
<p>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 (<code>awk</code>, <code>du</code>, <code>df</code>). Il fonctionne avec <strong>MySQL/MariaDB</strong> et avec <strong>PostgreSQL</strong>. Je le partage ici dans l&rsquo;espoir d&rsquo;épargner à quelqu&rsquo;un le même après-midi de frustration.</p>
<h2 class="wp-block-heading">Ce qu&rsquo;il fait</h2>
<p>Le script a quatre commandes :</p>
<ul class="wp-block-list">
<li><code>snapshot</code> enregistre l&rsquo;état actuel (bases, tables, fichiers du répertoire de données, espace du système de fichiers) ;</li>
<li><code>compare</code> compare deux instantanés et montre les différences, les plus grandes en tête ;</li>
<li><code>report</code> montre les bases et tables les plus grosses d&rsquo;un seul instantané ;</li>
<li><code>list</code> liste les instantanés enregistrés.</li>
</ul>
<p>La comparaison est divisée en quatre sections, pour ne laisser aucun angle mort :</p>
<ul class="wp-block-list">
<li><strong>taille de chaque base</strong> (données + index) ;</li>
<li><strong>espace libre ou récupérable</strong> (<code>data_free</code> sur MySQL, estimation des tuples morts sur PostgreSQL) ;</li>
<li><strong>taille de chaque table</strong> ;</li>
<li><strong>fichiers réels du répertoire de données</strong> : dossiers des bases, <code>ibdata1</code>, binlogs, undo, <code>pg_wal</code>.</li>
</ul>
<p>La dernière section est souvent celle qui résout le mystère. Bien des fois, l&rsquo;espace « anormal » n&rsquo;appartient à aucune base, mais aux journaux binaires ou aux fichiers partagés d&rsquo;InnoDB.</p>
<h2 class="wp-block-heading">Prérequis</h2>
<ul class="wp-block-list">
<li>Linux avec Bash 4 ou supérieur, <code>awk</code>, <code>du</code>, <code>df</code> (le tout en standard) ;</li>
<li>le client <code>mysql</code> ou <code>psql</code>, avec un utilisateur capable de lire les métadonnées de toutes les bases ;</li>
<li>les droits root (ou ceux de l&rsquo;utilisateur <code>mysql</code>/<code>postgres</code>) si vous voulez aussi mesurer les fichiers sur disque ; sans eux, cette section est ignorée avec un avertissement.</li>
</ul>
<h2 class="wp-block-heading">Installation</h2>
<p>Enregistrez le script (vous le trouverez à la fin de l&rsquo;article) dans un fichier et rendez-le exécutable :</p>
<pre class="wp-block-code"><code>nano db_space_compare.sh
chmod +x db_space_compare.sh</code></pre>
<h3 class="wp-block-heading">Identifiants</h3>
<p>Pour MySQL/MariaDB, le plus propre est un fichier <code>~/.my.cnf</code> contenant les identifiants, afin qu&rsquo;ils n&rsquo;apparaissent pas dans l&rsquo;historique des commandes :</p>
<pre class="wp-block-code"><code>&#91;client]
user=root
password=LA_TUA_PASSWORD</code></pre>
<p>Vous pouvez aussi passer les options via une variable d&rsquo;environnement :</p>
<pre class="wp-block-code"><code>export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"</code></pre>
<p>Pour PostgreSQL, <code>~/.pgpass</code>, les variables <code>PG*</code> ou <code>PG_OPTS</code> fonctionnent :</p>
<pre class="wp-block-code"><code>export PG_OPTS="-U postgres -h localhost"</code></pre>
<h2 class="wp-block-heading">Comment l&rsquo;utiliser</h2>
<p>Le déroulement type est <em>avant, optimisation, après, comparaison</em> :</p>
<pre class="wp-block-code"><code># 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</code></pre>
<p>Pour PostgreSQL, il suffit d&rsquo;ajouter l&rsquo;option <code>-e postgres</code> :</p>
<pre class="wp-block-code"><code>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</code></pre>
<p>Les options disponibles sont :</p>
<ul class="wp-block-list">
<li><code>-e mysql|postgres</code> choisit le moteur (par défaut : <code>mysql</code>) ;</li>
<li><code>-d DIR</code> change le dossier où sont enregistrés les instantanés (par défaut : <code>./db_snapshots</code>) ;</li>
<li><code>-n N</code> fixe le nombre de lignes affichées par section (par défaut : 20).</li>
</ul>
<p>Si vous n&rsquo;indiquez pas d&rsquo;étiquette, l&rsquo;instantané prend pour nom la date et l&rsquo;heure. C&rsquo;est pratique pour surveiller la croissance dans le temps.</p>
<h2 class="wp-block-heading">Exemple de sortie</h2>
<p>Voici une comparaison sur des données de test, construites exprès pour montrer un cas typique. La base <code>shop</code> diminue après l&rsquo;optimisation, mais la base <code>log</code> grossit et les binlogs doublent.</p>
<pre class="wp-block-code"><code>Confronto 'prima' (2026-10-11 20:00:00)  -&gt;  'dopo' (2026-10-11 22:00:00)
Filesystem /var: usato 46.6 GiB -&gt; 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</code></pre>
<p>Ici le coupable est évident. La table <code>log.eventi</code> a grossi de plus de 6 GiB. Les fichiers <code>mysql-bin.*</code> sont passés de 3,7 à 8,4 GiB. L&rsquo;optimisation sur <code>shop</code> a fonctionné, mais le gain d&rsquo;espace disque a été englouti par le reste.</p>
<h2 class="wp-block-heading">Comment lire les résultats</h2>
<ul class="wp-block-list">
<li>En haut de chaque section se trouvent les éléments qui ont le plus changé. Une base ou une table qui <strong>ne diminue pas</strong> (voire grossit) après l&rsquo;optimisation est le premier suspect.</li>
<li>Si les tables diminuent mais pas le système de fichiers, l&rsquo;espace disque est ailleurs. Regardez la section sur les fichiers du répertoire de données (binlogs, <code>ibdata1</code>, undo, <code>pg_wal</code>).</li>
<li><strong>MySQL/MariaDB :</strong> <code>ibdata1</code> ne rétrécit jamais, pas même avec <code>OPTIMIZE TABLE</code>. Avec <code>innodb_file_per_table=OFF</code>, la valeur de <code>data_free</code> est partagée et peu indicative.</li>
<li><strong>PostgreSQL :</strong> un simple <code>VACUUM</code> ne rend pas d&rsquo;espace au système d&rsquo;exploitation, il faut <code>VACUUM FULL</code> (ou <code>pg_repack</code>). L&rsquo;espace « récupérable » affiché est une estimation fondée sur les tuples morts.</li>
</ul>
<p>Pour PostgreSQL, la <a href="https://www.postgresql.org/docs/current/diskusage.html" target="_blank" rel="noopener">documentation officielle sur l&rsquo;utilisation du disque</a> explique comment mesurer l&rsquo;espace occupé.</p>
<h2 class="wp-block-heading">Surveiller l&rsquo;espace disque dans le temps</h2>
<p>Si le problème n&rsquo;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 :</p>
<pre class="wp-block-code"><code># crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot &gt;/dev/null 2&gt;&amp;1</code></pre>
<p>Sans étiquette, chaque instantané prend pour nom la date et l&rsquo;heure (par exemple <code>20261011-140000</code>). Avec <code>list</code> vous voyez ceux qui existent et avec <code>compare</code> vous en comparez deux. En quelques jours, on voit quelle base grossit de façon anormale.</p>
<pre class="wp-block-code"><code>./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000</code></pre>
<h2 class="wp-block-heading">Le script complet</h2>
<p>Voici le code. Copiez-le dans un fichier nommé <code>db_space_compare.sh</code>, rendez-le exécutable et vous êtes prêt.</p>
<pre class="wp-block-code"><code>#!/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 &#91;opzioni] snapshot &#91;etichetta]   (default: data-ora)
#   db_space_compare.sh &#91;opzioni] compare  &lt;prima&gt; &lt;dopo&gt;
#   db_space_compare.sh &#91;opzioni] report   &lt;etichetta&gt;
#   db_space_compare.sh &#91;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: $*" &gt;&amp;2; exit 1; }
warn() { echo "ATTENZIONE: $*" &gt;&amp;2; }
usage() { sed -n '2,/^$/{s/^# \{0,1\}//;p}' "$0"; exit "${1:-0}"; }

cleanup() {
  if &#91;&#91; -n $SNAP_IN_PROGRESS ]]; then
    rm -rf -- "$SNAP_IN_PROGRESS"
    echo "Fotografia incompleta rimossa: $SNAP_IN_PROGRESS" &gt;&amp;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))

&#91;&#91; $ENGINE == mysql || $ENGINE == postgres ]] || die "motore non valido: '$ENGINE' (usa mysql o postgres)"
&#91;&#91; $TOPN =~ ^&#91;0-9]+$ ]] || die "-n deve essere un numero"

read -r -a MYSQL_ARGS &lt;&lt;&lt; "${MYSQL_OPTS:-}"
read -r -a PG_ARGS    &lt;&lt;&lt; "${PG_OPTS:-}"

my() { mysql "${MYSQL_ARGS&#91;@]+"${MYSQL_ARGS&#91;@]}"}" --batch --skip-column-names "$@"; }
pg() { psql  "${PG_ARGS&#91;@]+"${PG_ARGS&#91;@]}"}" -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 &lt; 0) ? -b : b; i = 1
  while (a &gt;= 1024 &amp;&amp; i &lt; 6) { a /= 1024; i++ }
  if (i == 1) return sprintf("%s%d B", (b &lt; 0 ? "-" : ""), a)
  return sprintf("%s%.1f %s", (b &lt; 0 ? "-" : ""), a, u&#91;i])
}
function hs(b) { return (b &gt; 0 ? "+" : "") h(b) }
function key(   k, i) { k = $1; for (i = 2; i &lt;= nk; i++) k = k "." $i; return k }
function val(   n, c, i, s) { n = split(vc, c, ","); s = 0; for (i = 1; i &lt;= n; i++) s += $(c&#91;i]); return s }
'

meta_get() { awk -F= -v k="$2" '$1 == k { sub(/^&#91;^=]*=/, ""); 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
  : &gt; "$out"
  if &#91;&#91; -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&gt;/dev/null || true; } |
    awk -F'\t' -v p="$d/" '
      { n = $2
        if (index(n, p) == 1) n = substr(n, length(p) + 1)
        if (n ~ /\.&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]$/) sub(/&#91;0-9]+$/, "*", n)
        s&#91;n] += $1 }
      END { for (n in s) printf "%s\t%.0f\n", n, s&#91;n] }' &gt; "$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 &#91;&#91; -n $datadir &amp;&amp; -e $datadir ]]; then
      { df -P -B1 -- "$datadir" 2&gt;/dev/null || true; } |
        awk 'NR == 2 { print "fs_mount=" $6; print "fs_used=" $3; print "fs_avail=" $4 }'
    fi
  } &gt; "$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 &gt;/dev/null 2&gt;&amp;1 || die "client 'mysql' non trovato"
  my -e "SELECT 1" &gt;/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 &#91;&#91; -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')" &gt; "$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" &gt; "$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 &gt;/dev/null 2&gt;&amp;1 || die "client 'psql' non trovato"
  pg -d "$maindb" -c "SELECT 1" &gt;/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" &gt; "$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).
  : &gt; "$dir/tables.tsv"
  while IFS= read -r db; do
    &#91;&#91; -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 &gt; 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 &lt;&gt; 'information_schema'
         AND s.schemaname NOT LIKE 'pg_toast%'" &lt; /dev/null &gt;&gt; "$dir/tables.tsv" \
      || warn "impossibile leggere le tabelle del database '$db'"
  done &lt; "$dir/.dblist"

  pg -d "$maindb" -c "SELECT datname, pg_database_size(oid) FROM pg_database WHERE datallowconn AND NOT datistemplate" &gt; "$dir/.dbsize"
  awk -F'\t' '
    FILENAME == ARGV&#91;1] { d&#91;$1] += $4; i&#91;$1] += $5; f&#91;$1] += $6; next }
    { printf "%s\t%.0f\t%.0f\t%.0f\t%s\n", $1, d&#91;$1], i&#91;$1], f&#91;$1], $2 }' \
    "$dir/tables.tsv" "$dir/.dbsize" &gt; "$dir/databases.tsv"

  datadir=$(pg -d "$maindb" -c "SHOW data_directory" 2&gt;/dev/null || true)
  version=$(pg -d "$maindb" -c "SHOW server_version")
  collect_disk "$datadir" "$dir/disk.tsv"

  # Dettaglio di base/&lt;oid&gt; con il nome del database al posto dell'oid
  if &#91;&#91; -n $datadir &amp;&amp; -r $datadir/base &amp;&amp; -x $datadir/base ]]; then
    pg -d "$maindb" -c "SELECT oid, datname FROM pg_database" &gt; "$dir/.oidmap"
    { du -sb -- "$datadir"/base/* 2&gt;/dev/null || true; } |
      awk -F'\t' '
        FILENAME == ARGV&#91;1] { name&#91;$1] = $2; next }
        { o = $2; sub(/.*\//, "", o); printf "base/%s\t%s\n", ((o in name) ? name&#91;o] : o), $1 }' \
        "$dir/.oidmap" - &gt;&gt; "$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)
  &#91;&#91; -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 &#91;&#91; ! -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 &lt;= top &amp;&amp; $1 &gt; 0 { printf "  %-60s %12s\n", $2, $3 }'
}

compare_section() {
  local fa=$1 fb=$2 nk=$3 vc=$4 title=$5
  &#91;&#91; -f $fa ]] || fa=/dev/null
  &#91;&#91; -f $fb ]] || fb=/dev/null
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $fa &amp;&amp; ! -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&#91;1] { k = key(); A&#91;k] = val(); S&#91;k] = 1; next }
                        { k = key(); B&#91;k] = val(); S&#91;k] = 1 }
    END {
      for (k in S) {
        inA = (k in A); inB = (k in B)
        a = inA ? A&#91;k] : 0; b = inB ? B&#91;k] : 0; d = b - a
        if (!inA)                 note = "NUOVO"
        else if (!inB)            note = "RIMOSSO"
        else if (a &gt; 0 &amp;&amp; d != 0) note = sprintf("%+.1f%%", 100 * d / a)
        else                      note = ""
        printf "%.0f\t%s\t%s\t%s\t%s\t%s\n", (d &lt; 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 &lt;= 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')}
  &#91;&#91; $label =~ ^&#91;A-Za-z0-9]&#91;A-Za-z0-9._-]*$ ]] || die "etichetta non valida: usa solo lettere, numeri, . _ -"
  local dir=$OUTDIR/$label
  &#91;&#91; ! -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 &lt; "$dir/tables.tsv") tabelle)"
  print_fs "$dir"
  top_section "$dir/databases.tsv" 1 5 "Database più grandi"
}

cmd_report() {
  local label=${1:-}
  &#91;&#91; -n $label ]] || die "uso: report &lt;etichetta&gt;"
  local d=$OUTDIR/$label
  &#91;&#91; -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:-}
  &#91;&#91; -n $a &amp;&amp; -n $b ]] || die "uso: compare &lt;etichetta_prima&gt; &lt;etichetta_dopo&gt;"
  &#91;&#91; $a != "$b" ]] || die "le due etichette sono uguali"
  local da=$OUTDIR/$a db=$OUTDIR/$b
  &#91;&#91; -f $da/meta.txt ]] || die "fotografia '$a' non trovata in $OUTDIR"
  &#91;&#91; -f $db/meta.txt ]] || die "fotografia '$b' non trovata in $OUTDIR"
  &#91;&#91; $(meta_get "$da" engine) == "$(meta_get "$db" engine)" ]] || warn "le due fotografie sono di motori diversi"

  echo "Confronto '$a' ($(meta_get "$da" timestamp))  -&gt;  '$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 &#91;&#91; -n $ua &amp;&amp; -n $ub ]]; then
    awk -v a="$ua" -v b="$ub" -v m="$m" "$AWK_LIB"'
      BEGIN { printf "Filesystem %s: usato %s -&gt; %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 &lt;&lt;'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 &#91;&#91; ! -d $OUTDIR ]]; then echo "Nessuna fotografia in $OUTDIR"; return 0; fi
  local d found=0
  for d in "$OUTDIR"/*/; do
    &#91;&#91; -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
  &#91;&#91; $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</code></pre>
<h2 class="wp-block-heading">Notes finales</h2>
<ul class="wp-block-list">
<li>Le script est en <strong>lecture seule</strong> sur les bases : il n&rsquo;exécute que <code>SELECT</code> sur les métadonnées. Il n&rsquo;écrit que dans le dossier des instantanés.</li>
<li>Sur MySQL 8, il définit <code>information_schema_stats_expiry = 0</code> dans la session. Sinon, les statistiques en cache (jusqu&rsquo;à 24 heures) pourraient afficher d&rsquo;« anciennes » valeurs même après l&rsquo;optimisation.</li>
<li>Si un instantané échoue à mi-parcours, il est supprimé automatiquement pour ne pas laisser de données incomplètes.</li>
<li>Avant de l&rsquo;utiliser en production, essayez-le dans un environnement de test et vérifiez les droits de l&rsquo;utilisateur de la base.</li>
</ul>
<p>Si MySQL ne démarre plus après une mise à jour, voyez <a href="https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/">l&rsquo;article sur Debian 12</a>.</p>
<p>Si vous l&rsquo;utilisez pour surveiller l&rsquo;espace disque et trouvez un cas qu&rsquo;il ne couvre pas, écrivez-moi dans les commentaires. Vos idées pour l&rsquo;étendre sont aussi bienvenues (autres moteurs, export CSV, alertes par e-mail).</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/">Quelle base de données occupe l’espace disque ? Un script pour MySQL et PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
