# 200 000 lignes de postmeta scannées par une route REST faute d’un index

> Une route REST personnalisée ralentit progressivement à mesure que le contenu grandit. En cause, une meta_query qui force un scan complet de la table postmeta.

- Auteur : WordPress Développement
- Publié le : 2023-01-04
- Mis à jour le : 2023-01-04
- Catégorie : Headless &amp; API
- URL : https://www.wpmoderne.fr/headless/postmeta-route-rest-index-manquant/

## L’essentiel

- Une meta_query sans index adapté force un scan de table complet
- Un index composite sur meta_key et meta_value réduit le temps de réponse
- EXPLAIN permet de vérifier qu'une requête utilise bien l'index créé

200 000 lignes lues, 4 lignes retournées : c'est ce que révèle une analyse `EXPLAIN` lancée sur la requête générée par une route REST maison qui filtre des fiches produits selon un statut personnalisé. La route fonctionnait sans accroc à ses débuts, quand le catalogue comptait quelques centaines de références. Un an plus tard, avec plusieurs dizaines de milliers de fiches, chaque appel prend plus d'une seconde, et le temps continue de grimper avec le volume de contenu.

Ce scénario est classique sur les projets headless qui exposent un filtrage par métadonnée personnalisée : la route fonctionne, les résultats sont corrects, et rien dans les tests fonctionnels ne signale de problème. Seule la charge réelle en production, avec un volume de données représentatif, révèle le défaut de conception.

## Le diagnostic : une meta_query sans filtre efficace

La route en question s'appuie sur `WP_Query` avec un argument `meta_query` portant sur une clé personnalisée, par exemple `statut_stock` :

```
$query = new WP_Query( array(
    'post_type'  => 'produit',
    'meta_query' => array(
        array(
            'key'     => 'statut_stock',
            'value'   => 'disponible',
            'compare' => '=',
        ),
    ),
    'posts_per_page' => 20,
) );
```

Sur le papier, rien d'anormal. Le problème vient de la structure même de la table `wp_postmeta` : elle stocke toutes les métadonnées de tous les types de contenus, sous forme clé-valeur, avec un index par défaut qui ne couvre efficacement que la colonne `meta_key` seule. Dès que le volume de lignes dépasse plusieurs dizaines de milliers, MySQL doit lire un nombre croissant de lignes correspondant à la clé avant de filtrer sur la valeur.

### Lire un plan d'exécution avec EXPLAIN

La commande `EXPLAIN`, exécutée directement sur la requête générée, confirme le diagnostic :

```
EXPLAIN SELECT wp_posts.ID FROM wp_posts
INNER JOIN wp_postmeta ON wp_posts.ID = wp_postmeta.post_id
WHERE wp_postmeta.meta_key = 'statut_stock'
AND wp_postmeta.meta_value = 'disponible';
```

> L'essentiel à retenir : Une meta_query sans index adapté force un scan de table complet ; Un index composite sur meta_key et meta_value réduit le temps de réponse ; EXPLAIN permet de vérifier qu'une requête utilise bien l'index créé

La colonne `rows` du résultat affiche un nombre proche du total de lignes de la table `postmeta`, signe que l'index utilisé ne réduit pas suffisamment l'espace de recherche. La colonne `Extra` mentionne souvent `Using where`, confirmant qu'un filtrage supplémentaire s'opère après la lecture, ligne par ligne.

## Le correctif : un index composite ciblé

La solution consiste à créer un index couvrant à la fois `meta_key` et `meta_value`, directement sur la table concernée :

```
ALTER TABLE wp_postmeta
ADD INDEX idx_statut_stock (meta_key, meta_value(20));
```

Le préfixe de longueur sur `meta_value` reste nécessaire car cette colonne est de type `longtext`, que MySQL ne peut indexer intégralement. Une longueur de préfixe de 20 caractères suffit largement pour une valeur courte comme un statut, mais doit être ajustée selon la nature réelle des données stockées dans cette clé.

Après création de l'index, le même `EXPLAIN` affiche un nombre de lignes lues proche du nombre de résultats réellement retournés, et le temps de réponse de la route redescend à quelques dizaines de millisecondes.

## Une alternative plus radicale : sortir la donnée de postmeta

Pour les métadonnées interrogées très fréquemment, l'ajout d'un index reste une rustine efficace mais pas toujours suffisante à long terme. Plusieurs options structurelles existent :

- Stocker la valeur dans une taxonomie personnalisée plutôt qu'un champ personnalisé, ce qui bénéficie de l'indexation native de `wp_term_relationships`.
- Créer une table dédiée via `dbDelta()` pour les données interrogées en masse et à fort volume.
- Ajouter une couche de cache applicatif, avec `wp_cache_get()` et `wp_cache_set()`, en amont de la requête coûteuse, quand la fraîcheur absolue n'est pas critique.

## Prévenir la récidive

Un index bien pensé aujourd'hui ne garantit rien pour un futur champ personnalisé ajouté sans vigilance équivalente. Intégrer une revue systématique des plans d'exécution des routes REST maison avant leur mise en production, dès qu'elles s'appuient sur une `meta_query`, évite de reproduire le même scénario sur la prochaine fonctionnalité.

> Une route REST qui répond vite avec 500 fiches en environnement de recette ne dit rien de son comportement avec 50 000 fiches en production. Le volume de données de test doit refléter l'ordre de grandeur réel, sans quoi le problème n'apparaît jamais avant la mise en ligne.

## Pour aller plus loin

La documentation officielle de `WP_Meta_Query` détaille les arguments disponibles pour affiner un filtrage sur les métadonnées, mais elle ne remplace pas une vérification du plan d'exécution SQL sous-jacent. Sur un projet headless amené à grandir, cette vérification mérite d'entrer dans la liste des contrôles systématiques avant chaque mise en production d'une nouvelle route.
