# wp_postmeta et un module de dons Stripe : les meta_query qui rampent

> Symptôme d'une page d'administration des dons qui met douze secondes à charger, diagnostic d'une meta_query sur une table non indexée, correctif pour une association aux milliers de donateurs.

- Auteur : WordPress Développement
- Publié le : 2023-08-31
- Mis à jour le : 2023-08-31
- Catégorie : Performance
- URL : https://www.wpmoderne.fr/performance/meta-query-lente-dons-stripe-postmeta/

## L’essentiel

- meta_key et meta_value ne sont indexés que partiellement dans wp_postmeta
- Une meta_query avec plusieurs clauses multiplie les jointures sur la même table
- Une table personnalisée indexée reste la solution la plus stable au-delà d'un certain volume

Douze secondes. C'est le temps qu'affichait systématiquement le chronomètre de Query Monitor sur la page d'administration listant les dons d'une association, dès que la responsable des adhésions tentait de filtrer par montant supérieur à un seuil ou par statut de reçu fiscal envoyé.

L'association gérait plusieurs milliers de donateurs, chaque don étant enregistré comme un article personnalisé (`don`) avec des métadonnées : montant, date, statut de synchronisation Stripe, statut d'envoi du reçu fiscal. La page d'administration listait ces dons via une `WP_Query` avec une `meta_query` combinant plusieurs critères.

## Symptôme : une page qui rampe dès qu'on filtre

Sans filtre actif, la liste des dons récents s'affichait normalement. Dès qu'un filtre combinait deux critères de métadonnées (montant et statut de reçu, par exemple), le temps de chargement s'effondrait littéralement, passant de moins d'une seconde à une douzaine de secondes, au point que certains navigateurs affichaient un avertissement de script long.

## Diagnostic : une meta_query à deux clauses sur une table géante

> L'essentiel à retenir : meta_key et meta_value ne sont indexés que partiellement dans wp_postmeta ; Une meta_query avec plusieurs clauses multiplie les jointures sur la même table ; Une table personnalisée indexée reste la solution la plus stable au-delà d'un certain volume

Query Monitor a révélé la requête SQL générée par la `meta_query`, avec deux jointures successives sur `wp_postmeta` :

```
SELECT wp_posts.* FROM wp_posts
INNER JOIN wp_postmeta AS mt1 ON (wp_posts.ID = mt1.post_id)
INNER JOIN wp_postmeta AS mt2 ON (wp_posts.ID = mt2.post_id)
WHERE wp_posts.post_type = 'don'
AND ( (mt1.meta_key = 'montant' AND mt1.meta_value+0 >= 100)
AND (mt2.meta_key = 'statut_recu' AND mt2.meta_value = 'envoye') )
```

Le cœur du problème tenait dans la clause `mt1.meta_value+0 >= 100` : l'opérateur `+0`, utilisé pour forcer une comparaison numérique sur une colonne `meta_value` stockée en `LONGTEXT`, empêche purement et simplement MySQL d'utiliser un quelconque index sur cette colonne. Chaque ligne de `wp_postmeta` correspondant à la clé `montant` devait être convertie et comparée individuellement, sur une table qui dépassait les 800 000 lignes toutes métadonnées de dons confondues.

La table `wp_postmeta` possède bien un index sur `meta_key`, ce qui permet de restreindre rapidement l'ensemble des lignes candidates à une clé donnée, mais aucun index n'existe nativement sur `meta_value`, et encore moins sur une conversion numérique de cette colonne à la volée.

## Le correctif : une table personnalisée indexée

Plutôt que de tenter d'optimiser la requête `meta_query` elle-même — dont les marges de manœuvre restent limitées face à la structure générique de `wp_postmeta` — le choix s'est porté sur une table dédiée aux dons, avec un typage natif et des index adaptés aux filtres réellement utilisés par l'association :

```
CREATE TABLE wp_dons_index (
    post_id BIGINT UNSIGNED PRIMARY KEY,
    montant DECIMAL(10,2) NOT NULL,
    statut_recu VARCHAR(20) NOT NULL,
    date_don DATETIME NOT NULL,
    INDEX idx_montant (montant),
    INDEX idx_statut (statut_recu)
);
```

Cette table est tenue à jour via un hook `save_post_don`, en miroir des métadonnées existantes, sans rien retirer côté `wp_postmeta` pour ne pas casser la compatibilité avec d'éventuelles extensions tierces qui liraient ces métadonnées directement. La page d'administration a ensuite été réécrite pour interroger cette table via `$wpdb` plutôt que passer par une `meta_query` :

```
$resultats = $wpdb->get_results( $wpdb->prepare(
    "SELECT post_id FROM {$wpdb->prefix}dons_index
     WHERE montant >= %f AND statut_recu = %s
     ORDER BY date_don DESC LIMIT 50",
    100, 'envoye'
) );
```

## Résultat mesuré

| Scénario | Avant correctif | Après correctif |
| --- | --- | --- |
| Filtre montant seul | 7,2 s | 0,3 s |
| Filtre montant + statut reçu | 12 s | 0,4 s |

## Prévention pour les associations à fort volume

- Éviter les comparaisons numériques ou les tris sur `meta_value` dès que le volume de métadonnées dépasse quelques dizaines de milliers de lignes pour une même clé.
- Envisager une table miroir indexée dès qu'une page d'administration doit filtrer régulièrement sur un critère numérique ou une plage de dates.
- Garder `wp_postmeta` comme source de vérité pour la compatibilité, la table miroir n'étant qu'un index de lecture optimisé, jamais la source de vérité elle-même.

> Une meta_query à deux clauses numériques sur une table de plusieurs centaines de milliers de lignes n'est jamais un problème de configuration : c'est un problème de modèle de données qu'aucun réglage MySQL ne résoudra durablement.

## En résumé

Ce dossier ne traite volontairement pas la façon dont Stripe est intégré côté paiement : le problème restait entièrement circonscrit à la couche de lecture et de filtrage des dons dans l'administration WordPress. La leçon la plus généralisable reste que `wp_postmeta`, conçu pour la flexibilité, atteint vite ses limites de performance dès qu'on lui demande de se comporter comme une table relationnelle typée et indexée.
