Cours BUT Info

TP 3.01 — Premiers pas avec PostgreSQL

Prendre en main une base PostgreSQL : se connecter avec psql, écrire un script SQL ré-exécutable, insérer des données et répondre à neuf requêtes — corrigées et expliquées une par une.

Objectif du TP : prendre en main une base PostgreSQL, et pratiquer les ordres classiques du DDL (création de la structure) et du DML (insertion et interrogation des données).

Le support de ce TP est court — deux pages. C'est normal : un TP ne s'apprend pas en le lisant, il s'apprend en le tapant. Cette page reprend donc chaque consigne et le raisonnement qui mène à la commande.

1. PostgreSQL, c'est quoi ?

PostgreSQL est un SGBD relationnel : un logiciel qui stocke des données sous forme de tables et qui répond à des requêtes SQL. Il est réputé très robuste — il ne perd pas de données, même en cas d'arrêt brutal — et très complet.

Le support insiste sur un point de vocabulaire, parce que ces trois adjectifs sont constamment confondus.

L'enseignant insiste sur ce point : ces trois adjectifs sont souvent confondus, et pourtant aucun n'implique les autres. PostgreSQL cumule les trois, ce qui est rare.

Les trois liens à connaître

  • Le site officiel : postgresql.org
  • La documentation en français : docs.postgresqlfr.org
  • Le SGBD est déjà installé sur la machine psqlserv de l'IUT — c'est celle que vous utiliserez.

Travailler ailleurs que sur psqlserv

Il est tout à fait possible d'installer PostgreSQL sur sa propre machine, d'utiliser un serveur du réseau local, ou de créer une base « dans le cloud ». Quelques services cités par le support :

ServiceRemarque
Clever CloudOffre un plan de test gratuit, suffisant pour les exercices.
HerokuIdem — plan gratuit adapté à un usage pédagogique.
ElephantSQLIdem, spécialisé PostgreSQL.
Amazon AWS, Google Cloud, Microsoft AzureLes trois grands fournisseurs de cloud ; les offres gratuites sont plus limitées mais existent.

2. Se connecter à la base

Sur psqlserv, une instance de PostgreSQL est installée, et une base nommée but2 est à votre disposition. Un espace y est réservé à chacun, avec un compte à votre login habituel et le mot de passe moi.

Le client en ligne de commande s'appelle psql. La commande de connexion se décompose en quatre morceaux, et il vaut la peine de les connaître séparément.

La commande de connexion se lit comme une phrase : « avec le programme psql, sur la machine psqlserv, avec le compte prenomnometu, ouvre la base but2 ».
La commande, dans votre cas
psql -h psqlserv -U prenomnometu but2

La syntaxe générale, à retenir sous cette forme, est toujours la même :

psql -h nomMachine -U login nomBase

3. Les commandes élémentaires une fois connecté

Une fois la connexion établie, psql affiche une invite qui change de forme : elle devient but2=>. C'est le signal que le programme attend vos ordres.

psql est un programme interactif : on écrit, on appuie sur Entrée, on lit la réponse, on recommence. Le point-virgule est ce qui dit « j'ai fini d'écrire mon ordre ».

Il faut distinguer deux familles de commandes, et ne jamais les mélanger :

  • les ordres SQL, qui se terminent par un point-virgule ; et qui sont envoyés au serveur ;
  • les méta-commandes de psql, qui commencent par un antislash \ et qui concernent l'outil lui-même, pas la base. Elles n'ont pas besoin de point-virgule.
CommandeSignification
\?La liste des commandes psql disponibles
\hLa liste des ordres SQL
\h unOrdreLa description et la syntaxe d'un ordre précis
\qQuitte psql
\dListe les tables de la base
\d nomtableDécrit la structure d'une table
\i fichierExécute les commandes contenues dans le fichier

Les deux commandes à connaître par cœur

\d pour vérifier ce qui existe, \i pour exécuter un fichier. Entre les deux, on couvre 90 % du travail de ce TP.

4. Exercice 1 — créer les tables du club LaRaquette

La consigne : « Les ordres DDL de création SQL des tables du TD 3.01 dans un fichier, que vous interpréterez sous psql (\i). Préférez varchar(15) à text pour faciliter les traitements ultérieurs. Faites en sorte que votre fichier puisse être ré-exécuté plusieurs fois. »

Pourquoi un fichier plutôt que des commandes tapées à la main

Le TP demande un fichier ré-exécutable plusieurs fois. C'est la raison d'être du DROP TABLE IF EXISTS ... CASCADE placé en tête de script : il remet tout à zéro avant de recréer.

Un fichier se relit, se corrige, se rejoue et se partage. Une commande tapée dans un terminal disparaît dès qu'on ferme la fenêtre — et surtout, elle ne se rejoue pas. Sur un projet réel, tous les scripts de création de base vivent dans des fichiers versionnés avec le code.

Pourquoi varchar(15) plutôt que text

text accepte une chaîne de longueur quelconque : le SGBD ne peut rien prévoir. varchar(15) borne la longueur à 15 caractères, ce qui :

  • documente ce qu'on attend dans la colonne (un nom, un prénom, un libellé court) ;
  • permet au SGBD de réserver la place de façon plus efficace ;
  • fait échouer immédiatement une insertion aberrante, plutôt que de laisser entrer une chaîne de 4 000 caractères qu'il faudra nettoyer plus tard.

Rendre le fichier ré-exécutable

C'est le mot idempotent : une opération idempotente donne le même résultat qu'on l'exécute une fois ou dix fois. Ici, cela s'obtient en plaçant en tête du script une remise à zéro :

DROP TABLE IF EXISTS reservation, creneau, abonne, terrain, sport CASCADE;

Sans IF EXISTS, la deuxième exécution s'arrêterait sur une erreur. Sans CASCADE, PostgreSQL refuserait de détruire terrain tant que creneau la référence.

Le fichier complet

creer_laraquette.sql
-- Remise à zéro : le script peut être relancé autant de fois qu'on veut.
DROP TABLE IF EXISTS reservation, creneau, abonne, terrain, sport CASCADE;

CREATE TABLE sport (
    noSport   INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    libelle   VARCHAR(15) NOT NULL UNIQUE
);

CREATE TABLE terrain (
    noTerrain INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    nom       VARCHAR(15) NOT NULL UNIQUE,
    noSport   INTEGER NOT NULL,
    CONSTRAINT fk_terrain_sport FOREIGN KEY (noSport)
        REFERENCES sport (noSport)
        ON DELETE RESTRICT ON UPDATE CASCADE
);

CREATE TABLE creneau (
    noCreneau   INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    dateCreneau DATE    NOT NULL,
    heureDebut  INTEGER NOT NULL,
    noTerrain   INTEGER NOT NULL,
    CONSTRAINT ck_creneau_heure  CHECK (heureDebut >= 10 AND heureDebut <= 20),
    CONSTRAINT uq_creneau        UNIQUE (noTerrain, dateCreneau, heureDebut),
    CONSTRAINT fk_creneau_terrain FOREIGN KEY (noTerrain)
        REFERENCES terrain (noTerrain)
        ON DELETE RESTRICT ON UPDATE CASCADE
);

CREATE TABLE abonne (
    noAbo         INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    nom           VARCHAR(15) NOT NULL,
    prenom        VARCHAR(15) NOT NULL,
    dateNaissance DATE        NOT NULL,
    dateDebutAbo  DATE        NOT NULL DEFAULT CURRENT_DATE,
    dateFinAbo    DATE        NOT NULL DEFAULT (CURRENT_DATE + INTERVAL '1 year'),
    noParrain     INTEGER,
    CONSTRAINT ck_abonne_dates CHECK (dateFinAbo > dateDebutAbo),
    CONSTRAINT fk_abonne_parrain FOREIGN KEY (noParrain)
        REFERENCES abonne (noAbo)
        ON DELETE RESTRICT ON UPDATE CASCADE
);

CREATE TABLE reservation (
    noResa    INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    utilise   BOOLEAN NOT NULL DEFAULT false,
    noCreneau INTEGER NOT NULL,
    noAboResp INTEGER NOT NULL,
    noAboPart INTEGER NOT NULL,
    CONSTRAINT ck_resa_distincts CHECK (noAboResp <> noAboPart),
    CONSTRAINT uq_resa_creneau  UNIQUE (noCreneau),
    CONSTRAINT fk_resa_creneau FOREIGN KEY (noCreneau)
        REFERENCES creneau (noCreneau)
        ON DELETE RESTRICT ON UPDATE CASCADE,
    CONSTRAINT fk_resa_resp FOREIGN KEY (noAboResp)
        REFERENCES abonne (noAbo)
        ON DELETE RESTRICT ON UPDATE CASCADE,
    CONSTRAINT fk_resa_part FOREIGN KEY (noAboPart)
        REFERENCES abonne (noAbo)
        ON DELETE RESTRICT ON UPDATE CASCADE
);

Vérifier que ça a marché

Dans psql
but2=> \i creer_laraquette.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE

but2=> \d
              List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+---------
 public | abonne      | table | yann
 public | creneau     | table | yann
 public | reservation | table | yann
 public | sport       | table | yann
 public | terrain     | table | yann

but2=> \d creneau

\d liste les tables ; \d nomtable montre les colonnes, les types et les contraintes d'une table précise. Prendre l'habitude de vérifier après chaque création évite de chercher longtemps pourquoi une requête échoue alors qu'une colonne n'a jamais été créée.

5. Exercice 2 — la première inscription

La consigne : « Rien n'est encore saisi dans les tables. On souhaite maintenant inscrire Paul Durand, né le 12/07/2002, pour une première réservation avec Jean Leclercq, né le 23/02/2000, sur le terrain 1 de Tennis le 16/09 à 14h. Donnez les ordres nécessaires à cette inscription. »

Le raisonnement

Ce qui est demandé, en clair : créer deux abonnés, puis une réservation qui les relie à un créneau.

On ne peut pas écrire une clé étrangère qui ne pointe nulle part. C'est pour cette raison que l'ordre des INSERT suit exactement l'ordre des CREATE TABLE.

Créer les deux abonnés. Paul Durand est le responsable (celui qui réserve), Jean Leclercq est le partenaire invité. Les deux doivent exister dans la table abonne avant toute réservation.

Créer le créneau. Une réservation ne contient ni date ni heure : elle renvoie vers un créneau. Il faut donc créer la ligne « terrain 1 de Tennis, le 16/09, à 14h ».

Créer la réservation. Elle référence le créneau et les deux abonnés, par leurs numéros.

Les ordres

Les sports d'abord (sinon aucune table n'est exploitable)
INSERT INTO sport (libelle) VALUES ('Tennis'), ('Squash'), ('Badminton');
Les deux abonnés
INSERT INTO abonne (nom, prenom, dateNaissance, dateDebutAbo, dateFinAbo)
VALUES
  ('Durand',   'Paul', DATE '2002-07-12', DATE '2026-09-01', DATE '2027-09-01'),
  ('Leclercq', 'Jean', DATE '2000-02-23', DATE '2026-09-01', DATE '2027-09-01');
Le terrain 1 de Tennis, puis le créneau du 16/09 à 14h
INSERT INTO terrain (nom, noSport)
VALUES ('T1', (SELECT noSport FROM sport WHERE libelle = 'Tennis'));

INSERT INTO creneau (dateCreneau, heureDebut, noTerrain)
VALUES (
    DATE '2026-09-16',
    14,
    (SELECT noTerrain FROM terrain WHERE nom = 'T1')
);
La réservation
INSERT INTO reservation (noCreneau, noAboResp, noAboPart)
VALUES (
    (SELECT noCreneau FROM creneau
      WHERE dateCreneau = DATE '2026-09-16'
        AND heureDebut  = 14
        AND noTerrain   = (SELECT noTerrain FROM terrain WHERE nom = 'T1')),
    (SELECT noAbo FROM abonne WHERE nom = 'Durand'),
    (SELECT noAbo FROM abonne WHERE nom = 'Leclercq')
);

Deux pièges dans cet exercice

1. L'ordre compte. Insérer la réservation avant le créneau produit une erreur de clé étrangère : Key (noCreneau)=(1) is not present in table « creneau ». Ce n'est pas une malchance, c'est le SGBD qui fait son travail.

2. Ne jamais écrire les numéros en dur. VALUES (1, 2, 3) fonctionne tant que personne d'autre n'a inséré de ligne avant vous. Avec les sous-requêtes ci-dessus, la commande reste correcte quel que soit l'état de la base.

6. Exercice 3 — peupler la base

La consigne : « Saisir 6 salles, saisir 5 clients abonnés, saisir 5 créneaux par jour ouvré de la semaine prochaine et quelques réservations. »

« Salles » ou « terrains » ?

L'énoncé parle de « salles », alors que le modèle du club parle de terrains. Les deux désignent la même chose ici : lisez « 6 terrains ».

Les sports et les terrains

Les trois sports
INSERT INTO sport (libelle) VALUES ('Tennis'), ('Squash'), ('Badminton');
Six terrains, répartis sur les trois sports
INSERT INTO terrain (nom, noSport) VALUES
  ('T1', (SELECT noSport FROM sport WHERE libelle = 'Tennis')),
  ('T2', (SELECT noSport FROM sport WHERE libelle = 'Tennis')),
  ('S1', (SELECT noSport FROM sport WHERE libelle = 'Squash')),
  ('S2', (SELECT noSport FROM sport WHERE libelle = 'Squash')),
  ('B1', (SELECT noSport FROM sport WHERE libelle = 'Badminton')),
  ('B2', (SELECT noSport FROM sport WHERE libelle = 'Badminton'));

Cinq abonnés, dont deux parrainés

Cinq abonnés — le parrainage se fait après coup, par une mise à jour
INSERT INTO abonne (nom, prenom, dateNaissance, dateDebutAbo, dateFinAbo) VALUES
  ('Dupont',  'Jean',   DATE '1998-04-12', DATE '2026-09-01', DATE '2027-09-01'),
  ('Durand',  'Paul',   DATE '2002-07-12', DATE '2026-09-01', DATE '2027-09-01'),
  ('Martin',  'Lucie',  DATE '1995-11-30', DATE '2026-09-01', DATE '2027-09-01'),
  ('Bernard', 'Karim',  DATE '2001-01-05', DATE '2026-09-01', DATE '2027-09-01'),
  ('Petit',   'Emma',   DATE '1999-08-21', DATE '2026-09-01', DATE '2027-09-01');

-- Lucie et Karim ont été parrainés par Jean (noAbo = 1)
UPDATE abonne SET noParrain = 1 WHERE nom IN ('Martin', 'Bernard');
-- Emma a été parrainée par Paul (noAbo = 2)
UPDATE abonne SET noParrain = 2 WHERE nom = 'Petit';

Pourquoi deux UPDATE plutôt qu'un seul INSERT ?

Parce qu'un abonné ne peut pas être son propre parrain au moment où il est créé : la clé étrangère noParrain doit pointer vers une ligne déjà existante. On insère donc les personnes, puis on renseigne les parrainages. C'est un cas très fréquent, et une bonne raison pour que la colonne noParrain accepte NULL.

Cinq créneaux par jour ouvré

Saisir « à la main » cinq créneaux pour chacun des cinq jours ouvrés de la semaine prochaine ferait 25 lignes à taper. C'est exactement le genre de tâche qu'un SGBD fait mieux que nous.

Générer les créneaux avec generate_series
INSERT INTO creneau (dateCreneau, heureDebut, noTerrain)
SELECT d::date,                       -- la date du jour généré
       9 + n,                         -- 10h, 11h, 12h, 13h, 14h
       (SELECT noTerrain FROM terrain WHERE nom = 'T1')
FROM generate_series(1, 5) AS n       -- cinq créneaux par jour
CROSS JOIN generate_series(
         date_trunc('week', CURRENT_DATE) + INTERVAL '7 days',   -- lundi prochain
         date_trunc('week', CURRENT_DATE) + INTERVAL '11 days',  -- vendredi prochain
         INTERVAL '1 day'
       ) AS d
WHERE EXTRACT(ISODOW FROM d) BETWEEN 1 AND 5;   -- 1 = lundi … 5 = vendredi

Les fonctions employées méritent d'être présentées :

ÉlémentCe qu'il fait
generate_series(début, fin, pas)Fabrique une suite de valeurs — ici des dates, jour par jour.
date_trunc('week', …)Ramène une date au lundi de sa semaine.
EXTRACT(ISODOW FROM d)Donne le numéro du jour dans la semaine : 1 = lundi, 7 = dimanche.
CROSS JOINAssocie chaque ligne de gauche à chaque ligne de droite : 5 heures × 5 jours = 25 lignes.
d::dateConvertit l'horodatage produit par generate_series en date simple.

Pour couvrir tous les terrains plutôt qu'un seul, il suffit de remplacer la sous-requête par un CROSS JOIN terrain t et de sélectionner t.noTerrain.

Quelques réservations

Cinq réservations, dont une non utilisée
INSERT INTO reservation (noCreneau, noAboResp, noAboPart, utilise) VALUES
  ( 1, 1, 2, true  ),
  ( 2, 2, 3, true  ),
  ( 3, 3, 4, false ),
  ( 6, 4, 5, true  ),
  ( 7, 5, 1, false );

Le booléen utilise est ce qui permettra de calculer la statistique demandée dans l'énoncé du TD : combien de créneaux ont été réservés et non utilisés.

7. Les requêtes simples

Voici les neuf questions du TP, dans l'ordre. Pour chacune : ce qui est demandé en français, le raisonnement, puis la requête.

(a) Lister le contenu de la table Abonne

SELECT * FROM abonne;

SELECT * veut dire « toutes les colonnes ». C'est pratique pour explorer une table, à éviter dans une application : si on ajoute une colonne plus tard, le programme reçoit soudain une donnée qu'il n'attendait pas.

(b) La liste des terrains de Squash

Le mot « Squash » n'est pas dans la table terrain : il est dans la table sport. Il faut donc joindre les deux.

SELECT t.noTerrain, t.nom
FROM terrain t
JOIN sport s USING (noSport)
WHERE s.libelle = 'Squash'
ORDER BY t.nom;

USING (noSport) est un raccourci de ON t.noSport = s.noSport : les deux tables ont une colonne du même nom, on peut le dire en un mot.

(c) Les abonnés qui n'ont pas de parrain, triés par nom

SELECT noAbo, nom, prenom
FROM abonne
WHERE noParrain IS NULL
ORDER BY nom;

`= NULL` ne marche pas

WHERE noParrain = NULL ne renvoie jamais de ligne, même quand la colonne est vide. NULL n'est pas une valeur : c'est l'absence de valeur. Rien n'est « égal » à rien, pas même NULL à NULL. Il faut utiliser IS NULL ou IS NOT NULL.

(d) Les réservations en Squash aujourd'hui, avec le nom de l'abonné, du terrain et l'heure

Trois informations viennent de trois tables différentes : l'abonné de abonne, le terrain de terrain, l'heure de creneau. Il faut donc chaîner les jointures en suivant les clés étrangères.

SELECT a.nom  AS abonne,
       t.nom  AS terrain,
       c.heureDebut
FROM reservation r
JOIN creneau c ON c.noCreneau = r.noCreneau   -- la réservation → son créneau
JOIN terrain t ON t.noTerrain = c.noTerrain   -- le créneau    → son terrain
JOIN sport   s ON s.noSport   = t.noSport     -- le terrain    → son sport
JOIN abonne  a ON a.noAbo     = r.noAboResp   -- la réservation → le responsable
WHERE s.libelle    = 'Squash'
  AND c.dateCreneau = CURRENT_DATE            -- « aujourd'hui »
ORDER BY c.heureDebut;

Le cheminement est toujours le même : on part de la table qui porte le fait (reservation), et on remonte les clés étrangères jusqu'à l'information cherchée. CURRENT_DATE est une fonction qui renvoie la date du jour — elle évite d'écrire la date en dur.

(e) Ajouter le nom du partenaire

Même requête, plus une jointure vers abonne — mais cette fois sur la colonne noAboPart :

SELECT a.nom  AS abonne,
       p.nom  AS partenaire,
       t.nom  AS terrain,
       c.heureDebut
FROM reservation r
JOIN creneau c ON c.noCreneau = r.noCreneau
JOIN terrain t ON t.noTerrain = c.noTerrain
JOIN sport   s ON s.noSport   = t.noSport
JOIN abonne  a ON a.noAbo     = r.noAboResp
JOIN abonne  p ON p.noAbo     = r.noAboPart   -- ← la seule ligne qui change
WHERE s.libelle    = 'Squash'
  AND c.dateCreneau = CURRENT_DATE
ORDER BY c.heureDebut;

C'est une auto-jointure : la table abonne apparaît deux fois, sous deux alias différents, parce qu'elle joue deux rôles distincts.

(f) Les identifiants et noms des clients qui jouent le 10/09

Attention : « les clients qui jouent » désigne aussi bien le responsable que le partenaire. Il faut donc interroger les deux colonnes, et supprimer les doublons.

Version avec OR
SELECT DISTINCT a.noAbo, a.nom, a.prenom
FROM abonne a
JOIN reservation r ON r.noAboResp = a.noAbo OR r.noAboPart = a.noAbo
JOIN creneau c ON c.noCreneau = r.noCreneau
WHERE c.dateCreneau = DATE '2026-09-10'
ORDER BY a.nom;
Version avec UNION — plus lisible et plus rapide
SELECT a.noAbo, a.nom, a.prenom
FROM abonne a
JOIN reservation r ON r.noAboResp = a.noAbo
JOIN creneau c ON c.noCreneau = r.noCreneau
WHERE c.dateCreneau = DATE '2026-09-10'
UNION
SELECT a.noAbo, a.nom, a.prenom
FROM abonne a
JOIN reservation r ON r.noAboPart = a.noAbo
JOIN creneau c ON c.noCreneau = r.noCreneau
WHERE c.dateCreneau = DATE '2026-09-10';

UNION empile deux résultats et supprime les doublons (contrairement à UNION ALL). Ici, un abonné qui serait à la fois responsable et partenaire le même jour n'apparaîtrait qu'une fois.

(g) Les identifiants des clients qui ont parrainé quelqu'un

SELECT DISTINCT noParrain AS noAbo
FROM abonne
WHERE noParrain IS NOT NULL
ORDER BY noParrain;

DISTINCT est indispensable : si Jean a parrainé trois personnes, il apparaît trois fois dans la colonne noParrain, mais une seule fois dans la liste des parrains.

Une variante équivalente, en partant de la table des abonnés :

Avec EXISTS
SELECT a.noAbo, a.nom
FROM abonne a
WHERE EXISTS (SELECT 1 FROM abonne f WHERE f.noParrain = a.noAbo);

(h) Les clients parrainés, avec le nom de leur parrain, triés par nom de parrain

Pour afficher côte à côte un filleul et son parrain, il faut ouvrir deux fois la même table sous deux noms différents. C'est le seul moyen de comparer deux lignes de la même table.
SELECT a.nom AS filleul, p.nom AS parrain
FROM abonne a
JOIN abonne p ON a.noParrain = p.noAbo
ORDER BY p.nom;

Cette fois, la question dit bien « les clients qui ont été parrainés » : les abonnés sans parrain ne font donc pas partie du résultat. Un JOIN ordinaire (INNER) fait exactement cela.

(i) Tous les clients, avec leur parrain s'il existe

Le « s'il existe » change tout : il faut conserver les abonnés sans parrain.

Les deux requêtes (h) et (i) du TP ne diffèrent que par un mot : INNER ou LEFT. Le résultat, lui, ne compte pas le même nombre de lignes — et c'est précisément ce que la question teste.
SELECT a.nom AS filleul, p.nom AS parrain
FROM abonne a
LEFT JOIN abonne p ON a.noParrain = p.noAbo
ORDER BY a.nom;

La règle à retenir entre (h) et (i)

« seulement ceux qui ont… » → JOIN. « tous, avec … s'il existe » → LEFT JOIN. Le second conserve les lignes orphelines en remplissant les colonnes de droite par NULL. Compter les lignes du résultat est le moyen le plus rapide de vérifier qu'on a bien choisi : 2 lignes contre 4 dans l'exemple ci-dessus.

8. Auto-évaluation


tp-postgresql.pdf

TP 3.01 — Rappels BDD : PostgreSQL et le club LaRaquette · 2 pages

Consultation seule — téléchargement désactivé

Chargement du document…

Dernière mise à jour le

Sur cette page