Un client gérant un site de location de matériel de chantier signalait une page « projets similaires » particulièrement lente sur les fiches de ses catégories d’engins. Chaque fiche affichait un champ ACF de type Relationship pointant vers trois à cinq autres fiches jugées complémentaires par l’équipe commerciale. Rien de sophistiqué en apparence, et pourtant la page catégorie, qui listait trente fiches, mettait plus de deux secondes à s’afficher côté serveur.
L’analyse avec Query Monitor a montré un motif classique de requête N+1 : une requête supplémentaire déclenchée pour chaque article de la boucle principale, soit trente requêtes quasi identiques exécutées l’une après l’autre.
Comprendre ce que stocke réellement un champ Relationship
Un champ ACF de type Relationship ne stocke pas les articles liés eux-mêmes, mais un tableau de leurs identifiants, sérialisé dans wp_postmeta. Récupérer ce champ avec get_field() renvoie par défaut un tableau d’objets WP_Post complets, ce qu’ACF obtient en interne en appelant get_posts() une fois par article source, pour résoudre chaque tableau d’identifiants en objets complets.
Le code fautif ressemblait à ceci, tout à fait naturel à écrire sans connaître ce détail d’implémentation :
foreach ( $query->posts as $post ) {
$projets_lies = get_field( 'projets_similaires', $post->ID );
foreach ( $projets_lies as $projet ) {
echo '<a href="' . get_permalink( $projet ) . '">' . get_the_title( $projet ) . '</a>';
}
}
Chaque appel à get_field( 'projets_similaires', $post->ID ) déclenche en coulisses la résolution complète des articles liés, avec sa propre requête SQL, répétée pour chacune des trente fiches de la boucle principale.

La solution en une requête regroupée
Plutôt que de laisser ACF résoudre les objets complets pour chaque article séparément, la stratégie retenue a consisté à ne récupérer que les identifiants bruts, sans résolution automatique, en passant le troisième paramètre $format_value à false, puis à effectuer une seule requête WP_Query pour l’ensemble des identifiants collectés sur toute la page :
$tous_les_ids = array();
$ids_par_post = array();
foreach ( $query->posts as $post ) {
$ids = get_field( 'projets_similaires', $post->ID, false );
$ids_par_post[ $post->ID ] = $ids;
$tous_les_ids = array_merge( $tous_les_ids, $ids ?: array() );
}
$tous_les_ids = array_unique( $tous_les_ids );
$projets_lies_query = new WP_Query( array(
'post_type' => 'projet',
'post__in' => $tous_les_ids,
'posts_per_page' => -1,
'orderby' => 'post__in',
) );
$projets_par_id = array();
foreach ( $projets_lies_query->posts as $projet ) {
$projets_par_id[ $projet->ID ] = $projet;
}
La boucle d’affichage n’a ensuite plus qu’à piocher dans le tableau $projets_par_id déjà construit en mémoire, sans déclencher la moindre requête supplémentaire :
foreach ( $ids_par_post[ $post->ID ] as $id_lie ) {
if ( isset( $projets_par_id[ $id_lie ] ) ) {
$projet = $projets_par_id[ $id_lie ];
echo '<a href="' . get_permalink( $projet ) . '">' . get_the_title( $projet ) . '</a>';
}
}
Le gain mesuré
Le nombre de requêtes SQL liées à l’affichage des projets similaires est passé de trente à une seule, quel que soit le nombre de fiches affichées sur la page. Le temps de génération de la page catégorie est descendu de 2,1 secondes à environ 350 millisecondes côté serveur, sans changement d’hébergement ni ajout de cache supplémentaire.
- Toujours vérifier si une fonction pratique comme
get_field()masque une résolution coûteuse en coulisses. - Passer
$format_valueàfalsepermet de garder la main sur la façon dont les identifiants sont résolus. post__incombiné àorderby: post__inpermet de conserver un ordre cohérent malgré le regroupement.
Ce que ce correctif ne couvre pas
Cette technique cible spécifiquement le N+1 provoqué par un champ Relationship affiché en boucle sur une page de liste. Le cache de champs simples, comme le texte ou les nombres, suit une logique différente déjà traitée par ailleurs via update_meta_cache(), et ne suffit pas à lui seul à résoudre ce cas précis lié aux relations entre contenus.
Pour aller plus loin
Ce genre de restructuration demande un peu plus de rigueur au moment de l’écriture, mais elle transforme un coût qui grandit linéairement avec le nombre d’articles affichés en un coût constant, une propriété precieuse dès qu’un site grandit en volume de contenu.