Sélectionner et filtrer
- Leçon 4
- 45 min
- Module 2 : L'essentiel de SQL
Leçon 4 sur 18 · SQL et préparation des données pour l'IASélectionner et filtrer
1. Sources et structure des données
2. L'essentiel de SQL
3. Des requêtes d'analyse utiles
4. Nettoyer et transformer les données
5. Préparer la modélisation sans risque
6. Contrats de données et passation
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 Jupyter
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 main
Ce que vous devez savoir
- Les trois tables,
customers,ticketsetoutcomes, et leur granularité, présentées dans Les éléments d’un jeu de données. - La table
ticketscontient 1 417 lignes, mais 1 403 billets distincts, comme l’explique Les concepts relationnels.
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)
- Le dossier
~/projects/ticket-datacontenantlarkfield.dbetq.py. Vous l’avez créé dans Préparer une base de données d’exercice : créer la base de données. - Si le dossier ou la base de données manque ou a changé, reprenez cette section. Vous pouvez aussi réinitialiser la base de données avec
python3 get_data.py --reset. - Un terminal ouvert dans ce dossier. Tapez
python3sur macOS et Linux, etpythonsur Windows, comme l’explique Quelle commande Python saisir. - Le dossier
queries/pour vos requêtes enregistrées, comme dans Exécuter une requête enregistrée. Les requêtes de ce module sont numérotées à partir de21-. Cette leçon ajoute21-et22-.
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 sur six lignes
Section intitulée « Une requête sur six lignes »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 | delivery | 1 | |
| T-2 | chat | returns | 3 |
| T-3 | delivery | 2 | |
| T-4 | NULL | payment | 1 |
| T-5 | phone | delivery | 3 |
| T-6 | 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 showFROM tickets -- which table to readWHERE priority = 1 -- which rows to keepORDER BY ticket_id -- the order of the rowsLIMIT 10; -- the most rows to showSeul 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.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête.
Ce que WHERE garde
Section intitulée « Ce que WHERE garde »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 | vrai : gardée | faux : éliminée | |
| T-2 | chat | faux : éliminée | vrai : gardée |
| T-3 | 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 | 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'ouNULL <> 'email', donne inconnu, ni vrai ni faux.WHEREgarde 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.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête.
Les outils d’un filtre
Section intitulée « Les outils d’un filtre »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_rowsFROM ticketsWHERE 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 :
-- Every spelling of channel in the tickets table.SELECT DISTINCT channelFROM ticketsORDER BY channel;channel--------NULL chatEMAILEmailchatemailphoneweb_formwebform
9 rowsIl 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_rowsFROM ticketsWHERE 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.
Colonnes, alias et tri
Section intitulée « Colonnes, alias et tri »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
channelencontact_channel. Une requête qui nommechannels’arrête avec une erreur :no such column: channel(aucune colonne de ce nom : channel). Une requête avecSELECT *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.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Exercice guidé : enregistrer les requêtes
Section intitulée « Exercice guidé : enregistrer les requêtes »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/.
-
Ouvrez un terminal dans
~/projects/ticket-data. -
Créez le fichier
queries/21-channel-values.sqlavec la requête de l’exemple détaillé. -
Exécutez-le :
Fenêtre de terminal python3 q.py queries/21-channel-values.sqlFenêtre de terminal python q.py queries/21-channel-values.sqlVérification : la sortie contient 9 lignes, et la première est
NULL. -
Créez le fichier
queries/22-urgent-delivery.sqlavec la requête de la zone ci-dessus. -
Exécutez-le :
Fenêtre de terminal python3 q.py queries/22-urgent-delivery.sqlFenêtre de terminal python q.py queries/22-urgent-delivery.sqlVérification : la sortie contient 10 lignes, et la première ligne est
T-11384 2026-06-29 11:40:07 chat 84. -
Remplacez
LIMIT 10parLIMIT 3, puis exécutez le fichier à nouveau. Vérification : vous voyez 3 lignes, T-11384, T-11361 et T-11301. -
Remplacez
LIMIT 3parLIMIT 10. Enregistrez le fichier.
Afficher la sortie complète de l’étape 5
ticket_id created_at channel_for_report words--------- ------------------- ------------------ -----T-11384 2026-06-29 11:40:07 chat 84T-11361 2026-06-26 11:16:44 phone 175T-11301 2026-06-19 10:40:56 email 63T-11278 2026-06-17 11:50:29 chat 60T-11200 2026-06-06 07:04:17 email 40T-11198 2026-06-05 19:31:52 email 98T-11180 2026-06-03 15:21:56 chat 153T-11066 2026-05-20 11:44:46 chat 56T-11059 2026-05-19 15:08:42 email 127T-11054 2026-05-19 10:13:05 email 177
10 rowsSi 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.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Afficher l’exemple de réponse
SELECT ticket_id, channel, teamFROM ticketsWHERE priority = 1 AND channel IS NULLORDER 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.
Variante autonome : tout sauf le clavardage
Section intitulée « Variante autonome : tout sauf le clavardage »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.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Afficher l’exemple de réponse
SELECT COUNT(*) AS not_chat_rowsFROM ticketsWHERE 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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
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.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
SELECTchoisit les colonnes,FROMnomme la table etWHEREconserve les lignes où un test est vrai.ORDER BYtrie etLIMITlimite la liste. SansORDER BY, l’ordre des lignes n’est pas défini.- Nommez les colonnes dans une requête enregistrée. Utilisez
ASpour donner des noms clairs dans le résultat. - NULL signifie « aucune valeur ». Toute comparaison avec NULL est inconnue, donc
WHEREexclut la ligne. Testez sa présence avecIS 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, comparezLOWER(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