vendredi 25 septembre 2026

À propos

Contact

Performance

Champs ACF Relationship en boucle : le N+1 que WP_Query ne voit pas venir

Un champ Relationship affiché dans une boucle déclenche une requête par article lié. La solution : un unique WP_Query avec post__in plutôt que get_posts() répété.

Par Clément Hadrot • 28 avril 2022 • 4 min de lecture • Aucun commentaire
Champs ACF Relationship en boucle : le N+1 que WP_Query ne voit pas venir

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.

L'essentiel à retenir : Un champ Relationship stocke des identifiants, pas les articles complets ; Boucler avec get_posts() par article lié multiplie les requêtes ; post__in avec une seule requête regroupée élimine le N+1

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 à false permet de garder la main sur la façon dont les identifiants sont résolus.
  • post__in combiné à orderby: post__in permet 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.

Partager :

À propos de l'auteur

Clément Hadrot

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

Voir tous ses articles

Dans la même veine

À lire aussi