StraFormationStraFormation

SQL : pourquoi les clients sans commande disparaissent-ils malgré un LEFT JOIN ?

Illustration : une analyste soulève un volet masquant dans un registre les portraits de clients sans commande, tandis que d’autres portraits sont accompagnés de colis.
SQLreportinganalyse de donnéesformation SQLLEFT JOIN

Votre responsable commercial demande le nombre de commandes du mois pour chaque client, y compris ceux qui n’ont rien commandé. Vous partez de la table clients, faites un LEFT JOIN avec les commandes… et plusieurs clients disparaissent du résultat.

Le premier point à vérifier est le filtre de période. Si les conditions portant sur les commandes se trouvent dans WHERE, elles peuvent éliminer les clients que la jointure devait conserver. Pour obtenir tout le portefeuille avec un compteur éventuellement nul, placez les conditions définissant les commandes à compter dans ON, puis comptez leur identifiant avec COUNT(o.id).

Voici un cas fictif reproductible pour comprendre la différence, éviter un faux correctif et expliquer le résultat à la personne qui utilisera le reporting. Les requêtes et leurs résultats ont été vérifiés dans SQLite 3.53.1, sur une base de test isolée ; ils n’ont pas été testés sur vos données ni sur tous les moteurs SQL.

Commencez par la question commerciale

« Les commandes de septembre par client » laisse deux interprétations possibles :

  • les clients qui ont au moins une commande admissible en septembre ;
  • tous les clients du périmètre, avec le nombre de commandes admissibles en septembre, éventuellement zéro.

Ces deux tableaux peuvent être corrects et répondre à des décisions différentes. Le premier décrit l’activité observée. Le second permet aussi d’examiner les clients sans activité enregistrée sur la période.

Avant de corriger la requête, faites préciser trois éléments au responsable du reporting : la liste de clients à couvrir, les dates et les statuts de commande retenus. Un portefeuille géré par une équipe peut exclure certains comptes fermés ; cela doit être une règle explicite, pas une conséquence accidentelle de la jointure.

Dans notre exemple, la règle est simple : les quatre clients, et uniquement leurs commandes de septembre 2026 dont le statut vaut validee. Une commande annulée ne compte pas. Les noms et données ci-dessous sont entièrement fictifs.

Le jeu de données : quatre situations, pas quatre variantes du même client

Table clients :

idnom
1Atelier A
2Boutique B
3Cabinet C
4Dépôt D

Table commandes : chaque ligne représente une commande, et id est son identifiant unique, non nul.

idclient_iddate_commandestatut
10112026-09-03validee
10212026-09-22validee
20122026-08-28validee
40142026-09-12annulee

Atelier A a deux commandes à compter. Boutique B a commandé, mais hors période. Cabinet C n’a aucune commande. Dépôt D a une commande dans le mois, mais annulée.

Le résultat attendu est donc quatre clients, avec les compteurs 2, 0, 0, 0. Écrire cette attente avant la requête donne un contrôle indépendant de la simple apparence du tableau.

Pour reproduire l’exemple, utilisez ces deux tables dans une base de test. Ici, les dates sont des textes au format fixe AAAA-MM-JJ. Pour un environnement réel, faites adapter les types, paramètres et bornes de période au moteur utilisé. Avec des horodatages, précisez aussi le fuseau qui définit le mois métier.

La requête qui semble logique, mais ne garde qu’un client

SELECT c.id, c.nom, COUNT(o.id) AS nb_commandes
FROM clients AS c
LEFT JOIN commandes AS o
  ON o.client_id = c.id
WHERE o.date_commande >= '2026-09-01'
  AND o.date_commande < '2026-10-01'
  AND o.statut = 'validee'
GROUP BY c.id, c.nom
ORDER BY c.id;

Résultat observé sur le jeu fictif : Atelier A, 2 commandes. Les trois autres clients n’apparaissent plus.

Pourquoi ? Dans la logique de la requête, la jointure construit les correspondances, puis WHERE filtre les lignes obtenues. Boutique B ne passe pas la condition de date. Dépôt D ne passe pas celle de statut. Pour Cabinet C, les colonnes de commande valent NULL ; les comparaisons demandées ne sont pas vraies, donc la ligne n’est pas conservée.

La documentation officielle PostgreSQL, expressions de table et jointures externes distingue précisément les conditions de ON de celles de WHERE et rappelle que WHERE ne conserve que les lignes pour lesquelles sa condition est vraie. Il s’agit ici de la logique du résultat, pas d’une description de l’ordre physique choisi par l’optimiseur.

La correction : définir les commandes admissibles dans ON

SELECT c.id, c.nom, COUNT(o.id) AS nb_commandes
FROM clients AS c
LEFT JOIN commandes AS o
  ON o.client_id = c.id
  AND o.date_commande >= '2026-09-01'
  AND o.date_commande < '2026-10-01'
  AND o.statut = 'validee'
GROUP BY c.id, c.nom
ORDER BY c.id;

Cette fois, on cherche pour chaque client les commandes qui satisfont à la fois la relation, la période et le statut. En l’absence de correspondance admissible, le client demeure présent avec des colonnes de commande à NULL.

ClientCommandes comptées en septembre
Atelier A2
Boutique B0
Cabinet C0
Dépôt D0

La question n’est donc plus « cette ligne de commande doit-elle survivre au filtre ? », mais « quelles commandes doivent être rattachées à ce client pour ce calcul ? ».

Cela ne signifie pas qu’il faut déplacer systématiquement tous les filtres dans ON. Une condition définissant le périmètre des clients peut rester dans WHERE, par exemple un secteur commercial enregistré dans la table clients. Il faut décider ce que chaque filtre sélectionne : les personnes à afficher ou les événements à compter.

Si la demande porte finalement uniquement sur les clients ayant commandé, un résultat qui exclut les autres devient légitime. Ne conservez pas les zéros par principe : conservez-les parce que la question les exige.

Pourquoi COUNT(*) transforme vos zéros en uns

Après la jointure corrigée, essayez de remplacer COUNT(o.id) par COUNT(*). Vous obtenez 2, 1, 1, 1.

Ce n’est pas une commande retrouvée. La ligne conservant chaque client sans correspondance est elle-même comptée.

Selon la documentation SQLite sur les fonctions d’agrégation, COUNT(*) compte les lignes, tandis que COUNT(X) compte les valeurs non nulles de X. Ici, l’identifiant de commande convient : il est non nul lorsqu’une commande existe et devient nul sur la ligne produite sans correspondance.

Évitez de choisir une colonne facultative, comme un commentaire : une vraie commande dont le commentaire est vide pourrait alors ne pas être comptée. Et vérifiez le niveau de détail de la source : si vous joignez des lignes de produits plutôt qu’une ligne par commande, le même identifiant peut apparaître plusieurs fois. Le présent compteur suppose bien une ligne par commande.

Le faux raccourci : ajouter OR o.id IS NULL

Une tentative consiste à garder les filtres dans WHERE et à ajouter :

WHERE (
  o.date_commande >= '2026-09-01'
  AND o.date_commande < '2026-10-01'
  AND o.statut = 'validee'
) OR o.id IS NULL

Sur notre jeu, cela ramène Atelier A et Cabinet C seulement.

Cabinet C avait une ligne sans correspondance, donc un identifiant de commande nul. Boutique B et Dépôt D avaient, eux, une correspondance réelle lors de la jointure sur le client. Leur identifiant n’est pas nul, même si leur commande échoue ensuite au filtre. Ajouter OR o.id IS NULL ne recrée pas une ligne de remplacement pour eux.

Ce contre-exemple explique pourquoi un test avec seulement « un client qui commande » et « un client sans aucune commande » peut donner une fausse impression de réussite. Il faut aussi tester les commandes hors période et celles dont le statut est exclu.

Un exercice court avant de transmettre le tableau

Gardez la requête corrigée. Ajoutez fictivement deux commandes au jeu initial :

  • Cabinet C : commande validée le 1er octobre 2026 ;
  • Dépôt D : commande validée le 30 septembre 2026.

Quels compteurs attendez-vous pour septembre ?

Corrigé : 2, 0, 0, 1. La borne haute < '2026-10-01' exclut le 1er octobre ; la commande du 30 septembre est retenue. Les quatre clients restent visibles. Ce résultat a également été vérifié dans la base de test.

Avant livraison, recopiez ce petit contrôle dans vos notes de reporting :

ContrôleAttente pour l’exemple initial
Population affichéeLes quatre identifiants clients, une fois chacun
Commandes admissiblesDeux commandes validées en septembre
Somme des compteursDeux
Clients à zéroBoutique B, Cabinet C et Dépôt D
Cas hors période ou annuléClient conservé, commande non comptée

Le rapprochement du total et de la population détecte deux erreurs différentes. Un total de deux peut être exact alors que trois clients manquent.

Zéro commande enregistrée ne prouve pas une perte de client

Dans ce cas fictif, nous connaissons toutes les données. Dans votre entreprise, un zéro peut aussi provenir d’un import incomplet, d’un identifiant client mal rapproché ou d’un périmètre de commandes différent de celui attendu.

Avant de lancer une relance commerciale ou de qualifier un compte d’inactif, faites vérifier la complétude de la période et la définition de l’indicateur par son responsable métier. Le libellé prudent est : « aucune commande admissible présente dans les données contrôlées pour cette période ». La requête ne déduit ni les intentions du client ni sa relation future avec l’entreprise.

Si votre difficulté vient plutôt de totaux gonflés pendant un rapprochement, le guide Power Query : ajouter ou fusionner sans fausser les totaux traite le contrôle des clés et du niveau de détail.

Quelle formation demander pour fiabiliser ce reporting ?

La formation SQL pour l’analyse de données de StraFormation présente deux parcours de 21 heures, avec des prérequis distincts.

Fondations & analyse correspond au besoin de construire ses bases : sélection, filtres, valeurs nulles, agrégations et premières jointures. L’aisance avec les tableaux, filtres et calculs simples est attendue ; une expérience de programmation n’est pas requise.

SQL analytique avancé s’adresse aux personnes déjà autonomes sur SELECT, WHERE, GROUP BY, HAVING et les jointures entre deux tables. Le positionnement permet de vérifier ce niveau avant de travailler des analyses plus complexes. Réussir à copier une requête corrigée ne suffit pas à établir cette autonomie.

Pour préparer une demande utile, indiquez votre fonction, la décision que le tableau doit éclairer, la population et la période à conserver, les requêtes que vous savez déjà écrire, le moteur SQL utilisé et l’environnement de test autorisé. Si vous êtes responsable d’équipe, ajoutez le nombre de bénéficiaires, leur niveau, l’échéance et le format souhaité.

Le moteur, les accès, les données d’exercice et les modalités doivent être confirmés avec l’organisme. La formation vise l’acquisition de compétences ; elle ne promet pas la correction complète d’un reporting de production. Vous pouvez déjà arriver avec une question précise : « Je veux savoir expliquer et tester pourquoi chaque client est présent, avec le bon compteur, y compris zéro. »

4.8/5

225+ avis Google

Qualiopi

Certifié qualité

Éligible CPF

100% finançable

5000+

Apprenants formés

Formations recommandées

Articles pour aller plus loin