SQL for Data Engineers

Ce module couvre les fondamentaux et concepts avancés de SQL, avec une pratique directe via Python + DuckDB.


Approche de ce cours

┌─────────────────────────────────────────────────────────────────┐
│                       FOCUS DU COURS                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Ce cours utilise Python + DuckDB pour apprendre SQL           │
│                                                                 │
│   ✅ SQL standard (compatible PostgreSQL, MySQL, etc.)          │
│   ✅ Exécution directe dans Jupyter (pas de serveur)            │
│   ✅ Intégration native avec Pandas                             │
│   ✅ Compétences transférables à tout SGBD                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

💡 Le SQL appris ici fonctionne sur PostgreSQL, MySQL, SQLite, BigQuery, Snowflake, etc. avec des variations mineures de syntaxe.


Prérequis

Niveau Compétence
✅ Requis Avoir suivi le module 06_intro_relational_databases
✅ Requis Comprendre les concepts de tables, colonnes, clés
✅ Requis Bases de Python (module 04 et 05)

Objectifs du module

À la fin de ce notebook, tu seras capable de :

  • Écrire des requêtes SELECT, WHERE, ORDER BY
  • Utiliser les fonctions d’agrégation (COUNT, SUM, AVG)
  • Maîtriser GROUP BY et HAVING
  • Faire des jointures (JOIN, LEFT JOIN)
  • Utiliser les fonctions de date (DATE_TRUNC, EXTRACT)
  • Utiliser CASE, les CTEs et les Window Functions
  • Exécuter du SQL depuis Python avec DuckDB

💡 Ce notebook est interactif : tu peux exécuter les cellules de code directement !


C’est quoi SQL ?

SQL (Structured Query Language) est un langage de requêtes pour interagir avec des bases de données relationnelles.

Action Commande SQL
🔍 Rechercher SELECT
✏️ Insérer INSERT
🔄 Modifier UPDATE
🗑️ Supprimer DELETE
📊 Agréger COUNT, SUM, AVG
🔗 Joindre JOIN

Pourquoi c’est essentiel pour un Data Engineer ?

  • Interroger les data warehouses (BigQuery, Snowflake, Redshift…)
  • Extraire / filtrer les données pour les pipelines
  • Vérifier la qualité des données
  • Créer des vues et agrégats pour les dashboards

Outils en ligne pour tester du SQL

Avant de plonger dans Python + DuckDB, sache qu’il existe de nombreux outils en ligne gratuits pour tester des requêtes SQL sur différents SGBD. C’est utile pour :

  • Tester rapidement une requête
  • Vérifier la compatibilité entre SGBD
  • S’entraîner sans rien installer
  • Partager des exemples avec des collègues

Outils recommandés

Outil URL SGBD supportés Points forts
DB Fiddle db-fiddle.com PostgreSQL, MySQL, SQLite Interface claire, partage facile
SQL Fiddle sqlfiddle.com MySQL, PostgreSQL, Oracle, SQL Server Le classique, multi-SGBD
SQLite Online sqliteonline.com SQLite, PostgreSQL, MySQL Très simple, import CSV
Programiz SQL programiz.com/sql/online-compiler SQLite Idéal débutants, tutoriels intégrés
OneCompiler onecompiler.com/mysql MySQL, PostgreSQL, SQL Server Rapide, moderne
Replit replit.com SQLite, PostgreSQL Environnement complet, collaboratif

💡 Exemple avec DB Fiddle

  1. Aller sur db-fiddle.com
  2. Choisir le SGBD (ex: PostgreSQL 15)
  3. Dans le panneau gauche, créer le schéma :
CREATE TABLE employes (
    id INT PRIMARY KEY,
    nom VARCHAR(50),
    salaire INT
);

INSERT INTO employes VALUES 
(1, 'Alice', 50000),
(2, 'Bob', 60000),
(3, 'Charlie', 55000);
  1. Dans le panneau droit, écrire ta requête :
SELECT nom, salaire 
FROM employes 
WHERE salaire > 52000;
  1. Cliquer sur Run et voir le résultat !

⚠️ Différences entre SGBD

Le SQL est standardisé, mais chaque SGBD a ses particularités :

Fonctionnalité PostgreSQL MySQL SQL Server SQLite
Concaténation \|\| ou CONCAT() CONCAT() + ou CONCAT() \|\|
Limite résultats LIMIT 10 LIMIT 10 TOP 10 LIMIT 10
Auto-increment SERIAL AUTO_INCREMENT IDENTITY AUTOINCREMENT
Date actuelle CURRENT_DATE CURDATE() GETDATE() DATE('now')
Booléens TRUE/FALSE 1/0 1/0 1/0

💡 DuckDB utilise une syntaxe proche de PostgreSQL, ce qui est idéal car PostgreSQL est le standard de facto en Data Engineering.


🎯 Pourquoi on utilise Python + DuckDB dans ce cours ?

Avantage Explication
Tout-en-un SQL + Python dans le même notebook
Pas d’installation serveur DuckDB tourne en mémoire
Intégration Pandas Résultats directement en DataFrame
Fichiers directs Lire CSV/Parquet/JSON sans import
Syntaxe standard Compatible PostgreSQL → transférable
Performance Optimisé pour l’analytics (OLAP)
# Exemple : SQL → DataFrame en 1 ligne
df = con.execute("SELECT * FROM clients WHERE pays = 'France'").df()

Les compétences SQL apprises ici sont directement applicables à PostgreSQL, BigQuery, Snowflake, Redshift, et tout autre SGBD en production.


Python + DuckDB — SQL dans ton notebook !

Qu’est-ce que DuckDB ?

DuckDB est une base de données analytique embarquée (comme SQLite, mais optimisée pour l’analytics).

┌─────────────────────────────────────────────────────────────┐
│                    POURQUOI DUCKDB ?                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ Zéro installation serveur (fonctionne en mémoire)       │
│  ✅ Syntaxe SQL standard (PostgreSQL-like)                  │
│  ✅ Intégration native avec Pandas                         │
│  ✅ Très rapide pour l'analytics (colonnar storage)        │
│  ✅ Lit directement CSV, Parquet, JSON                      │
│  ✅ Parfait pour apprendre SQL dans un notebook             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

DuckDB vs autres bases

Critère SQLite DuckDB PostgreSQL
Installation Embarqué Embarqué Serveur
Optimisé pour OLTP (transactions) OLAP (analytics) Les deux
Intégration Pandas ⚠️ Via SQLAlchemy ✅ Native ⚠️ Via psycopg2
Fichiers CSV/Parquet ❌ Non ✅ Direct ❌ Non
Usage Mobile, embarqué Data Science, ETL Production
Voir le code
# Installation de DuckDB
!pip install duckdb --quiet

print("✅ DuckDB installé !")
Voir le code
import duckdb

# Créer une connexion en mémoire
con = duckdb.connect(database=':memory:')

print("✅ Connexion DuckDB créée !")
print(f"Version : {duckdb.__version__}")
Voir le code
# Création des tables de démonstration

# Table clients
con.execute("""
CREATE TABLE clients (
    id_client INTEGER PRIMARY KEY,
    nom VARCHAR(50),
    email VARCHAR(100),
    pays VARCHAR(50),
    date_inscription DATE
);
""")

con.execute("""
INSERT INTO clients VALUES
    (1, 'Alice', 'alice@mail.com', 'France', '2023-01-15'),
    (2, 'Bob', 'bob@mail.com', 'France', '2023-02-20'),
    (3, 'Charlie', 'charlie@mail.com', 'Allemagne', '2023-03-10'),
    (4, 'Diana', 'diana@mail.com', 'Belgique', '2023-04-05'),
    (5, 'Eve', 'eve@mail.com', 'France', '2023-05-12');
""")

# Table commandes
con.execute("""
CREATE TABLE commandes (
    id_commande INTEGER PRIMARY KEY,
    id_client INTEGER,
    produit VARCHAR(50),
    montant DECIMAL(10,2),
    date_commande DATE,
    FOREIGN KEY (id_client) REFERENCES clients(id_client)
);
""")

con.execute("""
INSERT INTO commandes VALUES
    (1, 1, 'Clavier', 50.00, '2023-07-12'),
    (2, 1, 'Souris', 25.00, '2023-07-15'),
    (3, 2, 'Écran', 120.00, '2023-08-01'),
    (4, 1, 'Webcam', 45.00, '2023-08-20'),
    (5, 4, 'Casque', 80.00, '2023-09-05'),
    (6, 2, 'Clavier', 55.00, '2023-09-15'),
    (7, 5, 'Souris', 30.00, '2023-10-01'),
    (8, 4, 'Écran', 150.00, '2023-10-20');
""")

print("✅ Tables créées : clients (5 lignes), commandes (8 lignes)")
Voir le code
# Vérifier les données
print(" Table clients :")
print(con.execute("SELECT * FROM clients").fetchdf())

print("\n Table commandes :")
print(con.execute("SELECT * FROM commandes").fetchdf())

Cheatsheet SQL — Commandes essentielles

Catégorie Commande Description Exemple
Lecture SELECT Sélectionner des colonnes SELECT nom, email FROM clients
SELECT * Toutes les colonnes SELECT * FROM clients
DISTINCT Valeurs uniques SELECT DISTINCT pays FROM clients
Filtrage WHERE Filtrer les lignes WHERE pays = 'France'
AND / OR Conditions multiples WHERE age > 18 AND pays = 'France'
IN Liste de valeurs WHERE pays IN ('France', 'Belgique')
BETWEEN Plage de valeurs WHERE montant BETWEEN 50 AND 100
LIKE Recherche pattern WHERE nom LIKE 'A%'
IS NULL Valeurs nulles WHERE email IS NULL
Tri ORDER BY Trier les résultats ORDER BY nom ASC
LIMIT Limiter le nombre LIMIT 10
Agrégation COUNT() Compter SELECT COUNT(*) FROM clients
SUM() Somme SELECT SUM(montant) FROM commandes
AVG() Moyenne SELECT AVG(montant) FROM commandes
MIN() / MAX() Min / Max SELECT MAX(montant) FROM commandes
Groupement GROUP BY Regrouper GROUP BY pays
HAVING Filtrer après agrégation HAVING COUNT(*) > 5
Jointures JOIN Jointure interne JOIN commandes ON ...
LEFT JOIN Jointure gauche LEFT JOIN commandes ON ...

1. SELECT — Sélectionner des données

Syntaxe de base

SELECT colonne1, colonne2
FROM table
WHERE condition
ORDER BY colonne;
Voir le code
# SELECT * — Toutes les colonnes
query = "SELECT * FROM clients"
con.execute(query).fetchdf()
Voir le code
# SELECT colonnes spécifiques
query = "SELECT nom, email, pays FROM clients"
con.execute(query).fetchdf()
Voir le code
# WHERE — Filtrer les lignes
query = """
SELECT nom, email, pays
FROM clients
WHERE pays = 'France'
"""
con.execute(query).fetchdf()
Voir le code
# ORDER BY + LIMIT
query = """
SELECT nom, montant, date_commande
FROM commandes
JOIN clients ON clients.id_client = commandes.id_client
ORDER BY montant DESC
LIMIT 3
"""
print(" Top 3 des commandes les plus chères :")
con.execute(query).fetchdf()

2. Agrégations — COUNT, SUM, AVG, MIN, MAX

Voir le code
# COUNT — Compter les lignes
query = "SELECT COUNT(*) AS nb_clients FROM clients"
con.execute(query).fetchdf()
Voir le code
# SUM, AVG, MIN, MAX
query = """
SELECT 
    COUNT(*) AS nb_commandes,
    SUM(montant) AS total,
    AVG(montant) AS moyenne,
    MIN(montant) AS min,
    MAX(montant) AS max
FROM commandes
"""
con.execute(query).fetchdf()

3. GROUP BY — Regrouper les données

Voir le code
# GROUP BY — Nombre de clients par pays
query = """
SELECT pays, COUNT(*) AS nb_clients
FROM clients
GROUP BY pays
ORDER BY nb_clients DESC
"""
con.execute(query).fetchdf()
Voir le code
# Total des achats par client
query = """
SELECT c.nom, SUM(cmd.montant) AS total_achats
FROM clients c
JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.nom
ORDER BY total_achats DESC
"""
con.execute(query).fetchdf()
Voir le code
# HAVING — Filtrer après agrégation
# Clients ayant dépensé plus de 100€
query = """
SELECT c.nom, SUM(cmd.montant) AS total_achats
FROM clients c
JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.nom
HAVING SUM(cmd.montant) > 100
ORDER BY total_achats DESC
"""
print("🏆 Clients ayant dépensé plus de 100€ :")
con.execute(query).fetchdf()

4. JOIN — Combiner les tables

┌──────────────────────────────────────────────────────────────────┐
│ INNER JOIN (JOIN)         │ Seulement les correspondances      │
│ A ∩ B                      │                                    │
├──────────────────────────────────────────────────────────────────┤
│ LEFT JOIN                 │ Tout A + correspondances B         │
│ A + (A ∩ B)               │ (NULL si pas de correspondance)    │
├──────────────────────────────────────────────────────────────────┤
│ RIGHT JOIN                │ Tout B + correspondances A         │
│ B + (A ∩ B)               │                                    │
├──────────────────────────────────────────────────────────────────┤
│ FULL OUTER JOIN           │ Tout A + Tout B                    │
│ A ∪ B                      │                                    │
└──────────────────────────────────────────────────────────────────┘
Voir le code
# INNER JOIN — Clients avec leurs commandes
query = """
SELECT c.nom, cmd.produit, cmd.montant
FROM clients c
JOIN commandes cmd ON c.id_client = cmd.id_client
ORDER BY c.nom
"""
con.execute(query).fetchdf()
Voir le code
# LEFT JOIN — Tous les clients, même sans commande
query = """
SELECT c.nom, cmd.produit, cmd.montant
FROM clients c
LEFT JOIN commandes cmd ON c.id_client = cmd.id_client
ORDER BY c.nom
"""
print("👀 Remarque : Charlie n'a pas de commande (NULL)")
con.execute(query).fetchdf()
Voir le code
# Trouver les clients sans commande
query = """
SELECT c.nom, c.email
FROM clients c
LEFT JOIN commandes cmd ON c.id_client = cmd.id_client
WHERE cmd.id_commande IS NULL
"""
print("😴 Clients sans aucune commande :")
con.execute(query).fetchdf()

5. Fonctions de date

Fonction Description Exemple
CURRENT_DATE Date du jour 2024-01-15
EXTRACT() Extraire une partie EXTRACT(YEAR FROM date)2024
DATE_TRUNC() Tronquer à une période DATE_TRUNC('month', date)2024-01-01
DATE_DIFF() Différence entre dates DATE_DIFF('day', date1, date2)
Voir le code
# Ventes par mois
query = """
SELECT 
    DATE_TRUNC('month', date_commande) AS mois,
    SUM(montant) AS total_ventes,
    COUNT(*) AS nb_commandes
FROM commandes
GROUP BY DATE_TRUNC('month', date_commande)
ORDER BY mois
"""
print(" Ventes mensuelles :")
con.execute(query).fetchdf()
Voir le code
# Commandes par jour de la semaine
query = """
SELECT 
    EXTRACT(DOW FROM date_commande) AS jour_num,
    CASE EXTRACT(DOW FROM date_commande)
        WHEN 0 THEN 'Dimanche'
        WHEN 1 THEN 'Lundi'
        WHEN 2 THEN 'Mardi'
        WHEN 3 THEN 'Mercredi'
        WHEN 4 THEN 'Jeudi'
        WHEN 5 THEN 'Vendredi'
        WHEN 6 THEN 'Samedi'
    END AS jour,
    COUNT(*) AS nb_commandes
FROM commandes
GROUP BY EXTRACT(DOW FROM date_commande)
ORDER BY jour_num
"""
con.execute(query).fetchdf()

6. CASE — Conditions dans les requêtes

Voir le code
# CASE — Catégoriser les clients
query = """
SELECT 
    c.nom,
    SUM(cmd.montant) AS total_achats,
    CASE
        WHEN SUM(cmd.montant) >= 150 THEN '🥇 Premium'
        WHEN SUM(cmd.montant) >= 100 THEN '🥈 Standard'
        ELSE '🥉 Basique'
    END AS categorie
FROM clients c
JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.nom
ORDER BY total_achats DESC
"""
con.execute(query).fetchdf()

7. CTE (Common Table Expression) — Requêtes lisibles

Les CTEs permettent de créer des “tables temporaires” pour clarifier les requêtes complexes.

WITH nom_cte AS (
    SELECT ...
)
SELECT * FROM nom_cte;
Voir le code
# CTE — Calcul intermédiaire réutilisable
query = """
WITH total_par_client AS (
    SELECT 
        c.id_client,
        c.nom,
        c.pays,
        SUM(cmd.montant) AS total
    FROM clients c
    JOIN commandes cmd ON c.id_client = cmd.id_client
    GROUP BY c.id_client, c.nom, c.pays
)
SELECT 
    nom,
    pays,
    total,
    CASE WHEN total > 100 THEN '✅ VIP' ELSE '❌' END AS vip
FROM total_par_client
ORDER BY total DESC
"""
con.execute(query).fetchdf()

8. Window Functions — Calculs avancés

Les Window Functions permettent de faire des calculs sur un groupe de lignes sans les regrouper.

Fonction Description
ROW_NUMBER() Numéro de ligne
RANK() Classement (avec ex-aequo)
DENSE_RANK() Classement sans saut
LAG() Valeur de la ligne précédente
LEAD() Valeur de la ligne suivante
SUM() OVER() Somme cumulative
Voir le code
# RANK — Classement des clients par total d'achats
query = """
SELECT 
    c.nom,
    SUM(cmd.montant) AS total_achats,
    RANK() OVER (ORDER BY SUM(cmd.montant) DESC) AS rang
FROM clients c
JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.nom
"""
con.execute(query).fetchdf()
Voir le code
# SUM OVER — Total cumulatif
query = """
SELECT 
    date_commande,
    montant,
    SUM(montant) OVER (ORDER BY date_commande) AS cumul
FROM commandes
ORDER BY date_commande
"""
print(" Évolution cumulative des ventes :")
con.execute(query).fetchdf()
Voir le code
# LAG — Comparer avec la période précédente
query = """
WITH mensuel AS (
    SELECT 
        DATE_TRUNC('month', date_commande) AS mois,
        SUM(montant) AS total
    FROM commandes
    GROUP BY DATE_TRUNC('month', date_commande)
)
SELECT 
    mois,
    total,
    LAG(total) OVER (ORDER BY mois) AS total_precedent,
    total - LAG(total) OVER (ORDER BY mois) AS evolution
FROM mensuel
ORDER BY mois
"""
print(" Évolution mensuelle :")
con.execute(query).fetchdf()

9. Intégration avec Pandas

DuckDB s’intègre parfaitement avec Pandas !

Voir le code
import pandas as pd

# Récupérer le résultat en DataFrame Pandas
query = """
SELECT c.nom, c.pays, SUM(cmd.montant) AS total
FROM clients c
JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.nom, c.pays
ORDER BY total DESC
"""

df = con.execute(query).fetchdf()
print(type(df))  # C'est un DataFrame Pandas !
df
Voir le code
# Requêter directement un DataFrame Pandas avec SQL !
df_exemple = pd.DataFrame({
    'produit': ['A', 'B', 'C', 'A', 'B'],
    'ventes': [100, 200, 150, 120, 180]
})

# DuckDB peut requêter le DataFrame directement
result = duckdb.query("""
    SELECT produit, SUM(ventes) AS total
    FROM df_exemple
    GROUP BY produit
    ORDER BY total DESC
""").fetchdf()

print("SQL directement sur un DataFrame Pandas :")
result
Voir le code
# DuckDB peut aussi lire directement des fichiers CSV/Parquet
# Exemple (si tu as un fichier) :
# duckdb.query("SELECT * FROM 'mon_fichier.csv' LIMIT 10")
# duckdb.query("SELECT * FROM 'data.parquet' WHERE date > '2024-01-01'")

print("💡 DuckDB peut lire directement :")
print("   - CSV  : SELECT * FROM 'fichier.csv'")
print("   - Parquet : SELECT * FROM 'fichier.parquet'")
print("   - JSON : SELECT * FROM 'fichier.json'")

10. Exercices pratiques

Essaie de résoudre ces exercices dans les cellules de code ci-dessous !


Exercice 1 — Facile

Afficher tous les clients de France, triés par nom.

💡 Solution
SELECT * FROM clients WHERE pays = 'France' ORDER BY nom;

Exercice 2 — Facile

Calculer le montant moyen des commandes.

💡 Solution
SELECT AVG(montant) AS montant_moyen FROM commandes;

Exercice 3 — Intermédiaire

Afficher le total des achats par client, y compris ceux sans commande (afficher 0).

💡 Solution
SELECT c.nom, COALESCE(SUM(cmd.montant), 0) AS total
FROM clients c
LEFT JOIN commandes cmd ON c.id_client = cmd.id_client
GROUP BY c.nom
ORDER BY total DESC;

Exercice 4 — Intermédiaire

Calculer les ventes par mois avec DATE_TRUNC.

💡 Solution
SELECT DATE_TRUNC('month', date_commande) AS mois, SUM(montant) AS total
FROM commandes
GROUP BY DATE_TRUNC('month', date_commande)
ORDER BY mois;

Exercice 5 — Avancé

Pour chaque client, afficher sa dernière commande (produit et date). Indice : ROW_NUMBER()

💡 Solution
WITH derniere AS (
    SELECT c.nom, cmd.produit, cmd.date_commande,
        ROW_NUMBER() OVER (PARTITION BY c.id_client ORDER BY cmd.date_commande DESC) AS rn
    FROM clients c
    JOIN commandes cmd ON c.id_client = cmd.id_client
)
SELECT nom, produit, date_commande FROM derniere WHERE rn = 1;
Voir le code
# ✏️ Espace pour tes exercices
query = """
-- Écris ta requête ici
SELECT * FROM clients LIMIT 5
"""
con.execute(query).fetchdf()

Quiz


❓ Q1. Quelle commande affiche toutes les colonnes d’une table clients ?

  1. SHOW * FROM clients;
  2. SELECT * FROM clients;
  3. LIST * FROM clients;
  4. DISPLAY * FROM clients;
💡 Réponse bSELECT * FROM clients; affiche toutes les colonnes.

❓ Q2. Quelle est la différence entre WHERE et HAVING ?

  1. Aucune différence
  2. WHERE s’utilise avant GROUP BY, HAVING après
  3. HAVING est plus rapide
  4. WHERE ne peut filtrer que les nombres
💡 Réponse bWHERE filtre avant agrégation, HAVING filtre après.

❓ Q3. Quel JOIN retourne UNIQUEMENT les lignes qui ont une correspondance ?

  1. LEFT JOIN
  2. RIGHT JOIN
  3. INNER JOIN
  4. FULL OUTER JOIN
💡 Réponse cINNER JOIN ne garde que les correspondances.

❓ Q4. À quoi sert une CTE (WITH) ?

  1. Créer une table permanente
  2. Définir une sous-requête réutilisable
  3. Supprimer des données
  4. Créer un index
💡 Réponse b — Les CTEs créent des “tables temporaires” pour clarifier les requêtes.

❓ Q5. Quelle Window Function permet de classer les lignes ?

  1. COUNT()
  2. RANK()
  3. SUM()
  4. GROUP BY
💡 Réponse bRANK() OVER (ORDER BY ...) attribue un classement.

❓ Q6. Quel avantage de DuckDB pour un Data Engineer ?

  1. Il nécessite un serveur
  2. Il peut requêter directement des DataFrames Pandas
  3. Il ne supporte pas SQL standard
  4. Il ne lit pas les fichiers CSV
💡 Réponse b — DuckDB peut requêter directement des DataFrames Pandas avec SQL.

📚 Ressources

🎮 Pratiquer SQL en ligne

🦆 DuckDB

📖 Documentation SQL


➡️ Prochaine étape

Tu maîtrises maintenant SQL et sais l’exécuter depuis Python ! Découvrons les concepts du Big Data et les bases NoSQL.

👉 Module suivant : 08_intro_big_data_nosql — Big Data, traitement distribué et NoSQL


🎉 Félicitations ! Tu as terminé le module SQL pour Data Engineers.

Retour au sommet