Exercice (6 points) — dans l'esprit des sessions 2023–2025, section Sciences de l'informatique
VéloCité est le service municipal de vélos en libre-service de la ville de Sfax. Chaque abonné a choisi une formule d'abonnement et emprunte des vélos stationnés dans les stations de la ville ; chaque vélo est d'un modèle donné et peut faire l'objet de réparations. La base de données simplifiée du service a la représentation graphique suivante :
| Champ | Description |
|---|---|
| idStation | Identifiant d'une station, entier. |
| nomStation | Nom d'une station, chaîne de 20 caractères. |
| quartier | Quartier où se trouve une station, chaîne de 30 caractères. |
| capacite | Nombre maximal de vélos d'une station, entier. |
| codeModele | Code d'un modèle de vélo, chaîne de 4 caractères. |
| libModele | Libellé d'un modèle, chaîne de 30 caractères. |
| typeModele | Type d'un modèle, « Classique » ou « Electrique », chaîne de 10 caractères. |
| tarifMinute | Prix d'une minute de trajet, en dinars, réel. |
| idVelo | Identifiant d'un vélo, entier. |
| dateService | Date de mise en service d'un vélo. |
| codeFormule | Code d'une formule d'abonnement, chaîne de 3 caractères. |
| libFormule | Libellé d'une formule (Journalier, Mensuel, Annuel…), chaîne de 20 caractères. |
| prixMensuel | Prix mensuel d'une formule, en dinars, réel. |
| numAbonne | Numéro d'un abonné, entier. |
| nomAbonne | Nom et prénom d'un abonné, chaîne de 40 caractères. |
| telAbonne | Téléphone d'un abonné, chaîne de 8 caractères. |
| emailAbonne | Adresse mail d'un abonné, chaîne de 50 caractères. |
| idTrajet | Identifiant d'un trajet, entier. |
| dateTrajet | Date d'un trajet (sans heure). |
| dureeMin | Durée d'un trajet, en minutes, entier. |
| montant | Montant payé pour un trajet, en dinars, réel. |
| dateRep | Date d'une réparation. |
| panne | Description de la panne réparée, chaîne de 50 caractères. |
| cout | Coût d'une réparation, en dinars, réel. |
On suppose que la base de données est déjà créée et contient des données. Écrire en SQL les requêtes permettant de répondre aux questions suivantes :
- Quels sont les abonnés ayant emprunté au moins une fois un vélo de type « Electrique » ? On affichera, sans répétition, les champs nomAbonne et telAbonne.
- Quels sont les trajets effectués entre le 1er et le 15 mars 2026 dont la durée dépasse 40 minutes ? On affichera les champs idTrajet, dateTrajet et dureeMin.
- Quels sont les vélos stationnés dans le quartier « Sfax Jadida » ? On affichera les champs idVelo, libModele et nomStation ; la liste sera triée de la mise en service la plus récente à la plus ancienne.
- Quel est le nombre de vélos stationnés dans chaque station ? On affichera les champs idStation et nomStation ainsi que le nombre de vélos.
- Quels sont les abonnés dont le montant total des trajets effectués en 2025 dépasse strictement 60 dinars ? On affichera les champs numAbonne et nomAbonne ainsi que ce montant total.
- Quels sont les abonnés qui n'ont effectué aucun trajet au cours du mois de janvier 2026 ? On affichera les champs numAbonne et nomAbonne.
- Quels sont les trajets ayant la durée la plus longue ? On affichera les champs idTrajet, nomAbonne et dureeMin.
- Quelles sont les réparations dont le coût dépasse le coût moyen de toutes les réparations ? On affichera les champs idVelo, dateRep, panne et cout.
Voir la correction commentéeAprès avoir posé votre démarche
1. Idée : le type est dans Modele, l'abonné dans Abonne ; aucune colonne commune : on traverse Trajet et Velo, dont rien n'est affiché. Un abonné qui a fait plusieurs trajets électriques apparaîtrait plusieurs fois, d'où DISTINCT (« sans répétition »).
SELECT DISTINCT nomAbonne, telAbonne
FROM Abonne A, Trajet T, Velo V, Modele M
WHERE A.numAbonne = T.numAbonne
AND T.idVelo = V.idVelo
AND V.codeModele = M.codeModele
AND typeModele = 'Electrique';Quatre tables dans le FROM, donc trois conditions de jointure (clé étrangère = clé primaire).
2. Idée : dateTrajet est une date sans heure : BETWEEN inclut les deux bornes, le 15 mars compris.
SELECT idTrajet, dateTrajet, dureeMin
FROM Trajet
WHERE dateTrajet BETWEEN '2026-03-01'
AND '2026-03-15'
AND dureeMin > 40;Équivalent : dateTrajet >= '2026-03-01' AND dateTrajet <= '2026-03-15'.
3. Idée : le quartier est dans Station et le libellé dans Modele : deux jointures autour de Velo. « De la plus récente à la plus ancienne » : tri décroissant sur dateService, même si ce champ n'est pas affiché.
SELECT idVelo, libModele, nomStation
FROM Velo V, Modele M, Station S
WHERE V.codeModele = M.codeModele
AND V.idStation = S.idStation
AND quartier = 'Sfax Jadida'
ORDER BY dateService DESC;4. Idée : « le nombre … de chaque station » : un groupe par station. Les colonnes affichées sans fonction d'agrégat (idStation, nomStation) figurent toutes dans le GROUP BY.
SELECT S.idStation, nomStation,
COUNT(idVelo) AS nbVelos
FROM Station S, Velo V
WHERE S.idStation = V.idStation
GROUP BY S.idStation, nomStation;idStation existe dans Station et dans Velo : elle est préfixée. La jointure ne garde que les couples station–vélo existants : une station vide n'apparaît pas.
5. Idée : l'année porte sur chaque trajet, c'est un filtre de lignes (WHERE, avant le regroupement) ; « total > 60 » porte sur une somme calculée par abonné, c'est un filtre de groupes (HAVING, après le regroupement).
SELECT A.numAbonne, nomAbonne,
SUM(montant) AS total
FROM Abonne A, Trajet T
WHERE A.numAbonne = T.numAbonne
AND YEAR(dateTrajet) = 2025
GROUP BY A.numAbonne, nomAbonne
HAVING SUM(montant) > 60;6. Idée : « aucun trajet » : on construit d'abord la liste des abonnés qui ont roulé en janvier 2026 (sous-requête), puis on garde ceux qui n'y figurent pas.
SELECT numAbonne, nomAbonne
FROM Abonne
WHERE numAbonne NOT IN (
SELECT numAbonne FROM Trajet
WHERE YEAR(dateTrajet) = 2026
AND MONTH(dateTrajet) = 1);Une jointure écrite avec A.numAbonne <> T.numAbonne ne convient pas : elle associe chaque abonné aux trajets des autres et le ferait apparaître dès que quelqu'un d'autre a roulé en janvier.
7. Idée : « la plus longue » : chaque durée est comparée au maximum calculé par une sous-requête ; si plusieurs trajets sont à égalité, ils sont tous affichés.
SELECT idTrajet, nomAbonne, dureeMin
FROM Trajet T, Abonne A
WHERE T.numAbonne = A.numAbonne
AND dureeMin = (SELECT MAX(dureeMin)
FROM Trajet);WHERE dureeMin = MAX(dureeMin) est refusé (fonction d'agrégat dans WHERE), et SELECT idTrajet, nomAbonne, MAX(dureeMin) … ne répond pas : MAX résume toutes les lignes en une seule et le nom affiché n'est pas forcément celui du trajet le plus long (MySQL, en mode ONLY_FULL_GROUP_BY activé par défaut, refuse même cette requête).
8. Idée : le coût moyen n'est pas connu à l'avance : une sous-requête AVG le calcule, puis chaque coût lui est comparé.
SELECT idVelo, dateRep, panne, cout
FROM Reparation
WHERE cout > (SELECT AVG(cout)
FROM Reparation);