Huit cent quarante fiches de biens, chacune avec une dizaine de champs personnalisés — surface, nombre de pièces, prix, ville, type de mandat — et un formulaire de recherche permettant de combiner plusieurs de ces critères à la fois. Sur le papier, ce volume reste modeste. Dans les faits, le temps de génération d’une page de résultats filtrés dépassait fréquemment la seconde et demie, avec des pics au-delà de trois secondes lors des consultations simultanées.
Le premier réflexe, face à ce type de ralentissement, consiste souvent à conclure que le nombre de fiches dépasse ce qu’un hébergement mutualisé peut raisonnablement absorber. L’analyse des requêtes générées a montré une réalité différente : le volume de données n’était pas en cause, la façon de les interroger l’était.
Ce que révèle l’analyse des requêtes lentes
L’activation du journal des requêtes lentes de MySQL, disponible même sur ce mutualisé via le tableau de bord de l’hébergeur, a mis en évidence des requêtes combinant plusieurs conditions meta_query sur des champs personnalisés non indexés, générées par le moteur de recherche du thème utilisé pour le catalogue.
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
INNER JOIN wp_postmeta AS mt3 ON wp_posts.ID = mt3.post_id
WHERE mt1.meta_key = 'prix' AND mt1.meta_value BETWEEN 150000 AND 300000
AND mt2.meta_key = 'ville' AND mt2.meta_value = 'Angers'
AND mt3.meta_key = 'nb_pieces' AND mt3.meta_value >= 3
Chaque critère de recherche supplémentaire ajoutait une nouvelle jointure sur la table wp_postmeta, une table par nature non optimisée pour ce type de filtrage combiné, puisqu’elle stocke toutes les métadonnées de tous les contenus dans une structure clé-valeur générique.

Pourquoi meta_query devient coûteux à cette échelle
Ce comportement n’est pas propre à cet hébergement : il s’agit d’une limite bien connue de la structure relationnelle de WordPress, où les champs personnalisés stockés via add_post_meta ne bénéficient pas d’index adaptés à des recherches combinées par plage de valeurs. Sur quelques dizaines de fiches, l’effet reste imperceptible. Sur plusieurs centaines, avec des filtres combinés, chaque jointure supplémentaire multiplie le coût de la requête.
Les corrections apportées, par ordre d’impact
- Création d’une table personnalisée dédiée aux critères de recherche fréquents (prix, ville, nombre de pièces), avec des index adaptés, alimentée à chaque enregistrement d’une fiche via le hook
save_post - Réécriture du formulaire de recherche pour interroger cette table directement via
$wpdb, plutôt que de passer par uneWP_Queryavecmeta_queryimbriquée - Ajout d’un cache de résultats pour les combinaisons de filtres les plus fréquentes, avec une durée de vie courte compte tenu de la mise à jour régulière du catalogue
Le résultat mesuré après correction
| Indicateur | Avant correction | Après correction |
|---|---|---|
| Temps moyen de génération | 1,8 s | 0,3 s |
| Requêtes MySQL par recherche filtrée | 4 à 6 jointures | 1 requête indexée |
| Charge PHP-FPM en heure de pointe | Pool régulièrement saturé | Marge confortable |
Sur un mutualisé, la question n’est presque jamais « combien de fiches puis-je afficher ? » mais « combien de jointures ma requête de recherche génère-t-elle réellement ? ». Un catalogue de mille biens bien indexé consomme souvent moins de ressources qu’un catalogue de cent biens interrogé avec des
meta_querycombinées.
Le hook retenu pour maintenir la table à jour
La table dédiée n’a d’intérêt que si elle reste synchronisée avec le contenu réel des fiches. Le choix technique s’est porté sur le hook save_post, filtré sur le type de contenu concerné, plutôt que sur une reconstruction périodique complète de la table, jugée trop coûteuse à mesure que le catalogue grandirait :
add_action( 'save_post_bien_immobilier', function ( $post_id ) {
global $wpdb;
$wpdb->replace(
$wpdb->prefix . 'recherche_biens',
array(
'post_id' => $post_id,
'prix' => get_post_meta( $post_id, 'prix', true ),
'ville' => get_post_meta( $post_id, 'ville', true ),
'nb_pieces' => get_post_meta( $post_id, 'nb_pieces', true ),
)
);
} );
Ce que ce dossier change pour les prochains catalogues
Aucune migration vers un serveur plus puissant n’a été nécessaire pour résoudre ce dossier. La leçon retenue porte sur la conception même du moteur de recherche : dès qu’un catalogue combine plusieurs critères de filtrage sur des champs personnalisés, une table dédiée correctement indexée devient préférable à une accumulation de meta_query, quelle que soit la puissance de l’hébergement sous-jacent.