Astuces pour optimiser ses requêtes WP_Query dans WordPress

Lorsque l’on développe des applications web métiers pour des clients, écrire un code performant est absolument primordial. L’optimisation des requêtes SQL déclenchées par votre code d’extension WordPress est donc un aspect à ne pas négliger.

Dans cette leçon, nous allons aborder quelques points importants à connaître quand on écrit des requêtes SQL dans WordPress avec la classe WP_Query ou la fonction get_posts(). Grâce à ces astuces, j’espère que le temps d’exécution de vos requêtes SQL restera raisonnable !


Améliorer ses compétences WordPress

Cet article fait partie d’une série de leçons présentant les différentes APIs et fonctions PHP disponibles dans WordPress. Découvrez l’article de présentation Comment devenir un bon développeur back-end WordPress ? pour en savoir plus et accéder aux autres leçons.

Comment accélérer les requêtes SQL dans WordPress ?

N’utilisez pas posts_per_page => -1

Il est probablement rare que vous souhaitiez récupérer tous les résultats de posts d’une requête. Limitez toujours le nombre de résultats en indiquant un paramètre posts_per_page qui correspond à votre besoin.

Dans le cas contraire, vous risqueriez de rencontrer des problèmes de performance. Imaginez un site avec des centaines de milliers de posts dans la base de données : si votre requête demande à récupérer tous les articles, vous avez des chances de faire crasher votre serveur SQL et rendre votre site indisponible.

Ne demandez que ce dont vous avez besoin

Par défaut, l’instantiation d’un nouvel objet WP_Query déclenche 5 requêtes liées qui inclue notamment le calcul de la pagination et l’amorçage des caches des terms et des metas.

Les paramètres suivants aident à diminuer le nombre de requêtes liées au cache et améliorent le temps d’exécution de votre requête.

$query = new WP_Query( [
	'no_found_rows'          => true,
	'update_post_meta_cache' => false,
	'update_post_term_cache' => false,
	'fields'                 => 'ids',
] );

Paramètre no_found_rows: true

Si vous n’avez pas besoin de paginer vos résultats, utilisez toujours ce paramètre. Il indiquera à WordPress de ne pas exécuter SQL_CALC_FOUND_ROWS sur la requête en question, ce qui l’améliorera considérablement.

SQL_CALC_FOUND_ROWS calcule le nombre total de lignes de résultats de la requête, ce qui est utile si l’on veut connaître le nombre total de pages à afficher dans une pagination. Mais si vous cette requête n’a pas besoin de pagination, désactivez-la avec ce paramètre.

update_post_meta_cache: false et update_post_term_cache: false

Dans le contexte d’une WP_Query ou WP_Term_Query, il existe une étape post-requête qui s’occupe d’analyser et itérer sur chaque résultat pour les ajouter dans certains caches de courte durée. Le but est de sauvegarder ces données pour un éventuel usage dans la boucle en cours, et d’optimiser les requêtes futures qui souhaiteraient filtrer sur les métadonnées des résultats.

Cependant, cette mise en cache faite « sous le capot » n’est parfois pas utile et si vous possédez beaucoup de terms ou de metas, l’impact pourrait être négatif.

Si votre fonction/requête ne fait pas un usage des metas ou des terms des résultats, définissez ces deux paramètres sur false pour court-circuiter ces amorçages de cache automatiques déclenchés par WordPress.

Mettez en cache les requêtes coûteuses

Pensez à mettre en cache un maximum de vos requêtes (en utilisant l’API Transients de WordPress) pour éviter d’exécuter des requêtes similaires à charque chargement de page.

En effet, si vous n’avez pas la nécessité d’avoir toujours des résultats frais et avez la possibilité de calculer une requête 1 fois par heure ou une fois par jour, faites-le et votre site et serveur vous remercieront !

Voyez ci-dessous comment mettre en place une telle logique pour récupérer un certain nombre de posts dans une catégorie spécifique.

/**
 * Récupère X posts dans la catégorie Y.
 *
 * @param integer $amount
 * @param string $category
 * @return array
 */
function get_some_posts( $amount = 15, $category = '' ) {
	$transient_name = sanitize_title( "query_posts-{$amount}{$category}" );

	if ( ( $results = get_transient( $transient_name ) ) === false ) {
		$results = get_posts( [
			'posts_per_page'         => $amount,
			'post_type'              => 'post',
			'category_name'          => $category,
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'fields'                 => 'ids',
		] );

		set_transient( $transient_name, is_array( $results ) ? $results : [], DAY_IN_SECONDS );
	}

	return $results;
}

On vérifie d’abord l’existence d’un transient avec get_transient() : s’il n’existe pas encore dans notre base de données, on exécute notre requête WP_Query (ici, avec get_posts()) et on met en cache les résultats obtenus avec set_transient().

Concrètement, détaillons ce qu’il se passe si on appelle la fonction get_some_posts( 15, 'decouverte' ) plusieurs fois au cours de la journée :

  1. la première fois, aucun cache n’existe pour cette demande : la requête get_posts() est exécutée et les résultats sont mis en cache dans un transient dans la table wp_options,
  2. les fois suivantes — et jusqu’à l’expiration du cache prévue 1 jour après sa création — les résultats proviennent directement de ce cache et l’exécution de la requête get_posts() potentiellement coûteuse est évitée.

Évitez l’usage de post__not_in

L’utilisation du paramètre post__not_in semble avoir une grande utilité en appraence, mais son impact sur les performances d’un gros site peut être fort et coûteux car il affecte le hit rate du cache du site.

À quoi sert ce paramètre ? Il permet d’exclure des IDs de posts spécifiques d’une requête. Par exemple, sur la page d’un article, on souhaite afficher en bas de page 5 autres articles classés dans la même catégorie — en excluant l’article actuellement visité.

Dans la majorité des cas, votre code sera plus efficace si vous filtrez/ignorez le ou les posts à exclure directement en PHP après votre requête SQL. Cela permettra d’une part une exécution SQL plus rapide (avec une facette en moins), mais aussi une meilleure mise en cache de cette requête et de ses résultats.

Voyons un exemple concret :

function display_some_news( $exclude = [] ) {
	$recent_posts = new WP_Query( [
		'category_name'       => 'news',
		'posts_per_page'      => 5,
		'post_status'         => 'publish',
		'ignore_sticky_posts' => true,
		'no_found_rows'       => true,
		'post__not_in'        => $exclude,
	] );

	while ( $recent_posts->have_posts() ) {
		$recent_posts->the_post();
		the_title( '<h2><a href="' . get_permalink() . '">', '</a></h2>');
	}

	wp_reset_postdata();
}

On souhaite récupérer 5 articles de la catégorie news en excluant certains articles spécifiques (fournis avec l’argument $exclude).

On pourrait optimiser cette logique de la manière suivante afin d’accélérer la requête SQL qui découle de cette fonction :

function display_some_news( $exclude = [] ) {
	$recent_posts = new WP_Query( [
		'category_name'       => 'news',
		'posts_per_page'      => 5 + count( $exclude ),
		'post_status'         => 'publish',
		'ignore_sticky_posts' => true,
		'no_found_rows'       => true,
	]; );

	$posts = 0;

	while ( $recent_posts->have_posts() && $posts < 5 ) {
		$recent_posts->the_post();

		if ( ! in_array( get_the_ID(), $exclude ) ) {
			$posts++;
			the_title( '<h2><a href="' . get_permalink() . '">', '</a></h2>');
		}
	}

	wp_reset_postdata();
}

Cette technique consiste :

  1. à supprimer le paramètre post__not_in de la query,
  2. à demander un peu plus de posts que prévu (5 + le nombre de posts exclus),
  3. puis à analyser/boucler sur les résultats et vérifier si l’ID d’un post est présent dans le tableau d’exclusion $exclude : s’il n’est pas présent dans ce tableau, on peut le traiter et l’afficher.

Cette approche, même si elle demande un peu de bricolage PHP pour arriver au même résultat, vous permettra de profiter au mieux de votre système de cache et vous assurera des requêtes SQL optimales.

Vérifiez seulement l’existence d’une meta_key quand c’est possible

Sur des sites WordPress avec beaucoup de contenus, la table wp_postmeta stockant les métadonnées des posts peut être énorme. Soyez donc astucieux lorsque vous souhaitez faires des requêtes de posts en fonction de la valeur d’une de leur métadonnée.

$query = new WP_Query( [
	'meta_query' => [
		[
			'key'   => 'hide_on_homepage',
			'value' => '1',
		]
	]
] );

Lorsque la meta_value d’une requête présentant une meta_query est binaire/booléenne (true, 1), MySQL va analyser chaque ligne possèdant la meta_key désirée afin de vérifier si la meta_value correspond à 1.

Vous pouvez aider MySQL et optimiser votre requête en ne lui demandant que de vérifier la présence de cette meta_key sans s’inquiéter de la meta_value associée. Cela aura un impact positif sur la requête car elle pourra s’appuyer sur l’index de la colonne meta_key ; ce ne serait pas le cas si une vérification sur la meta_value existait.

Et voici la WP_Meta_Query retravaillée :

$query = new WP_Query( [
	'meta_query' => [
		[
			'key'     => 'hide_on_homepage',
			'compare' => 'EXISTS',
		]
	]
] );

Si un post possède cette métadonnée, on le cachera sur la page d’accueil. S’il ne doit pas ou plus être caché, supprimez simplement la métadonnée en question avec delete_post_meta( $post_id, 'hide_on_homepage' ).

N’enregistrez pas une meta_value à 0 ou false dans ces cas là car cette astuce ne fonctionnerait plus et vous surchargerez la table de métadonnées pour rien. Si un état spécifique n’existe plus sur un post, autant supprimer sa métadonnée relative.

Faites bon usage des taxonomies

Dans WordPress, une taxonomie est un outil puissant qui permet de grouper ou catégoriser plusieurs posts.

A contrario, les métadonnées (stockées dans la table wp_postmeta) permettent d’associer des informations uniques à des posts spécifiques.

La manière dont ces métadonnées sont stockées (index sur les clés mais pas sur les valeurs) ne permet pas de faire des recherches complexes très performantes. Les tables SQL des taxonomies possèdent plus d’indexes et une architecture plus adaptée à des recherches filtrées performantes.

En règle générale, évitez tant que possible de faire des requêtes meta_query complexes — même si cela est impossible à éviter totalement lors de développement de solutions WordPress avancées. Et si vous devez le faire, essayez de les mettre en cache comme recommandé un peu plus haut.

Certaines requêtes sur des valeurs de métadonnée peuvent être transformées en des requêtes sur des taxonomies. Par exemple, au lieu d’utiliser un filtre sur une meta_value pour savoir si un post doit être rendu visible aux utilisateurs avec un abonnement actif, utilisez une taxonomie pour créer une telle relation et filtrer vos posts de manière plus efficace.

Au lieu de :

$results = WP_Query( [
	'meta_query' => [
		[
			'key'   => 'abonnement',
			'value' => 'premium',
		],
	]
] );

Préférez :

$results = WP_Query( [
	'tax_query' => [
		[
			'taxonomy' => 'abonnement',
			'terms'    => 'premium',
			'field'    => 'slug',
		],
	]
] );

Utilisez Query Monitor et préférez les calculs côté PHP

Liste des requêtes SQL d'une extension back-end WordPress avec Query Monitor
L’extension Query Monitor vous aide à lister et analyser les requêtes SQL exécutées sur une page

Durant les phases de développement de votre extension ou thème WordPress, installez et activez Query Monitor. Ce plugin est vital pour vous permettre la visualisation et le déboggage des logiques qui sont exécutées dans les coulisses PHP d’une page d’un site WordPress.

L’onglet « Requêtes » notamment permet de voir les requêtes SQL exécutées sur la page en cours — et Query Monitor met même en valeur en rouge les éventuelles requêtes lentes.

En cliquant sur la colonne « Heure », vous pourrez trier les requêtes selon leur temps d’exécution. Cherchez toujours à naviguer sur les pages de votre site en cours de développement et à analyser ce tableau de requêtes pour cibler celles qui prennent le plus de temps.

Après avoir identifié les requêtes les plus coûteuses, vous pourrez voir qui les appelle et commencer à les optimiser avec les techniques évoquées dans cet article.

Favorisez le traitement PHP d’une liste de résultats

Si une requête complexe alourdit trop le chargement de la page, une technique possible est de faire une requête avec moins de conditions pour récupérer une liste de résultats plus large pour ensuite l’affiner côté PHP.

Autre astuce, plutôt que de faire une requête multi-dimensionnelle complexe cumulant :

  1. plusieurs segments meta_key => meta_value,
  2. et plusieurs segments d’appartenance du post à plusieurs termes de plusieurs taxonomies,

Il est peut-être plus judicieux :

  1. d’effectuer 2 requêtes séparées (une récupérant les posts correspondant au premier filtre sur les métadonnées, et l’autre récupérant ceux du deuxième filtre de taxonomies),
  2. et de traiter les 2 en PHP pour comparer les 2 listes de résultats et déterminer les posts existants simultanément dans les 2 tableaux.

Query Monitor vous permettra de comparer ces deux logiques de requêtes SQL. Sa documentation indique comment faire du profilage basic pour comparer le temps d’exécution et l’impact mémoire de vos fonctions.

Écrire un code WordPress performant, c’est possible !

Sources et applications

Ces différentes astuces sont issues de sources de professionnels du développement web : les conseils « Performance PHP » de 10up, les guides « Qualité du code et bonnes pratiques » de WPVip.com et bien sûr ma propre expérience de professionnel en développement WordPress back-end.
Elles proviennent d’optimisations mises en place sur des gros sites contenant beaucoup de contenus dans la base SQL de WordPress.

Ces quelques astuces sont à considérer — intelligemment — lorsque vous codez avec et pour WordPress.

En tant que développeur back-end, il peut parfois nous arriver d’oublier que notre extension ou notre thème vivra à côté d’autres plugins, ou que notre site sera servi par un hébergeur qui propulse des milliers/millions d’autres sites avec une base de données commune (comme WordPress.com).

En ce concentrant uniquement sur la logique métier de nos solutions, on peut vite oublier des règles simples d’optimisation qui permettraient pourtant d’assurer un meilleur chargement des pages et un usage optimisé des ressources du serveur.

Pourtant, comme on vient de le voir, WordPress fournit un ensemble de fonctionnalités et d’APIs pratiques afin de nous aider à construire des extensions et des thèmes plus performants, sans compromettre la vitesse globale du site et l’expérience-utilisateur des visiteurs.


Et vous, avez-vous des astuces intéressantes pour améliorer les requêtes SQL d’un site WordPress ? Partagez-les nous en commentaire !


Vous avez aimé cet article ?

Partagez-le sur vos réseaux sociaux en guise de remerciement :)


2 commentaires

Franchement, c’est top. Tu montres qu’on peut faire du vrai dev avec WordPress, que c’est pas juste un outil pousse-bouton, clic clic, ou que sais-je.

Merci pour ton contenu 🙂

Laisser un commentaire