Aller au contenu
English

Sélectionner et filtrer

  • Leçon 4
  • 45 min
  • Module 2 : L'essentiel de SQL

Ce que vous saurez faire

Obtenez exactement les lignes et les colonnes d’une table nécessaires pour répondre à une question. Vérifiez ensuite qu’un filtre n’a pas éliminé des lignes que vous vouliez garder. Un filtre qui élimine des lignes ne produit aucune erreur. Vous apprendrez donc à compter.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter contient toutes les requêtes, tâches et vérifications de cette leçon, avec les consignes. Sa première cellule télécharge les données d’exercice et crée la base de données. Il fonctionne donc sans le projet local du module 1. Lisez les explications ici et exécutez le code dans le carnet.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur (seulement pour travailler localement; le carnet et les zones de cette page n’en ont pas besoin)

Testé sur macOS avec Python 3.14.6 (SQLite 3.50.4). Les zones du navigateur utilisent SQLite 3.49.1. Le carnet a été testé dans Jupyter.

Larkfield est une boutique en ligne fictive de produits pour la maison et le jardin. Ses clients contactent le soutien, et chaque contact constitue un billet de soutien. Mei, ingénieure de données, prépare les données des billets pour un modèle qui prédit les transferts à un niveau supérieur. Dans ce module, elle répond aux questions de l’équipe de soutien avec SQL. Les données sont inventées pour ce cours; elles ne concernent aucune personne réelle.

Une requête est une question écrite en SQL que vous envoyez à une base de données. La base de données répond avec une table de lignes. Commencez avec une table assez petite pour la voir au complet : six billets.

ticket_id channel team priority
T-1 email delivery 1
T-2 chat returns 3
T-3 Email delivery 2
T-4 NULL payment 1
T-5 phone delivery 3
T-6 email account 2

NULL signifie « aucune valeur n’est stockée ici ». Pour T-4, personne n’a enregistré le canal. NULL n’est ni le texte 'NULL' ni un texte vide ''. C’est l’absence d’une valeur.

Une requête comprend des parties appelées clauses. Chaque clause commence par un mot-clé :

SELECT ticket_id, channel -- which columns to show
FROM tickets -- which table to read
WHERE priority = 1 -- which rows to keep
ORDER BY ticket_id -- the order of the rows
LIMIT 10; -- the most rows to show

Seul SELECT est obligatoire; FROM est obligatoire quand vous lisez une table. Les clauses doivent suivre cet ordre. La base de données les traite dans un ordre différent. Elle lit la table (FROM) et garde les lignes qui satisfont au test (WHERE). Ensuite, elle choisit les colonnes (SELECT), trie (ORDER BY) et limite la liste (LIMIT). Le texte après -- est un commentaire que SQL ignore.

Exécutez du SQL dans votre navigateur

Essayez : six billets

Cette zone contient seulement les six billets ci-dessus. Avant d’appuyer sur Run (exécuter), prédisez combien de lignes la requête renverra. Exécutez-la ensuite. Vous devriez voir 2 lignes : T-1 et T-4. Remplacez priority = 1 par team = 'delivery' et exécutez-la à nouveau : 3 lignes.

WHERE teste chaque ligne. Il garde une ligne seulement quand le test est vrai. Voici deux tests sur les six billets :

ticket_id channel channel = 'email' channel <> 'email'
T-1 email vrai : gardée faux : éliminée
T-2 chat faux : éliminée vrai : gardée
T-3 Email faux : éliminée vrai : gardée
T-4 NULL inconnu : éliminée inconnu : éliminée
T-5 phone faux : éliminée vrai : gardée
T-6 email vrai : gardée faux : éliminée
Lignes gardées 2 3

Deux éléments de cette table sont surprenants :

  • T-3 n’est pas un billet reçu par courriel pour channel = 'email'. La comparaison de texte est exacte : 'Email' avec un E majuscule est une valeur différente. Une personne comprend les deux comme « courriel »; SQL ne le fait pas.
  • T-4 ne figure dans aucun des deux résultats. 2 + 3 = 5, pas 6. Toute comparaison avec NULL, comme NULL = 'email' ou NULL <> 'email', donne inconnu, ni vrai ni faux. WHERE garde seulement les résultats vrais. La ligne disparaît donc des deux résultats. Vous ne recevez ni erreur ni avertissement.

Pour tester la présence de NULL, utilisez IS NULL ou IS NOT NULL. channel = NULL donne aussi inconnu pour chaque ligne et ne renvoie donc aucune ligne.

Exécutez du SQL dans votre navigateur

Essayez : les comparaisons avec NULL

Cette requête ne lit aucune table; elle calcule seulement trois tests. SQLite affiche vrai comme 1 et faux comme 0. Vous devriez voir NULL, NULL et 1 : seul IS NULL donne une réponse réelle.

Voici les éléments que vous utilisez dans WHERE. Chaque exemple s’exécute sur la vraie table tickets, qui contient 1 417 lignes.

Élément Signification Exemple Lignes
=, <> égal, différent (!= fonctionne aussi) team = 'payment' 241
<, <=, >, >= comparer des nombres, ou du texte par ordre alphabétique word_count > 1000 1
AND les deux tests sont vrais priority = 1 AND team = 'delivery' 49
OR au moins un test est vrai team = 'payment' OR team = 'returns' 568
NOT le contraire d’un test NOT team = 'account' 1 229
IN (…) égal à une des valeurs d’une liste channel IN ('chat', 'phone') 598
LIKE correspond à un motif : % représente n’importe quel texte, _ représente un caractère channel LIKE 'web%' 205
IS NULL, IS NOT NULL aucune valeur, une valeur channel IS NULL 38

Les valeurs textuelles vont entre apostrophes : 'payment'. Pas les nombres : priority = 1. Les noms de colonnes n’ont pas d’apostrophes.

LIKE 'web%' trouve 205 lignes : 200 lignes de web_form et 5 lignes de webform. Un motif permet de trouver des graphies que vous ne connaissiez pas.

Dans PostgreSQL : LIKE distingue les majuscules des minuscules. Donc, channel LIKE 'email' trouve seulement email (courriel). Dans SQLite, LIKE ignore la casse des lettres A à Z. Il trouve donc aussi Email et EMAIL. PostgreSQL propose ILIKE pour une recherche qui ignore la casse. Ne dépendez d’aucun de ces comportements : écrivez la règle voulue, comme le montre l’exemple détaillé.

Exemple détaillé : combien de billets reçus par courriel?

Section intitulée « Exemple détaillé : combien de billets reçus par courriel? »

Grace, responsable du soutien, demande à Mei : « Combien de billets sont arrivés par courriel? Je veux planifier l’équipe des courriels. » Mei écrit d’abord la requête évidente et compte les lignes. COUNT(*) compte les lignes; la prochaine leçon l’explique en détail.

SELECT COUNT(*) AS email_rows
FROM tickets
WHERE channel = 'email';

La réponse est 541 lignes. Avant de l’envoyer, Mei vérifie les valeurs que la colonne contient réellement. SELECT DISTINCT renvoie chaque valeur différente une seule fois :

queries/21-channel-values.sql
-- Every spelling of channel in the tickets table.
SELECT DISTINCT channel
FROM tickets
ORDER BY channel;
Output of python3 q.py queries/21-channel-values.sql
channel
--------
NULL
chat
EMAIL
Email
chat
email
phone
web_form
webform
9 rows

Il y a 9 valeurs pour 5 canaux réels. chat a une espace au début. Email et EMAIL sont des variantes de courriel avec des majuscules. webform est web_form sans le trait de soulignement. Et NULL signifie « non enregistré ». C’est une catégorie désordonnée : un sens, plusieurs graphies. Regardez aussi l’ordre : SQLite place NULL en premier, puis trie selon le code des caractères. Une espace précède donc les majuscules, et les majuscules précèdent les minuscules.

Pour un rapport, Mei compare une forme standard du texte. TRIM retire les espaces au début et à la fin, et LOWER transforme les majuscules en minuscules :

SELECT COUNT(*) AS email_rows
FROM tickets
WHERE LOWER(TRIM(channel)) = 'email';

La réponse est 568 lignes : 541 + 20 (Email) + 7 (EMAIL). La première requête a manqué 27 lignes, soit environ 5 % du travail par courriel.

Grace pose ensuite une deuxième question : « Et combien sont arrivés par un autre canal? » Mei prédit 1 417 − 541 = 876 lignes, puis vérifie :

Requête Lignes
WHERE channel = 'email' 541
WHERE channel <> 'email' 838
WHERE channel IS NULL 38
Total 1 417

Les deux filtres « opposés » couvrent ensemble seulement 1 379 lignes. Les 38 lignes sans canal ne figurent dans aucun des deux résultats, exactement comme T-4. Mei envoie les deux nombres à Grace avec une phrase : « 38 billets n’ont aucun canal enregistré; je les compte séparément. »

Deux remarques sur ces nombres :

  • Ils comptent les lignes, pas les billets. La table contient 14 lignes en double, comme vous l’avez découvert au module 1. Le module 2 compte les billets distincts dans la prochaine leçon.
  • Mei ne modifie pas les données. LOWER(TRIM(channel)) modifie seulement ce que cette requête compare. Le module 4 nettoie la colonne elle-même, en consignant chaque modification.

SELECT * renvoie toutes les colonnes. C’est utile pour examiner une table une première fois. Dans une requête enregistrée, nommez les colonnes nécessaires :

  • La requête indique ce qu’elle utilise. La personne qui la lit n’a pas besoin de consulter la table.
  • Le résultat ne change pas quand l’exportation change. La prochaine exportation du service de soutien renomme channel en contact_channel. Une requête qui nomme channel s’arrête avec une erreur : no such column: channel (aucune colonne de ce nom : channel). Une requête avec SELECT * s’exécute et vous donne une table différente sans rien signaler.

Un alias donne un nouveau nom à une colonne du résultat avec AS : word_count AS words. Un alias est utile pour une colonne calculée, comme LOWER(TRIM(channel)) AS channel_for_report. Il modifie seulement le résultat, pas la table.

ORDER BY trie les lignes : ASC (par défaut) du plus petit au plus grand, DESC du plus grand au plus petit. Sans ORDER BY, l’ordre des lignes n’est pas défini. Il peut changer entre les exécutions ou les versions de la base de données. Donc, LIMIT sans ORDER BY donne « 10 lignes quelconques », pas « les 10 premières ».

Les dates de cette base de données sont du texte sous la forme YYYY-MM-DD HH:MM:SS. Le texte sous cette forme se trie par ordre chronologique. Ainsi, ORDER BY created_at DESC place le billet le plus récent en premier.

Exécutez du SQL dans votre navigateur

Essayez : les billets urgents de livraison les plus récents

Grace veut lire les billets urgents de livraison les plus récents. Faites une prédiction : la première ligne sera-t-elle de juin 2026? Exécutez la requête. Vous devriez voir 10 lignes; la première est T-11384, créée le 29 juin 2026. Retirez ensuite les deux lignes WHERE et exécutez la requête à nouveau. La première ligne est maintenant T-10147, créée en 2036. Le tri montre les extrêmes d’une colonne. C’est donc une façon rapide de trouver des valeurs impossibles.

Exécutez les mêmes requêtes sur votre ordinateur et enregistrez-les dans votre projet. Désormais, chaque requête enregistrée est un fichier dans queries/.

  1. Ouvrez un terminal dans ~/projects/ticket-data.

  2. Créez le fichier queries/21-channel-values.sql avec la requête de l’exemple détaillé.

  3. Exécutez-le :

    Fenêtre de terminal
    python3 q.py queries/21-channel-values.sql

    Vérification : la sortie contient 9 lignes, et la première est NULL.

  4. Créez le fichier queries/22-urgent-delivery.sql avec la requête de la zone ci-dessus.

  5. Exécutez-le :

    Fenêtre de terminal
    python3 q.py queries/22-urgent-delivery.sql

    Vérification : la sortie contient 10 lignes, et la première ligne est T-11384 2026-06-29 11:40:07 chat 84.

  6. Remplacez LIMIT 10 par LIMIT 3, puis exécutez le fichier à nouveau. Vérification : vous voyez 3 lignes, T-11384, T-11361 et T-11301.

  7. Remplacez LIMIT 3 par LIMIT 10. Enregistrez le fichier.

Afficher la sortie complète de l’étape 5
Output of python3 q.py queries/22-urgent-delivery.sql
ticket_id created_at channel_for_report words
--------- ------------------- ------------------ -----
T-11384 2026-06-29 11:40:07 chat 84
T-11361 2026-06-26 11:16:44 phone 175
T-11301 2026-06-19 10:40:56 email 63
T-11278 2026-06-17 11:50:29 chat 60
T-11200 2026-06-06 07:04:17 email 40
T-11198 2026-06-05 19:31:52 email 98
T-11180 2026-06-03 15:21:56 chat 153
T-11066 2026-05-20 11:44:46 chat 56
T-11059 2026-05-19 15:08:42 email 127
T-11054 2026-05-19 10:13:05 email 177
10 rows

Si vous voyez SQL error: no such column (erreur SQL : aucune colonne de ce nom), vérifiez l’orthographe de chaque nom de colonne. Si vous voyez larkfield.db is not in this folder (larkfield.db n’est pas dans ce dossier), votre terminal n’est pas dans ~/projects/ticket-data.

Exécutez du SQL dans votre navigateur

À vous : les billets urgents sans canal

Grace veut savoir quels billets urgents n’ont aucun canal enregistré. La requête dans la zone contient une erreur et ne renvoie donc aucune ligne. Trouvez l’erreur et corrigez-la. Une fois la requête corrigée, vous voyez 5 lignes, la première est T-10133, et la vérification réussit.

Afficher l’exemple de réponse
SELECT ticket_id, channel, team
FROM tickets
WHERE priority = 1
AND channel IS NULL
ORDER BY ticket_id;

channel = NULL donne inconnu pour chaque ligne, donc WHERE n’en garde aucune. channel IS NULL est vrai pour les 38 lignes sans canal. Cinq d’entre elles ont la priorité 1 : T-10133, T-10160, T-10229, T-10374 et T-10437.

Omar, le gestionnaire de produit, demande : « Combien de billets ne sont pas arrivés par clavardage? Incluez les billets sans canal enregistré et comptez chat avec une espace comme du clavardage. »

Exécutez du SQL dans votre navigateur

À vous de jouer : sans clavardage

La requête de départ donne un nombre, mais elle comporte deux erreurs. Modifiez seulement la clause WHERE. Avant d’exécuter votre réponse, notez une prédiction : la table contient 1 417 lignes, dont 421 représentent le clavardage sous différentes formes.

Afficher l’exemple de réponse
SELECT COUNT(*) AS not_chat_rows
FROM tickets
WHERE LOWER(TRIM(channel)) <> 'chat'
OR channel IS NULL;

La réponse est 996 lignes : 1 417 − 421 = 996. La requête de départ donne 966 : elle compte les 8 lignes de chat comme « sans clavardage » (+8). Elle exclut aussi les 38 lignes sans canal (−38). Sans la ligne OR channel IS NULL, la comparaison corrigée donne 958, soit encore 38 de moins. Vérifiez toujours un filtre de négation ainsi : les lignes retenues et les lignes rejetées doivent totaliser toute la table.

Erreur courante

Un filtre « sans courriel » perd les lignes sans canal

Symptôme : Tomás prépare une table d’entraînement contenant « tous les billets qui ne sont pas arrivés par courriel ». Il veut vérifier si le modèle fonctionne de la même façon pour les autres canaux. Il utilise WHERE channel <> 'email' et obtient 838 lignes. Plus tard, Priya compte à la main : 1 417 lignes moins 541 lignes de courriel donnent 876. Où sont les 38 autres?

Cause : pour les 38 lignes où channel vaut NULL, channel <> 'email' est inconnu, et non vrai. WHERE les exclut. SQL ne donne ni erreur ni avertissement. Ce même filtre a aussi conservé les 27 lignes de Email et EMAIL, qui sont des billets reçus par courriel.

Diagnostic : comptez les deux côtés du filtre et les valeurs NULL, puis additionnez-les : 541 + 838 + 38 = 1 417. Si la somme des « oui » et des « non » est inférieure au total, les lignes NULL manquent.

Correction : décidez ce que signifient les lignes NULL et indiquez-le dans la requête. Ici, « sans canal » ne signifie pas courriel, donc Tomás les conserve : WHERE LOWER(TRIM(channel)) <> 'email' OR channel IS NULL donne 849 lignes (1 417 − 568).

Conséquence pour le travail en IA : le modèle ne voit jamais de billet sans canal pendant l’entraînement. En production, ces billets arrivent quand même, et le score du modèle pour ces billets est inconnu. Un filtre qui supprime un groupe sans avertissement peut rendre les données d’entraînement différentes des données réelles.

Vérifiez votre compréhension

Une table contient 100 lignes. WHERE status = 'open' renvoie 60 lignes, et le filtre opposé, WHERE NOT status = 'open', renvoie 30 lignes. Expliquez les 10 lignes manquantes et écrivez une requête qui les renvoie.

Afficher la réponse

Les 10 lignes ont NULL dans status. Toute comparaison avec NULL est inconnue, et WHERE conserve seulement les lignes où le test est vrai. Ces lignes ne figurent donc dans aucun des deux résultats. Pour les trouver : SELECT * FROM the_table WHERE status IS NULL; (ou nommez les colonnes). La requête devrait renvoyer 10 lignes, et 60 + 30 + 10 = 100.

Vérifiez votre compréhension

La requête de Mei WHERE channel = 'email' renvoie 541 lignes, et WHERE LOWER(TRIM(channel)) = 'email' en renvoie 568. Que révèle cette différence de 27 lignes sur les données? Pourquoi Mei ne corrige-t-elle pas les données dans cette requête?

Afficher la réponse

27 lignes enregistrent le courriel avec des majuscules : 20 Email et 7 EMAIL. La colonne contient une catégorie désordonnée : un seul sens avec plusieurs graphies. La deuxième requête compare une forme standard du texte pour ce rapport seulement, sans modifier la table. Les modifications des données appartiennent à une étape de nettoyage qui conserve les valeurs originales et compte chaque changement (module 4). Tout le monde utilise alors les mêmes données corrigées et peut les vérifier.

Vérifiez votre compréhension

Pourquoi une requête enregistrée devrait-elle nommer ses colonnes plutôt qu'utiliser SELECT *? Donnez une raison importante lors de l'arrivée du prochain export.

Afficher la réponse

Une requête enregistrée qui nomme ses colonnes indique ce qu’elle utilise et échoue clairement lorsque la source change. Dans le prochain export de Larkfield, channel est renommé contact_channel. Une requête qui nomme channel s’arrête avec no such column: channel (aucune colonne nommée channel). Vous le remarquez donc immédiatement. Une requête avec SELECT * s’exécute et renvoie un ensemble différent de colonnes. Le problème apparaît plus tard, dans un rapport ou un modèle.

Récapitulatif

  • SELECT choisit les colonnes, FROM nomme la table et WHERE conserve les lignes où un test est vrai. ORDER BY trie et LIMIT limite la liste. Sans ORDER BY, l’ordre des lignes n’est pas défini.
  • Nommez les colonnes dans une requête enregistrée. Utilisez AS pour donner des noms clairs dans le résultat.
  • NULL signifie « aucune valeur ». Toute comparaison avec NULL est inconnue, donc WHERE exclut la ligne. Testez sa présence avec IS NULL.
  • La comparaison de texte est exacte. 'Email' n’est pas 'email'. Examinez les valeurs distinctes d’une catégorie avant de filtrer selon celle-ci. Pour un rapport, comparez LOWER(TRIM(column)); nettoyez les données elles-mêmes au module 4.
  • Vérifiez un filtre en comptant : les lignes retenues, les lignes rejetées et les lignes NULL doivent totaliser toute la table.

Ensuite, vous compterez et résumerez plusieurs lignes à la fois. Vous verrez comment les valeurs NULL et les valeurs erronées changent un total ou une moyenne.

Leçon suivante : Agréger correctement