WPML lento? Il backend WordPress che non risponde? Probabilmente è il database.

Nel mio caso, ogni click nel pannello admin richiedeva 15-20 secondi. Il database aveva raggiunto i 2.5GB per un sito con “solo” 18.000 articoli. Il colpevole: 550.000 stringhe Gutenberg registrate da WPML nella tabella wp_icl_strings, stringhe che non avrebbero mai dovuto esistere.

Quello che segue è il percorso completo: diagnosi SQL, pulizia del database, cronjob di prevenzione automatica e tuning MariaDB. Tutto testato su un sito in produzione.

Il setup del sito WordPress multilingua

Il cliente era uno studio legale con un portale legislativo. Circa 18.000 articoli (custom post type “legislazione”) tradotti in 4 lingue con WPML.

L’infrastruttura: Ubuntu 24.04 su VPS Keliweb (8GB RAM, 6 CPU, 200GB SSD), gestita con 1Panel. Stack completo: MariaDB 11.8.5, WordPress 6.9, Redis 8.4 per object cache, OpenResty come web server, CrowdSec + Cloudflare bouncer per la sicurezza.

La stessa VPS ha richiesto diversi interventi infrastrutturali nel tempo: limite memory per il bouncer CrowdSec (che leakava fino a saturare la RAM), network watchdog systemd per un problema di networking al reboot, ottimizzazione Redis (256MB, allkeys-lru), aumento swap a 3GB, OPcache a 256MB. Tutto questo per dire che l’infrastruttura era già ottimizzata. Il problema era nel database.

Il problema: oltre 1,5 GB di tabelle WPML

Ho iniziato con una diagnosi delle dimensioni delle tabelle:

-- Dimensioni tabelle WPML - il primo segnale d'allarme
SELECT
    table_name AS 'Tabella',
    ROUND(data_length / 1024 / 1024, 2) AS 'Dati (MB)',
    ROUND(index_length / 1024 / 1024, 2) AS 'Indici (MB)',
    ROUND((data_length + index_length) / 1024 / 1024, 2) AS 'Totale (MB)',
    table_rows AS 'Righe stimate'
FROM information_schema.tables
WHERE table_schema = DATABASE()
    AND table_name LIKE 'wp_icl_%'
ORDER BY (data_length + index_length) DESC;

I risultati:

TabellaDimensioneRighe
wp_icl_translate960 MB819.000
wp_icl_translation_status580 MB-
wp_icl_stringsgrande550.000+

Il database totale era 2.5GB, di cui oltre 1.5GB erano solo tabelle WPML.

Il pannello WPML stesso segnalava 552.636 stringhe “non utilizzate o con dati errati”. Ma il bottone di pulizia automatica? Non funzionava. Timeout dopo timeout.

Prima di passare al SQL vale la pena provare gli strumenti di WPML, che sono la strada più prudente. Le note di rilascio di WPML Troubleshooting 5.1.0 descrivono il comportamento della pulizia nativa:

Removing strings by domain and “Cleanup and optimize string tables” keep the strings that are waiting for a translation unless you ask to remove them too.

Nel mio caso quella strada andava in timeout, e con 550.000 righe è comprensibile. Le query che seguono sono l’alternativa manuale, con un perimetro stretto: toccano solo le stringhe il cui post non esiste più.

Perché WPML registra stringhe Gutenberg ridondanti

Per capire il problema serve sapere come WPML gestisce le traduzioni. Ci sono due metodi principali.

Metodo “Duplica post” (quello che usava il sito): WPML crea una copia del post per ogni lingua. Il traduttore lavora direttamente sul post duplicato, come se fosse un articolo indipendente. I contenuti tradotti vivono nella tabella wp_posts, come qualsiasi altro post WordPress.

Metodo “String Translation”: WPML estrae ogni blocco di testo dal post (titoli, paragrafi, bottoni) e lo registra come “stringa” nella tabella wp_icl_strings. Il traduttore traduce le singole stringhe da un’interfaccia dedicata, e le traduzioni vanno in wp_icl_string_translations.

Il primo metodo non ha bisogno della tabella stringhe: i contenuti sono già nei post duplicati. Il secondo sì, perché le stringhe SONO il meccanismo di traduzione.

Il bug: anche con il metodo “duplica post” attivo, WPML registrava comunque tutte le stringhe dei blocchi Gutenberg in wp_icl_strings. Doppia registrazione: i contenuti erano sia nei post duplicati (dove servivano) sia nelle stringhe (dove non servivano a nessuno). Ogni blocco Gutenberg, ogni paragrafo, ogni heading di ogni articolo, moltiplicato per 18.000 post.

Per capire la portata:

-- Conta stringhe per tipo di contesto
-- Il campo "context" in wp_icl_strings indica da dove arriva la stringa.
-- WPML usa il pattern "gutenberg-{post_id}" per le stringhe dei blocchi editor.
SELECT
    CASE
        WHEN context LIKE 'gutenberg-%' THEN 'Gutenberg blocks'
        WHEN context LIKE 'theme%' THEN 'Theme strings'
        WHEN context LIKE 'plugin%' THEN 'Plugin strings'
        ELSE context
    END AS tipo,
    COUNT(*) AS totale
FROM wp_icl_strings
GROUP BY tipo
ORDER BY totale DESC
LIMIT 10;

Oltre il 90% delle stringhe aveva contesto gutenberg-{post_id}. Ogni blocco Gutenberg di ogni articolo generava stringhe ridondanti.

Pulizia database WPML: guida step by step

Step 1. Backup

Prima di toccare qualsiasi cosa, backup completo:

#!/bin/bash
# backup-db.sh - Dump completo prima della pulizia WPML

BACKUP_DIR="/root/backups/wpml-cleanup"
DB_NAME="wordpress_db"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

# Dump completo del database
# --single-transaction: fa il dump senza bloccare le tabelle (usa uno snapshot InnoDB)
# --routines: include stored procedure e function
# --triggers: include i trigger associati alle tabelle
mysqldump --single-transaction --routines --triggers \
    "$DB_NAME" > "$BACKUP_DIR/full_dump_${DATE}.sql"

# Dump separato delle sole tabelle WPML (per restore parziale)
# Se qualcosa va storto, puoi ripristinare solo queste senza toccare il resto
mysqldump --single-transaction "$DB_NAME" \
    wp_icl_strings \
    wp_icl_string_translations \
    wp_icl_translate \
    wp_icl_translation_status \
    > "$BACKUP_DIR/wpml_tables_${DATE}.sql"

# Comprimi (un dump da 2.5GB non compresso occupa spazio inutile)
gzip "$BACKUP_DIR/full_dump_${DATE}.sql"
gzip "$BACKUP_DIR/wpml_tables_${DATE}.sql"

echo "Backup completato: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"

Step 2. Identificare le stringhe orfane

Prima di cancellare, verifica esattamente cosa stai per rimuovere:

-- Conta le stringhe Gutenberg che puntano a post NON esistenti
--
-- Come funziona la query:
-- 1. context LIKE 'gutenberg-%' filtra solo le stringhe dei blocchi editor
-- 2. SUBSTRING(context, 11) estrae tutto dopo "gutenberg-" (10 caratteri),
--    cioè il post_id. Esempio: "gutenberg-12345" diventa "12345"
-- 3. CAST(... AS UNSIGNED) converte la stringa "12345" in numero intero,
--    necessario per confrontarlo con wp_posts.ID che è un intero
-- 4. NOT IN (SELECT ID FROM wp_posts) verifica se quel post esiste ancora
--
-- Se il post è stato cancellato, le sue stringhe sono orfane: occupano spazio
-- e non servono più a nessuno.

SELECT COUNT(*) AS stringhe_orfane
FROM wp_icl_strings
WHERE context LIKE 'gutenberg-%'
AND CAST(SUBSTRING(context, 11) AS UNSIGNED)
    NOT IN (SELECT ID FROM wp_posts);

-- Per confronto: conta le stringhe Gutenberg con post ancora esistente
-- (queste le teniamo, anche se ridondanti con il metodo "duplica post")
SELECT COUNT(*) AS stringhe_con_post
FROM wp_icl_strings
WHERE context LIKE 'gutenberg-%'
AND CAST(SUBSTRING(context, 11) AS UNSIGNED)
    IN (SELECT ID FROM wp_posts);

Nel nostro caso, circa 350.000 stringhe erano completamente orfane, puntavano a post che non esistevano più (cancellati, draft rimossi, revisioni eliminate).

Step 3. Pulizia chirurgica

Due query, in quest’ordine. L’ordine conta: le traduzioni nella tabella wp_icl_string_translations hanno un campo string_id che punta alla riga corrispondente in wp_icl_strings. Se cancelli prima le stringhe, le traduzioni restano orfane (nessun vincolo foreign key le rimuove automaticamente, WPML non usa foreign key). Quindi: prima le traduzioni, poi le stringhe.

-- 1. Elimina le TRADUZIONI delle stringhe Gutenberg orfane
-- La sintassi "DELETE st FROM ... INNER JOIN ..." è specifica di MySQL/MariaDB.
-- Funziona così: il JOIN collega ogni traduzione alla sua stringa madre,
-- il WHERE filtra solo le stringhe orfane, e "DELETE st" cancella solo
-- le righe dalla tabella delle traduzioni (non dalle stringhe).
DELETE st FROM wp_icl_string_translations st
INNER JOIN wp_icl_strings s ON st.string_id = s.id
WHERE s.context LIKE 'gutenberg-%'
AND CAST(SUBSTRING(s.context, 11) AS UNSIGNED)
    NOT IN (SELECT ID FROM wp_posts);

-- 2. Ora elimina le STRINGHE Gutenberg orfane
-- Questa è una DELETE semplice: cancella dalla tabella principale
-- le righe il cui contesto punta a post non più esistenti.
DELETE FROM wp_icl_strings
WHERE context LIKE 'gutenberg-%'
AND CAST(SUBSTRING(context, 11) AS UNSIGNED)
    NOT IN (SELECT ID FROM wp_posts);

Attenzione: su database grandi queste query possono impiegare diversi minuti. Se hai più di 500k righe da cancellare, valuta di procedere a batch. Una nota sulla versione: il DELETE su più tabelle con LIMIT è accettato da MariaDB 11.8.1 in poi, come dice la documentazione di DELETE:

the multiple-table syntax also accepts ORDER BY and LIMIT

Su una versione precedente il primo DELETE dà errore di sintassi, e uno script che non controlla l’esito gira all’infinito, perché il confronto numerico con una stringa vuota non fa mai scattare l’uscita dal ciclo. Per questo lo script qui sotto si ferma se la risposta non è un numero. Lo script bash che automatizza i batch:

#!/bin/bash
# wpml-batch-delete.sh - Cancellazione a batch per database grandi
# Cancella 50.000 righe alla volta per evitare lock prolungati
# e permettere al server di respirare tra un batch e l'altro.

DB_NAME="wordpress_db"
BATCH=50000

echo "=== Pulizia traduzioni stringhe orfane ==="
while true; do
    AFFECTED=$(mysql -N -e "
        DELETE st FROM ${DB_NAME}.wp_icl_string_translations st
        INNER JOIN ${DB_NAME}.wp_icl_strings s ON st.string_id = s.id
        WHERE s.context LIKE 'gutenberg-%'
        AND CAST(SUBSTRING(s.context, 11) AS UNSIGNED)
            NOT IN (SELECT ID FROM ${DB_NAME}.wp_posts)
        LIMIT ${BATCH};
        SELECT ROW_COUNT();
    ")
    # Se la DELETE fallisce (es. MariaDB < 11.8.1) AFFECTED non è un numero: meglio fermarsi che girare a vuoto
    case "$AFFECTED" in ''|*[!0-9]*) echo "Errore: risposta non numerica, interrotto"; exit 1;; esac
    echo "  Traduzioni rimosse in questo batch: $AFFECTED"
    # Se il batch ha cancellato meno righe del limite, abbiamo finito
    [ "$AFFECTED" -lt "$BATCH" ] && break
    # Pausa di 2 secondi tra i batch per non saturare I/O
    sleep 2
done

echo "=== Pulizia stringhe orfane ==="
while true; do
    AFFECTED=$(mysql -N -e "
        DELETE FROM ${DB_NAME}.wp_icl_strings
        WHERE context LIKE 'gutenberg-%'
        AND CAST(SUBSTRING(context, 11) AS UNSIGNED)
            NOT IN (SELECT ID FROM ${DB_NAME}.wp_posts)
        LIMIT ${BATCH};
        SELECT ROW_COUNT();
    ")
    case "$AFFECTED" in ''|*[!0-9]*) echo "Errore: risposta non numerica, interrotto"; exit 1;; esac
    echo "  Stringhe rimosse in questo batch: $AFFECTED"
    [ "$AFFECTED" -lt "$BATCH" ] && break
    sleep 2
done

echo "=== Completato ==="

Step 4. Ottimizzazione tabelle

Dopo una DELETE massiva, MariaDB non libera automaticamente lo spazio su disco. I dati sono stati cancellati logicamente (le righe non esistono più), ma il file fisico della tabella mantiene la stessa dimensione, pieno di “buchi” dove c’erano le righe eliminate. OPTIMIZE TABLE ricostruisce la tabella da zero, compattando i dati e riscrivendo gli indici.

-- OPTIMIZE crea una copia temporanea della tabella, la riempie
-- con i dati compattati, poi sostituisce l'originale. Su InnoDB la documentazione
-- MariaDB lo tratta come DDL online: permette DML concorrenti e blocca la tabella
-- in modo esclusivo solo per un breve periodo all'inizio e alla fine.
-- Il carico di I/O si sente comunque: su una tabella da 960MB può impiegare 5-15 minuti.
-- Eseguilo in un orario di basso traffico (notte, weekend).

OPTIMIZE TABLE wp_icl_strings;
OPTIMIZE TABLE wp_icl_string_translations;
OPTIMIZE TABLE wp_icl_translate;
OPTIMIZE TABLE wp_icl_translation_status;

Step 5. Cronjob settimanale

Il problema si ripresenta. Ogni nuovo articolo genera nuove stringhe ridondanti. Ho creato uno script di pulizia automatica:

#!/bin/bash
# /usr/local/bin/wpml-cleanup.sh
# Pulizia settimanale stringhe Gutenberg orfane + OPTIMIZE
# Eseguito via cron ogni domenica alle 05:00

LOG="/var/log/wpml-cleanup.log"
DB_NAME="wordpress_db"
DATE=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$DATE] Inizio pulizia WPML" >> "$LOG"

# Conta orfane prima della pulizia
ORPHANS=$(mysql -N -e "
    SELECT COUNT(*) FROM ${DB_NAME}.wp_icl_strings
    WHERE context LIKE 'gutenberg-%'
    AND CAST(SUBSTRING(context, 11) AS UNSIGNED)
        NOT IN (SELECT ID FROM ${DB_NAME}.wp_posts);
")
echo "[$DATE] Stringhe orfane trovate: $ORPHANS" >> "$LOG"

if [ "$ORPHANS" -gt 0 ]; then
    # Pulizia traduzioni
    mysql -e "
        DELETE st FROM ${DB_NAME}.wp_icl_string_translations st
        INNER JOIN ${DB_NAME}.wp_icl_strings s ON st.string_id = s.id
        WHERE s.context LIKE 'gutenberg-%'
        AND CAST(SUBSTRING(s.context, 11) AS UNSIGNED)
            NOT IN (SELECT ID FROM ${DB_NAME}.wp_posts);
    "

    # Pulizia stringhe
    mysql -e "
        DELETE FROM ${DB_NAME}.wp_icl_strings
        WHERE context LIKE 'gutenberg-%'
        AND CAST(SUBSTRING(context, 11) AS UNSIGNED)
            NOT IN (SELECT ID FROM ${DB_NAME}.wp_posts);
    "

    # Optimize
    mysql -e "
        OPTIMIZE TABLE ${DB_NAME}.wp_icl_strings;
        OPTIMIZE TABLE ${DB_NAME}.wp_icl_string_translations;
    "

    echo "[$DATE] Pulizia completata. Rimossi $ORPHANS orfani." >> "$LOG"
else
    echo "[$DATE] Nessuna stringa orfana trovata." >> "$LOG"
fi

Per programmarlo, aggiungi una riga al crontab del server:

# Rendi eseguibile lo script
chmod +x /usr/local/bin/wpml-cleanup.sh

# Apri il crontab dell'utente root
crontab -e

# Aggiungi questa riga:
# ┌───── minuto (0)
# │ ┌───── ora (5 = le 5 di mattina)
# │ │ ┌───── giorno del mese (* = tutti)
# │ │ │ ┌───── mese (* = tutti)
# │ │ │ │ ┌───── giorno della settimana (0 = domenica)
# │ │ │ │ │
  0 5 * * 0 /usr/local/bin/wpml-cleanup.sh

Step 6. Prevenzione

Nelle impostazioni WPML, disattiva quello che non serve. I nomi esatti dei menu cambiano tra le versioni di WPML, e il sito su cui ho fatto questo intervento è stato nel frattempo migrato, quindi non posso riverificarli: cerca le voci equivalenti.

  1. Registrazione automatica delle stringhe (nelle impostazioni di String Translation): disattivala se non la usi.
  2. Traduzione dei media: disattivala se non traduci gli alt text delle immagini.
  3. Metodo di traduzione: verifica che sia solo “Duplica post”, senza String Translation attivo in parallelo.

Step 7. Tuning MariaDB

Con un database di queste dimensioni, i default di MariaDB non bastano. Ecco i parametri che ho modificato e perché. Per il buffer pool la guida MariaDB sull’allocazione della memoria dice:

Set innodb_buffer_pool_size to about 60% to 70% of available RAM.

Per il caso “Mostly InnoDB”, dove la RAM disponibile è quella che resta dopo sistema operativo e altri servizi. Io sono rimasto più basso per i motivi nel commento qui sotto.

# /etc/mysql/mariadb.conf.d/99-custom.cnf
# Crea questo file (il "99-" garantisce che venga caricato per ultimo
# e sovrascriva i default)

[mysqld]
# Buffer pool: è la cache in RAM dove MariaDB tiene le pagine di dati
# e indici più usate. Più è grande, meno deve leggere da disco.
# La guida MariaDB sull'allocazione della memoria indica circa il 60-70%
# della RAM disponibile (quella che resta dopo OS e altri servizi) per un
# server che usa soprattutto InnoDB.
# Qui ho scelto 2GB su 8GB, volutamente sotto quell'intervallo: la VPS
# ospita anche Redis, OpenResty e PHP, e dopo la pulizia il database
# pesa circa 900MB, quindi ci sta tutto in memoria.
innodb_buffer_pool_size = 2G

# Log file: MariaDB scrive qui le modifiche prima di applicarle
# alle tabelle (write-ahead log). Un file più grande riduce la
# frequenza di flush su disco, utile durante operazioni massive
# come le nostre DELETE da 350k righe.
innodb_log_file_size = 512M

# Join buffer: memoria allocata per ogni JOIN che non usa un indice.
# Le query WPML con JOIN tra stringhe e traduzioni ne beneficiano.
# Default: 256KB, troppo poco per JOIN su tabelle da 500k+ righe.
join_buffer_size = 8M

# Query cache: in teoria memorizza i risultati delle SELECT ripetute.
# In pratica, con WordPress (che fa molte scritture) + Redis (che già
# fa da cache a livello applicativo), la query cache di MariaDB
# diventa un collo di bottiglia. Meglio disabilitarla.
query_cache_type = 0
query_cache_size = 0

# Temp table: dimensione massima delle tabelle temporanee in RAM.
# Le query con GROUP BY (come la nostra diagnostica per contesto)
# creano tabelle temporanee. Se superano questo limite, MariaDB
# le scrive su disco, molto più lento.
tmp_table_size = 64M
max_heap_table_size = 64M
# Restart MariaDB dopo le modifiche
systemctl restart mariadb

# Verifica che i parametri siano stati applicati
mysql -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"

Risultati: da 2.5GB a 900MB

MetricaPrimaDopo
Database totale2.5 GB~900 MB
wp_icl_strings550.000 righe~180.000 righe
wp_icl_translate960 MB~350 MB
Tempo caricamento admin15-20s2-3s
Crescita settimanale DB~50 MB~5 MB (con cronjob)

Il backend WordPress è tornato utilizzabile. Non veloce come vorremmo (è pur sempre un WP con 18.000 articoli), ma gestibile.

Proiezione di scalabilità

Questa è un’estrapolazione lineare da un solo caso, non una misura. Dal portale del cliente: 550.000 stringhe e 2,5 GB per circa 18.000 articoli in 4 lingue, cioè circa 31.000 stringhe e 140 MB ogni 1.000 articoli, prima della pulizia.

Articoli (come nel caso, 4 lingue)Stringhe stimateDimensione DB stimata
1.000~31k~140 MB
5.000~150k~700 MB
10.000~310k~1,4 GB
20.000~610k~2,8 GB

La crescita è lineare, ma a queste dimensioni non è la scala in sé a fare il danno. Nel mio caso 350.000 stringhe su 550.000 erano orfane e il resto era in gran parte registrazione ridondante: è quello che ha trasformato un database gestibile in uno da 2,5 GB.

Limiti di WPML e alternative per siti multilingua grandi

Il collo di bottiglia non è il numero di articoli, è il modo in cui WPML registra le stringhe. Con le stringhe ridondanti e gli orfani il mio database è arrivato a 2,5 GB e l’admin a 15-20 secondi; ripulito, lo stesso sito sta in circa 900 MB. Se parti da zero con 10.000+ contenuti tradotti in 3+ lingue, valuta comunque le alternative fin dall’inizio: MultilingualPress (un’installazione WordPress per lingua, multisite), Polylang (più leggero di WPML per setup semplici), Weglot (SaaS, le traduzioni stanno fuori dal tuo database), o un approccio headless con i18n nativo del framework frontend. Non le ho provate sullo stesso portale, quindi questa è una lista di strade da valutare e non un confronto misurato.

Le query di pulizia funzionano solo per il pattern gutenberg-{post_id}. Se WPML ha registrato stringhe con altri contesti (theme, plugin, custom), vanno gestite separatamente. Verifica sempre con la query di diagnosi per contesto prima di cancellare.

Il cronjob è un cerotto, non una soluzione. La vera soluzione sarebbe che WPML non registrasse stringhe ridondanti quando il metodo di traduzione è “duplica post”. Finché non fixano questo comportamento, il cronjob è necessario.

Se stai gestendo un caso simile e ti chiedi perché per il mio blog personale ho scelto uno stack completamente diverso, ne parlo in Astro, Ghost o WordPress per un blog tecnico? Come (e perché) ho scelto Markdown. Spoiler: niente WordPress, niente database, niente problemi.

Domande frequenti

Perché WPML rende lento il backend di WordPress?

Nel caso descritto qui, un portale con circa 18.000 articoli in 4 lingue, il database era arrivato a 2,5 GB e oltre 1,5 GB erano tabelle WPML: wp_icl_translate da 960 MB e wp_icl_translation_status da 580 MB, più 550.000 righe in wp_icl_strings. Ogni click nel pannello admin richiedeva 15-20 secondi. Dopo la pulizia il database è sceso a circa 900 MB e l'admin a 2-3 secondi. È un caso solo: la misura da fare sul tuo sito parte dalla query di diagnosi delle dimensioni delle tabelle.

Posso cancellare le righe di wp_icl_strings con una query SQL?

Solo quelle che puntano a un post che non esiste più, e solo dopo un backup completo. Le query di questo articolo cancellano le stringhe con contesto gutenberg-{post_id} il cui post non c'è più in wp_posts, e prima le traduzioni e poi le stringhe, perché WPML non usa foreign key. Prima però prova gli strumenti di WPML: nella pagina Troubleshooting c'è “Cleanup and optimize string tables”, che dalla versione 5.1.0 tiene le stringhe in attesa di traduzione a meno che tu non chieda di rimuoverle.

Lo script di cancellazione a batch funziona su ogni versione di MariaDB?

No. Il DELETE su più tabelle con LIMIT è accettato da MariaDB 11.8.1 in poi, come indica la documentazione di DELETE. Su una versione precedente il primo DELETE dà errore di sintassi: lo script riportato nell'articolo controlla che la risposta sia un numero e si ferma, invece di girare a vuoto.

OPTIMIZE TABLE blocca il sito mentre lavora?

Per InnoDB la documentazione MariaDB sul DDL online dice che permette DML concorrenti e che la tabella viene bloccata in modo esclusivo solo per un breve periodo all'inizio e alla fine. Serve comunque spazio su disco per la copia e il carico di I/O si sente, quindi conviene eseguirlo in un orario di basso traffico e controllare il comportamento sulla tua versione.

Quanto deve essere grande innodb_buffer_pool_size?

La guida MariaDB sull'allocazione della memoria indica circa il 60-70% della RAM disponibile quando il server usa soprattutto InnoDB, dove RAM disponibile significa quella che resta dopo sistema operativo e altri servizi. Nel caso di questo articolo ho scelto 2 GB su 8 GB, volutamente sotto quell'intervallo, perché la VPS ospita anche Redis, OpenResty e PHP e, dopo la pulizia, il database era di circa 900 MB.