Académique Documents
Professionnel Documents
Culture Documents
Correction TP1 Q1 A 5
Correction TP1 Q1 A 5
Exercice 2
Le sujet de cet exercice est l'oeuvre de Cécile CAPPONI, tout comme la majorité de cette correction.
Certaines questions vous ont été épargnées dans l'énoncé du TD, vous les trouverez toutefois
dans cette correction.
Pour chaque question on donne sa réponse sous forme de langage algébrique puis en SQL en
police "courier". Pour la plupart des questions, il peut exister plusieurs solutions algébriques
(puisque certains opérateurs s'expriment en fonction d'autres), et plusieurs solutions SQL. Nous
donnons ici une solution pour chaque question, et parfois deux solutions pour montrer des
différences importantes.
Pour bien comprendre les requêtes que vous jugez difficiles, un bon moyen est de spécifier, à
chaque étape de l'expression algébrique, les relations mises en jeu (avec leurs attributs).
*) Lister les clients dont le nom commence par 'H' (la fonction START(string, char) renvoie TRUE si la
chaîne string commence par le caractère char).
On applique une restriction sur le nom des clients, en appliquant à chaque nom une fonction pour
vérifier la condition de restriction.
RESULTAT = RESTRICT(client, START(nom, 'H'))
En SQLite, la fonction START n'est pas implémenté : on s'en sors avec LIKE :
SELECT *
FROM client
WHERE nom LIKE 'H%';
Pour des raisons historiques et d'efficacité, certains développeurs SQL n'utilisent pas explicitement la
primitive JOIN en SQLite. A la place ils font un produit cartésien qu'ils restreignent par la suite :
SELECT hotel.numhotel, nom, ville, etoiles
FROM hotel, reservation --produit catésien
WHERE hotel.numhotel = reservation.numhotel;
A l'exécution de cette requête, on s'aperçoit que le même hôtel peut revenir plusieurs fois en
résultat : ceci est dû au fait qu'il peut faire l'objet de plusieurs réservations, et comme le produit
cartésien (donc la jointure) est effectué avant la projection, la table intermédiaire obtenue par la
jointure contient autant de fois un même hôtel qu'il y a de réservations dans cet hôtel. Ensuite, la
projection de cette table intermédiaire est faite sur les attributs de la table hotel, mais tous les
tuples, non identiques dans la table intermédiaire (car associant un hôtel à une réservation),
peuvent devenir identiques dans la table résultat puisque ne sont gardés que les attributs de la
table hotel (donc plus de trace des réservations). Pour vous en persuader, ajoutez l'attribut numresa
dans la projection : on s'aperçoit bien que tous les tuples de la table intermédiaire sont distincts les
uns des autres.
Pour éviter ce petit problème, il existe la clause DISTINCT qui permet, après projection, d'éliminer
tous les tuples identiques pour n'en garder qu'un exemplaire de chacun. Notre requête devient
alors :
4) Lister les clients ayant déjà occupé (ou en cours d'occupation d') une chambre.
Tout comme dans la précédente nous pouvons rajouter la directive DISTINCT pour ne garder en
résultat qu'un seul exemplaire de chaque client, sinon chaque client revient autant de fois qu'il a
occupé de chambres.
*) Lister les clients n'ayant pas occupé (ou pas en cours d'occupation d') une chambre.
Soit R1 le résultat la question précédente. Il suffit ici de considérer tous les clients de la base, et
d'enlever ceux qui ont déjà occupé une chambre : il reste alors ceux qui n'en ont jamais occupé.
Attention, SQLite ne gère pas les parenthèses de façon optimal, en particulier on ne peut pas avoir
de parenthèses entre les tables des opérateurs ensemblistes (UNION, EXCEPT, INTERSECT).
5) Pour chaque hôtel, indiquer le nombre total de chambres
On groupe le résultat de la jointure entre hotel et chambre par hôtel (numéro, nom et ville). On obtient
alors, pour chaque hôtel, l'ensemble des chambres qui y sont : il suffit alors de compter le nombre
de numchambre, unique à l'intérieur de chaque hôtel.
*) Pour chaque hôtel, quel est le prix moyen de ses chambres ? Le prix maximum ? Le prix
minimum ?
On retrouve pour cette question la relation R1 de la question 9 ; mais la fonction d'agrégat diffère,
et s'applique non pas sur le numéro de la chambre, mais sur le prix de la chambre.
Pour avoir le minimum (resp. le maximum) du prix, on remplace la fonction de calcul de l'agrégat
par MIN (resp. MAX). Même chose dans la requête SQL.
6) Quels sont tous ceux qui ont été, ou sont encore, clients de l'hôtel des voyageurs à Nice ?
Pour la première question, il s'agit de repérer les clients dont le numéro est référencé dans une
occupation de chambre d'un hôtel précis. Pour cela, on commence par ne garder que l'hôtel en
question (son numéro suffit: R2) puis on doit faire le lien entre cet hôtel et les occupations qui le
concerne (R3), et à partir de là, on peut faire une jointure pour mettre en relation chaque client
avec la chambre occupée.
Comme dans la question 3, on remarque, à la vue du résultat, qu'un même client peut
apparaître plusieurs fois, et c'est encore pour la même raison. Afin d'éliminer les doublons, il suffit
de spécifier un DISTINCT sur la projection englobante.
SELECT COUNT(triple.numchambre)
FROM ( SELECT *
FROM chambre
WHERE type = 'triple' ) AS triple
JOIN ( SELECT *
FROM hotel
WHERE nom = 'Hotel des ambassadeurs'
AND ville = 'Grenoble') AS ambassadeur
ON triple.numhotel = ambassadeur.numhotel;
On groupe selon chaque hôtel restreint à Bordeaux, et on compte dans chaque groupe, le nombre
de numéros d'hôtel trouvés dans ce groupe.
*) Pour chaque chambre de chaque hôtel, compter le nombre de clients y ayant séjourné, ou y
séjournant encore.
On groupe par chambre d'hôtel, on récupère les clients de chaque groupe par jointure, et on
compte le numéro de client dans chaque groupe (un groupe étant une chambre).
8) Pour chaque client, lister le nom des hôtels dans lesquels il a séjourné, ou séjourne encore,
avec les dates d'arrivée et de départ correspondant à chaque séjour.
Il faut ici faire attention aux jointures naturelles car deux attributs ont le même identificateur mais
n'ont rien à voir : le nom de l'hôtel et le nom du client. Pour lever l'ambiguïté, nous précisons d'où
vient l'attribut en le préfixant par une variable liée à la table correspondante. Dans la requête SQL,
lever cette ambiguïté est plus aisé, puisque nous devons renommer les tables intermédiaires qui
sont arguments de jointure.
9) Lister les numéros de chambres disponibles dans l'hôtel de la gare, à Bordeaux, pour les nuits
du 12 avril 2014 au 17 avril 2014 .
Nous devons ici répertorier les chambres qui ne sont ni occupées ni réservées pendant une
période considérée. On répertorie donc toutes les chambres occupées -- ou réservées -- pendant
cette période, et on retire ces dernières de la liste de toutes les chambres de l'hôtel de la base de
données.
-- ci-dessous, on se restreint à l'hôtel de la gare de Bordeaux
R1 = RESTRICT( hotel, nom = 'Hotel de la gare' AND ville = 'Bordeaux' )
-- ci-dessous, on répertorie toutes les occupations de cet hôtel
R2 = NATURAL JOIN( R1, occupation)
-- ci-dessous, on répertorie toutes les réservations de cet hôtel
R'2 = NATURAL JOIN( R1, reservation)
-- ci-dessous, on sélectionne toutes les occupations de l'hôtel pendant la période considérée
R3 = RESTRICT ( R2, (17/04/2014 >= datedepart > 12/04/2014) OR (17/04/2014 > datearrivee >= 12/04/2014)
OR ( datearrivee <= 12/04/2014 AND (datedepart >= 17/04/2014 OR datedepart IS NULL ) ) )
R'3 = PROJECT (R3, numhotel, numchambre)
-- ci-dessous, on sélectionne toutes les réservations de l'hôtel pendant la période considérée
R4 = RESTRICT ( R'2, (17/04/2014 >= datedepart > 12/04/2014) OR (17/04/2014 > datearrivee >= 12/04/2014)
OR ( datearrivee <= 12/04/2014 AND (datedepart >= 17/04/2014 OR datedepart IS NULL ) ) )
R'4 = PROJECT (R4, numhotel, numchambre)
-- ci dessous, on fait de l'union de touotes les chambres occupées ou réservées pendant la période
R5 = UNION(R'3, R'4) -- on s'assure que les schémas de R'3 et R'4 sont identiques
-- ci-dessous, on garde toutes les chambres de l'hôtel sans critère de date
R6 = NATURAL JOIN( R1, chambre )
R'6 = PROJECT ( R6, numhotel, numchambre )
-- ci-dessous, on retire de toutes les occupations et résa celles qui sont pendant la période considérée
R7 = MINUS (R'6, R5) -- on s'assure que les schémas de R'6 et R5 sont identiques
-- On projette sur le numéro de la chambre
RESULTAT = PROJECT( R7, numchambre )
Il faut répertorier les séjour du client précisé. Nous commençons par repérer le client en question
(dans R1). puis nous passons par la relation occupation pour récupérer toutes les chambres que ce
client a occupées avant le 02/04/2014 (R2 puis R3), et nous passons enfin par la relation chambre
pour obtenir le prix de la chambre occupée dans le séjour concerné (jointure R4), et enfin nous
projetons le résultat obtenu par hôtel, chambre, client, dates d'arrivée et de départ (tout cela
constitue un "séjour"), et nous faisons un calcul sur ces attributs pour calculer le montant de la
facture TTC (le prix des chambres est HT).
11) Quelles sont les chambres (hôtel et numéro) qui ont été occupées plus de 40 jours ? Quels
sont les clients qui ont occupé toutes ces chambres ?
Il suffit de compter, pour chaque chambre jointe avec les occupations de cette chambre, le nombre
de jours d'occupation en sommant sur la différence départ-arrivée de chaque séjour. On ne garde
alors que ces chambres dont le nombre de jours calculés en R2 dépasse 40.
Pour la seconde question, il suffit de réaliser une jointure entre le résultat de la requête précédente
et les clients, via les occupations enregistrées. On utilise ainsi deux fois la même relation
occupation, mais pour des objectifs différents (pour repérer les chambres très utilisées d'une part, et,
ensuite, pour repérer les clients qui ont séjourné dans ces chambres, même pour une très courte
durée.
R1 = PROJECT ( occupation, numchambre, numhotel, (datedepart - datearrivee) nbj )
R2 = AGREGAT( R1 ; numchambre, numhotel, numclient ; SUM(nbj) totaljours )
R3 = RESTRICT( R2, totaljours >=40 )
R4 = NATURAL JOIN( client, occupation)
R5 = NATURAL JOIN(R4, R3)
RESULTAT = PROJECT(R5, numclient, nom, prenom)
Ce qui donne, en SQL (et en reprenant la requête donnée précédente pour répondre à la première
partie de la question) :
SELECT cli.numclient, cli.nom, cli.prenom
FROM ( SELECT numhotel, numchambre, SUM(nbj) AS totaljours
FROM ( SELECT numhotel, numchambre,
julianday(datedepart)-julianday(datearrivee) AS nbj
FROM occupation
) AS sejour
GROUP BY numhotel, numchambre
) AS sejourtotal
JOIN ( SELECT client.numclient, nom, prenom, numchambre, numhotel
FROM client
JOIN occupation ON occupation.numclient = client.numclient) AS cli
ON (cli.numchambre = sejourtotal.numchambre
AND cli.numhotel = sejourtotal.numhotel )
WHERE totaljours >= 40;
*) Quels sont les clients (nom et prénom) qui ont toujours séjourné au premier étage ?
Pour répondre à ce type de question où apparaît un « toujours » (et donc, un « jamais » sur une
condition opposée), l'opérateur de différence est le plus pratique (comme dans la question 8) : on
répond à la question « quels sont les clients qui ont occupé une chambre ailleurs qu'au premier
étage », et on enlève ceux-là de la liste de tous les clients ayant déjà occupé une chambre.
Concernant cette question comme pour beaucoup d'autres, il est possible d'y répondre sans
passer par des requêtes imbriquées, mais c'est alors moins efficace à l'exécution (même si,
dans le TP, cela n'est pas trop visible car la base de données GestionHotel n'est pas grande). Par
exemple, pour cette dernière question, nous pouvons écrire (c'est le second terme de la différence
qui change) :
SELECT client.numclient, nom, prenom
FROM client
JOIN occupation ON occupation.numclient = client.numclient
EXCEPT
SELECT client.numclient, nom, prenom
FROM occupation
JOIN chambre ON ( occupation.numchambre = chambre.numchambre
AND occupation.numhotel = chambre.numhotel )
JOIN client ON client.numclient = occupation.numclient
WHERE etage!= 1;
Cette expression de requête est peut-être plus simple à lire, est correcte, mais encore une fois,
dans le cadre de grandes bases de données, elle ne s'avère guère efficace car la restriction sur
l'étage de la chambre est faite après la jointure, alors que la faire avant (comme dans l'expression
précédente) restreint considérablement le nombre de tuples à joindre. Mais pour faire les
restrictions le plus en aval possible, il faut imbriquer des requêtes.
Faisons une remarque générale sur les ambiguïtés d'attributs, à partir de l'exemple de cette
requête. Travaillons pour cela sur la première des requêtes SQL précédentes pour répondre à
cette question. Par exemple dans le premier SELECT, nous projetons typiquement la relation
obtenue sur les attributs de la relation client. Mais, dans le cas de l'attribut numclient, si nous
omettons de le préfixer par la relation client (ou par la relation occupation...), l'interpréteur SQL nous
gratifie d'une erreur peu ambigüe : ERROR: column reference "numclient" is
ambiguous. Or, nous pourrions penser que, dans la mesure où nous faisons une jointure sur
l'égalité des colonnes numclient de client et d'occupation, il n'y a plus d'ambiguïté sur la provenance de
cet attribut. En réalité, l'interpréteur ne va pas si loin dans l'unification des noms d'attributs : il est
donc nécessaire, même en cas de jointure naturelle, de préciser de quelle relation vient l'attribut à
partir du moment où plusieurs relations de la même expression algébrique, dans le même espace
de noms, possèdent un attribut de même nom.