Le WordPress d'aujourd'hui, décodé pour les développeurs

Performance

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.

Par WordPress Développement • 31 août 2023 • 5 min de lecture • Aucun commentaire
wp_postmeta et un module de dons Stripe : les meta_query qui rampent

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énarioAvant correctifAprès correctif
Filtre montant seul7,2 s0,3 s
Filtre montant + statut reçu12 s0,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.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Partager :

À propos de l'auteur

WordPress Développement

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi