<?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/tag/script-bash/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cisilino.com</link>
	<description>Guide su WordPress, server e sicurezza</description>
	<lastBuildDate>Sat, 31 Oct 2026 10:03:56 +0000</lastBuildDate>
	<language>it-IT</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>Rotazione delle credenziali database dopo una violazione ISPConfig</title>
		<link>https://www.cisilino.com/2026/10/31/rotazione-credenziali-database-violazione-ispconfig/</link>
					<comments>https://www.cisilino.com/2026/10/31/rotazione-credenziali-database-violazione-ispconfig/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 10:03:56 +0000</pubDate>
				<category><![CDATA[Server e database]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[risposta agli incidenti]]></category>
		<category><![CDATA[rotazione credenziali]]></category>
		<category><![CDATA[script bash]]></category>
		<category><![CDATA[server management]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1414</guid>

					<description><![CDATA[<p>Perché la rotazione delle credenziali database diventa urgente dopo una violazione ISPConfig sospetta, e come lo script a tre fasi la gestisce con backup integrati.</p>
<p>The post <a href="https://www.cisilino.com/2026/10/31/rotazione-credenziali-database-violazione-ispconfig/">Rotazione delle credenziali database dopo una violazione ISPConfig</a> first appeared on <a href="https://www.cisilino.com">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Ruoto le credenziali nel momento in cui sospetto una violazione. Non dopo averla confermata. La rotazione delle credenziali database su un server ISPConfig suona come manutenzione di routine. Smette di essere routine il giorno in cui trovi una web shell su un sito ospitato. O un file di configurazione che non sai spiegare, finito in un backup pubblico. A quel punto, ogni password legata a quello stack di database deve essere nuova. Verificata. Documentata. Non semplicemente presunta a posto.</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="rotazione delle credenziali database: lucchetto e chiave sopra un database ISPConfig" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Quando la rotazione non è più facoltativa?</h2>



<p class="wp-block-paragraph">Ci sono alcuni scenari che tornano a sufficienza da farmi tenere pronto un piano di rotazione. Preferisco non improvvisarne uno sotto pressione.</p>



<p class="wp-block-paragraph">Un plugin o un CMS su un sito ospitato viene compromesso. Ho raccontato un caso reale su questo tipo di server in <a href="https://www.cisilino.com/2026/10/29/php2shell-cve-2026-109618-mitigazione-server/">PHP2Shell: come ho protetto i server in attesa della patch</a>, una vulnerabilità PHP che permetteva a un attaccante di piazzare una web shell prima che esistesse una patch. Una volta che una web shell viene eseguita sotto un utente di hosting, spesso può leggere il config.inc.php di quel sito. Su un box ISPConfig condiviso, quel file sta proprio accanto alle credenziali del database del pannello di controllo stesso. Non solo la credenziale che l&#8217;attaccante stava effettivamente cercando.</p>



<p class="wp-block-paragraph">Un file di configurazione finisce dove non dovrebbe. Un archivio di backup lasciato in una cartella pubblica. Un repository pubblicato con un file fuori posto. Un log più leggibile di quanto chiunque intendesse. config.inc.php e mysql_clientdb.conf non hanno motivo di stare in nessuno di questi posti. Gli errori succedono comunque, compresi i miei.</p>



<p class="wp-block-paragraph">Un amministratore di sistema lascia il team. Per quanto amichevole sia l&#8217;addio, le credenziali database condivise che deteneva sono credenziali di cui non mi fido più del tutto. La fiducia non si trasferisce automaticamente quando qualcuno esce dalla porta.</p>



<p class="wp-block-paragraph">Un&#8217;intrusione sospetta, anche non confermata. Questo è il caso più difficile da spiegare a chi non ha mai gestito un server sotto sospetto di attacco. Non si aspetta la prova prima di agire. Si presume la compromissione e si ruota. Una rotazione non necessaria costa un pomeriggio. Una saltata, dopo una violazione reale, può costare ogni sito ospitato dal server.</p>



<h2 class="wp-block-heading">Perché la rotazione delle credenziali database manuale è così incline all&#8217;errore</h2>



<p class="wp-block-paragraph">La prima volta che ho ruotato a mano le credenziali database di ISPConfig, ho sottovalutato quanti file fossero coinvolti. Devono tutti cambiare insieme, altrimenti qualcosa si rompe silenziosamente poche ore dopo.</p>



<p class="wp-block-paragraph">C&#8217;è config.inc.php sul lato server, e una seconda copia sul lato interfaccia. Entrambe devono avere gli stessi nuovi valori. C&#8217;è mysql_clientdb.conf, che ISPConfig legge per gestire il database di ogni sito ospitato, non solo la propria manutenzione interna. Dovecot, Pure-FTPd e Postfix mantengono ciascuno una propria copia di alcune delle stesse credenziali. Mail e FTP possono smettere di autenticarsi silenziosamente se aggiorni l&#8217;utente del database e ne dimentichi anche solo uno dei tre.</p>



<p class="wp-block-paragraph">Poi c&#8217;è la topologia Master/Slave che alcune delle mie installazioni usano. Un nodo Slave si autentica al database del suo Master sulla rete, con una password separata. Quella password va ruotata sul Master stesso, non solo in locale. Sbaglia la sequenza, e uno Slave può chiudersi fuori dallo stesso Master da cui dovrebbe replicare.</p>



<p class="wp-block-paragraph">Niente di tutto questo è concettualmente difficile. Sono tanti file. Diversi servizi da riavviare, in un ordine sensato. Quasi nessuna tolleranza per un errore di digitazione, specialmente mentre lo fai sotto pressione, appena dopo aver trovato qualcosa che non volevi trovare. Questa combinazione è esattamente il motivo per cui la rotazione delle credenziali database continuava a sembrarmi più rischiosa a mano di quanto il compito stesso dovrebbe essere. È per questo che ho scritto reset_DB_password.sh, dopo aver vissuto il problema io stesso più di una volta.</p>



<h2 class="wp-block-heading">Cosa fa davvero reset_DB_password.sh</h2>



<p class="wp-block-paragraph">reset_DB_password.sh rileva se il nodo è Standalone, Master o Slave. Poi esegue solo le fasi che quel ruolo richiede.</p>



<p class="wp-block-paragraph">La Fase A ruota la password root locale di MySQL. È la password che ISPConfig stesso usa per gestire il database di ogni sito ospitato. Lo script aggiorna mysql_clientdb.conf per farlo corrispondere al nuovo valore.</p>



<p class="wp-block-paragraph">La Fase B ruota la password dell&#8217;utente di controllo ispconfig. Aggiorna config.inc.php sia sul lato server che sul lato interfaccia. Poi aggiorna le credenziali corrispondenti dentro Dovecot, Pure-FTPd e Postfix, così mail e FTP continuano ad autenticarsi senza un passaggio manuale separato su altri tre file di configurazione.</p>



<p class="wp-block-paragraph">La Fase C viene eseguita solo su uno Slave. Si connette da remoto al database del Master. Ruota lì la password dell&#8217;utente Slave, poi aggiorna dbmaster_password in locale, così la replica continua ad autenticarsi correttamente con il nuovo valore.</p>



<p class="wp-block-paragraph">Dopo la Fase B, lo script offre di riavviare Dovecot, Pure-FTPd e Postfix per te. Dico quasi sempre di sì. Una rotazione delle credenziali che lascia un servizio di posta in esecuzione con la vecchia password non è finita, è solo silenziosamente rotta. Riavviare nella stessa sessione, subito dopo il cambio dei file di configurazione, mantiene tutto dentro una sola breve finestra di manutenzione invece di tre scoperte separatamente più avanti.</p>



<h2 class="wp-block-heading">Backup prima che qualcosa cambi</h2>



<p class="wp-block-paragraph">Prima di toccare un solo file, lo script esegue una copia rsync completa di /usr/local/ispconfig/ in una cartella con timestamp. Tenta anche un mysqldump non bloccante dei database mysql e dbispconfig. Non bloccante di proposito: preferisco finire una rotazione delle credenziali database piuttosto che restare in attesa di un dump bloccato per qualche motivo non collegato. Ogni file di configurazione che modifica riceve prima una propria copia .bak, prima che qualsiasi stringa al suo interno venga sostituita.</p>



<p class="wp-block-paragraph">Niente di tutto questo sostituisce una strategia di backup indipendente. Continuo a fare uno snapshot separato, o un backup completo, prima di eseguire questo script su qualsiasi server che conta davvero. Il disclaimer dello script stesso dice la stessa cosa, in modo più diretto di quanto abbia appena fatto io.</p>



<h2 class="wp-block-heading">Dry run prima, sempre</h2>



<p class="wp-block-paragraph">Ogni fase chiede conferma prima di cambiare qualcosa. &#8211;dry-run simula un&#8217;intera esecuzione senza toccare un solo file. Uso il dry-run prima su ogni server, ogni volta. Anche su quelli su cui ho già ruotato le credenziali, e penso di conoscere bene. &#8211;skip-local-root salta la Fase A, per quando serve ruotare solo la password dell&#8217;utente di controllo. &#8211;role=master o &#8211;role=slave forzano il rilevamento automatico, per il raro caso in cui non legga correttamente da solo la topologia di un server.</p>



<p class="wp-block-paragraph">Le password che genera sono di 32 caratteri, solo lettere e cifre. Lunghe abbastanza da resistere a un tentativo di indovinare. Semplici abbastanza che i parser di MySQL e PHP non si inceppino mai su un simbolo fuori posto in una stringa di connessione. Ho testato lo script su ISPConfig 3.3.1p1. Non l&#8217;ho eseguito contro rami più vecchi o più nuovi. Non darei per scontato un comportamento identico lì, senza prima verificarlo io stesso.</p>



<p class="wp-block-paragraph">Il ruolo di un nodo non è sempre ovvio a colpo d&#8217;occhio, specialmente su un server che hai ereditato invece di costruire tu stesso. È per questo che esiste &#8211;role, e perché l&#8217;output del dry-run merita di essere letto riga per riga la prima volta, non scorso velocemente. Preferirei passare due minuti a confermare che uno Slave è davvero uno Slave, piuttosto che scoprirlo nel modo peggiore a metà rotazione.</p>



<h2 class="wp-block-heading">Cosa ti direi prima che tu lo esegua</h2>



<p class="wp-block-paragraph"><strong>Avviso</strong>: lo script è fornito così com&#8217;è, a scopo informativo. L&#8217;uso è sotto la tua esclusiva responsabilità: non mi assumo alcuna responsabilità per perdita di dati, interruzioni di servizio, o qualsiasi errore generato da un utilizzo scorretto o da un contesto diverso da quello in cui lo eseguo io stesso. Fai sempre un backup indipendente o uno snapshot VM prima di eseguirlo in produzione, indipendentemente da ciò che il passaggio di backup dello script già catturi per te.</p>



<p class="wp-block-paragraph">È software libero. Puoi ridistribuirlo e modificarlo, con attribuzione a me come autore originale. Il codice sorgente di reset_DB_password.sh, e il resto della collezione ispconfig-fast-tools, è su GitHub su <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 rotazione delle credenziali database è un&#8217;abitudine, non un lavoro di pulizia</h2>



<p class="wp-block-paragraph">La rotazione delle credenziali database dopo una violazione confermata è semplicemente una risposta agli incidenti competente. La maggior parte degli amministratori lo farebbe senza bisogno di sentirlo due volte. L&#8217;abitudine più difficile è farlo dopo una violazione solo sospettata, prima che ci sia una prova. È l&#8217;unica versione del piano che limita davvero il danno, mentre è ancora limitabile. Uno script che esegue le fasi giuste per il ruolo di un server, fa prima i propri backup, e mi permette di simulare l&#8217;intera esecuzione prima di toccare qualcosa di reale. È quello che rende l&#8217;abitudine praticabile un martedì sera, invece che teorica in un articolo di blog.</p>



<p class="wp-block-paragraph">ISPConfig, MySQL, PHP, Dovecot, Pure-FTPd, Postfix e gli altri nomi citati sono marchi dei rispettivi proprietari. Sono usati solo per identificare i prodotti.</p><p>The post <a href="https://www.cisilino.com/2026/10/31/rotazione-credenziali-database-violazione-ispconfig/">Rotazione delle credenziali database dopo una violazione ISPConfig</a> first appeared on <a href="https://www.cisilino.com">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/2026/10/31/rotazione-credenziali-database-violazione-ispconfig/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Quale database occupa spazio sul disco? Lo script per MySQL e PostgreSQL</title>
		<link>https://www.cisilino.com/2026/10/20/script-spazio-disco-database-mysql/</link>
					<comments>https://www.cisilino.com/2026/10/20/script-spazio-disco-database-mysql/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 07:53:00 +0000</pubDate>
				<category><![CDATA[Server e database]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[script bash]]></category>
		<category><![CDATA[spazio disco]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=51</guid>

					<description><![CDATA[<p>Il disco del server si riempie e non sai quale database sia il responsabile? Uno script per misurare lo spazio di MySQL e PostgreSQL e confrontarlo nel tempo.</p>
<p>The post <a href="https://www.cisilino.com/2026/10/20/script-spazio-disco-database-mysql/">Quale database occupa spazio sul disco? Lo script per MySQL e PostgreSQL</a> first appeared on <a href="https://www.cisilino.com">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Capita a tutti prima o poi: lo spazio su disco del server finisce, l’allarme scatta. La domanda è sempre la stessa. <strong>Quale database sta occupando tutto questo spazio?</strong></p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/spazio-disco-database-mysql-postgresql.webp" alt="Grafico ad anello in quattro settori con tre barre di legenda: lo spazio su disco" width="1200" height="630"/></figure>



<p class="wp-block-paragraph">Io mi ci sono trovato di fronte dopo un’ottimizzazione. Avevo lanciato <code>OPTIMIZE TABLE</code> sperando di recuperare spazio, ma il filesystem non scendeva come previsto. Senza un “prima” da confrontare con il “dopo” era impossibile capire se l’ottimizzazione avesse funzionato e quale database fosse il vero responsabile. O se lo spazio mancante fosse addirittura da un’altra parte.</p>



<p class="wp-block-paragraph">Gli strumenti che avevo a disposizione rispondevano solo a metà del problema:</p>



<ul class="wp-block-list">
<li><code>du</code> mostra i file su disco, ma non dice a quale tabella appartengono.</li>



<li><code>information_schema</code> (o <code>pg_class</code> su PostgreSQL) dà le dimensioni logiche, ma non vede binlog, <code>ibdata1</code>, undo log o WAL.</li>



<li>entrambi fotografano un solo istante: il confronto nel tempo resta un lavoro manuale, fatto con copia e incolla e un foglio di calcolo.</li>
</ul>



<p class="wp-block-paragraph">Mi serviva una cosa semplice: <strong>fotografare lo stato di tutti i database in un dato momento, rifare la fotografia più tardi e farmi dire cosa è cambiato</strong>, ordinando le differenze dalla più grande alla più piccola. Così il colpevole salta subito all’occhio.</p>



<p class="wp-block-paragraph">Ne è uscito uno script Bash unico. Non ha dipendenze oltre al client del database e agli strumenti standard di Linux (<code>awk</code>, <code>du</code>, <code>df</code>). Funziona con <strong>MySQL/MariaDB</strong> e con <strong>PostgreSQL</strong>. Lo condivido qui nella speranza che faccia risparmiare a qualcuno lo stesso pomeriggio di frustrazione.</p>



<h2 class="wp-block-heading">Cosa fa</h2>



<p class="wp-block-paragraph">Lo script ha quattro comandi:</p>



<ul class="wp-block-list">
<li><code>snapshot</code> salva lo stato attuale (database, tabelle, file nella data directory, spazio del filesystem).</li>



<li><code>compare</code> confronta due fotografie e mostra le differenze, le più grandi in cima.</li>



<li><code>report</code> mostra i database e le tabelle più grandi di una singola fotografia.</li>



<li><code>list</code> elenca le fotografie salvate.</li>
</ul>



<p class="wp-block-paragraph">Il confronto è diviso in quattro sezioni, per non lasciare angoli bui:</p>



<ul class="wp-block-list">
<li><strong>dimensione di ogni database</strong> (dati + indici).</li>



<li><strong>spazio libero o recuperabile</strong> (<code>data_free</code> su MySQL, stima delle tuple morte su PostgreSQL).</li>



<li><strong>dimensione di ogni tabella</strong>.</li>



<li><strong>file reali nella data directory</strong>: cartelle dei database, <code>ibdata1</code>, binlog, undo, <code>pg_wal</code>.</li>
</ul>



<p class="wp-block-paragraph">L’ultima sezione è spesso quella che risolve il mistero. Molte volte lo spazio “anomalo” non appartiene a nessun database, ma ai log binari o ai file condivisi di InnoDB.</p>



<h2 class="wp-block-heading">Requisiti</h2>



<ul class="wp-block-list">
<li>Linux con Bash 4 o superiore, <code>awk</code>, <code>du</code>, <code>df</code> (tutto di serie).</li>



<li>il client <code>mysql</code> oppure <code>psql</code>, con un utente che possa leggere i metadati di tutti i database.</li>



<li>i permessi di root (o dell’utente <code>mysql</code>/<code>postgres</code>) se vuoi misurare anche i file su disco; senza, quella sezione viene saltata con un avviso.</li>
</ul>



<h2 class="wp-block-heading">Installazione</h2>



<p class="wp-block-paragraph">Salva lo script (lo trovi in fondo all’articolo) in un file e rendilo eseguibile.</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">Credenziali</h3>



<p class="wp-block-paragraph">Per MySQL/MariaDB il modo più pulito è un file <code>~/.my.cnf</code> con le credenziali, così non compaiono nella cronologia dei comandi.</p>



<pre class="wp-block-code"><code>&#91;client]
user=root
password=LA_TUA_PASSWORD</code></pre>



<p class="wp-block-paragraph">In alternativa puoi passare le opzioni con una variabile d’ambiente.</p>



<pre class="wp-block-code"><code>export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"</code></pre>



<p class="wp-block-paragraph">Per PostgreSQL funzionano <code>~/.pgpass</code>, le variabili <code>PG*</code> oppure <code>PG_OPTS</code>.</p>



<pre class="wp-block-code"><code>export PG_OPTS="-U postgres -h localhost"</code></pre>



<h2 class="wp-block-heading">Come si usa</h2>



<p class="wp-block-paragraph">Il flusso tipico è <em>prima, ottimizzazione, dopo, confronto</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 class="wp-block-paragraph">Per PostgreSQL basta aggiungere l’opzione <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 class="wp-block-paragraph">Le opzioni disponibili sono:</p>



<ul class="wp-block-list">
<li><code>-e mysql|postgres</code> sceglie il motore (predefinito: <code>mysql</code>).</li>



<li><code>-d DIR</code> cambia la cartella in cui salvare le fotografie (predefinita: <code>./db_snapshots</code>).</li>



<li><code>-n N</code> imposta quante righe mostrare per sezione (predefinito: 20).</li>
</ul>



<p class="wp-block-paragraph">Se non indichi un’etichetta, la fotografia prende come nome la data e l’ora. Questo è comodo per monitorare la crescita nel tempo.</p>



<h2 class="wp-block-heading">Esempio di output</h2>



<p class="wp-block-paragraph">Ecco un confronto su dati di prova, costruiti apposta per mostrare un caso tipico. Il database <code>shop</code> si riduce dopo l’ottimizzazione, ma il database <code>log</code> cresce e i binlog raddoppiano.</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 class="wp-block-paragraph">Qui il colpevole è evidente: la tabella <code>log.eventi</code> è cresciuta di oltre 6 GiB. I file <code>mysql-bin.*</code> sono passati da 3,7 a 8,4 GiB. L’ottimizzazione su <code>shop</code> ha funzionato, ma il guadagno è stato mangiato dal resto.</p>



<h2 class="wp-block-heading">Come leggere i risultati e ritrovare lo spazio su disco</h2>



<ul class="wp-block-list">
<li>In cima a ogni sezione ci sono le voci cambiate di più. Un database o una tabella che <strong>non cala</strong> (o addirittura cresce) dopo l’ottimizzazione è il primo sospetto.</li>



<li>Se le tabelle calano ma il filesystem no, lo spazio su disco è altrove. Guarda la sezione sui file nella data directory (binlog, <code>ibdata1</code>, undo, <code>pg_wal</code>).</li>



<li><strong>MySQL/MariaDB:</strong> <code>ibdata1</code> non si restringe mai, nemmeno con <code>OPTIMIZE TABLE</code>. Con <code>innodb_file_per_table=OFF</code> il valore di <code>data_free</code> è condiviso e poco indicativo.</li>



<li><strong>PostgreSQL:</strong> un <code>VACUUM</code> semplice non restituisce spazio al sistema operativo, serve <code>VACUUM FULL</code> (o <code>pg_repack</code>). Per capire come PostgreSQL misura le dimensioni, la <a href="https://www.postgresql.org/docs/current/diskusage.html" target="_blank" rel="noopener">documentazione ufficiale sull’uso del disco</a> spiega come controllare lo spazio di tabelle e indici. Lo spazio “recuperabile” mostrato è una stima basata sulle tuple morte.</li>
</ul>



<h2 class="wp-block-heading">Monitorare la crescita nel tempo</h2>



<p class="wp-block-paragraph">Se il problema non è legato a una singola ottimizzazione ma a una crescita graduale, conviene automatizzare le fotografie con cron, una all’ora, e poi confrontare due istanti qualsiasi.</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 class="wp-block-paragraph">Senza etichetta, ogni fotografia prende come nome la data e l’ora (per esempio <code>20261011-140000</code>). Con <code>list</code> vedi quelle disponibili e con <code>compare</code> ne confronti due. In pochi giorni emerge quale database cresce in modo anomalo.</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">Lo script completo</h2>



<p class="wp-block-paragraph">Ecco il codice. Copialo in un file chiamato <code>db_space_compare.sh</code>, rendilo eseguibile e sei pronto.</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">Note finali</h2>



<ul class="wp-block-list">
<li>Lo script è in <strong>sola lettura</strong> sui database: esegue solo <code>SELECT</code> sui metadati. Scrive esclusivamente nella cartella delle fotografie.</li>



<li>Su MySQL 8 imposta <code>information_schema_stats_expiry = 0</code> nella sessione. Altrimenti le statistiche in cache (fino a 24 ore) potrebbero mostrare i valori “vecchi” anche dopo l’ottimizzazione.</li>



<li>Se una fotografia fallisce a metà, viene rimossa automaticamente per non lasciare dati incompleti.</li>



<li>Se invece il problema è un database che non parte dopo un aggiornamento, leggi <a href="https://www.cisilino.com/2026/10/13/mysql-non-si-avvia-debian-12/">MySQL non si avvia dopo l’aggiornamento a Debian 12</a>.</li>
<li>Prima di usarlo in produzione, provalo su un ambiente di test e verifica i permessi dell’utente del database.</li>
</ul>



<p class="wp-block-paragraph">Se lo usi per tenere d’occhio lo spazio su disco e trovi un caso che non copre, o hai idee per estenderlo (altri motori, esportazione in CSV, avvisi via email), scrivimi nei commenti.</p><p>The post <a href="https://www.cisilino.com/2026/10/20/script-spazio-disco-database-mysql/">Quale database occupa spazio sul disco? Lo script per MySQL e PostgreSQL</a> first appeared on <a href="https://www.cisilino.com">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/2026/10/20/script-spazio-disco-database-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
