⌂ Bases de données
Fiche 6 sur 12

Créer les tables

Traduire le dossier d'analyse en vraies tables PostgreSQL.

Après cette fiche, vous saurez :

1Le SQL et ses familles

SQL se divise en quatre familles de commandes. Vous n'en utiliserez vraiment que deux.

FamilleSert àCommandesQuand ?
DDLDéfinir la structureCREATE, ALTER, DROPCette fiche
DMLManipuler les donnéesINSERT, UPDATE, DELETE, SELECTFiches 8 à 11
DCLGérer les droitsGRANT, REVOKECulture générale
TCLGérer les transactionsCOMMIT, ROLLBACKCulture générale

À retenir

La distinction utile est la première : DDL = les meubles, DML = ce qu'on met dedans.

On construit l'armoire une fois (DDL), on y range et on en sort des vêtements tous les jours (DML).

2CREATE TABLE

La forme générale est toujours la même.

CREATE TABLE nom_de_la_table (
    colonne1 TYPE contraintes,
    colonne2 TYPE contraintes,
    colonne3 TYPE contraintes
);

Une ligne par colonne, séparées par des virgules. Pas de virgule après la dernière, et un point-virgule à la fin de la commande.

Notre première table

CREATE TABLE auteurs (
    num_auteur   INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    nom          VARCHAR(50) NOT NULL,
    prenom       VARCHAR(50),
    nationalite  VARCHAR(40)
);
MorceauCe qu'il fait
auteursLe nom de la table, au pluriel et en minuscules
INTEGERLe type : un nombre entier
PRIMARY KEYCette colonne identifie chaque ligne
GENERATED ALWAYS AS IDENTITYPostgreSQL remplit le numéro tout seul : 1, 2, 3…
VARCHAR(50)Du texte, 50 caractères au maximum
NOT NULLCette colonne ne peut pas rester vide

À retenir

La clé primaire auto-incrémentée est le réflexe à prendre.

num_auteur INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY

C'est long à écrire, mais ça vous évite de gérer les numéros à la main. Vous ne donnerez jamais de valeur à cette colonne : PostgreSQL s'en charge.

Vous verrez souvent SERIAL à la place, dans les anciens tutoriels :

num_auteur SERIAL PRIMARY KEY

Ça fonctionne, mais c'est une écriture propre à PostgreSQL et considérée comme dépassée. GENERATED ALWAYS AS IDENTITY est la forme standard, celle qui marche aussi ailleurs.

3Les contraintes

Une contrainte est une règle que le SGBD fait respecter à votre place. Il refusera toute donnée qui la viole.

ContrainteGarantit que…Exemple
PRIMARY KEYla colonne identifie chaque lignenum_livre INTEGER PRIMARY KEY
NOT NULLla colonne n'est jamais videtitre VARCHAR(150) NOT NULL
UNIQUEaucune valeur n'apparaît deux foisisbn VARCHAR(20) UNIQUE
CHECKla valeur respecte une conditionannee INTEGER CHECK (annee > 1400)
DEFAULTune valeur est mise si on n'en donne pasdisponible BOOLEAN DEFAULT true
CREATE TABLE livres (
    num_livre    INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    titre        VARCHAR(150) NOT NULL,
    annee        INTEGER CHECK (annee > 1400),
    isbn         VARCHAR(20) UNIQUE,
    disponible   BOOLEAN DEFAULT true
);

À retenir

Pourquoi ne pas simplement vérifier dans le programme ?

Parce qu'une base de données est rarement utilisée par un seul programme. Il y a l'application, pgAdmin, un script d'import, un collègue pressé…

Une contrainte dans la base protège contre tout le monde, tout le temps. Une vérification dans le programme ne protège que contre ce programme-là.

Attention

NULL n'est pas zéro, et n'est pas une chaîne vide.

NULL veut dire « on ne sait pas » ou « pas encore rempli ».

C'est très différent d'un zéro. Nous verrons à la fiche 9 que NULL demande des précautions particulières dans les requêtes.

4L'ordre de création compte

La table livres contient une clé étrangère vers auteurs. Impossible donc de créer livres en premier : PostgreSQL refuserait de pointer vers une table qui n'existe pas.

À retenir

Règle : on crée les tables de l'indépendante vers la dépendante.

Pour la bibliothèque :

  1. auteurs et lecteurs — elles ne dépendent de personne
  2. livres — elle pointe vers auteurs
  3. emprunts — elle pointe vers lecteurs et livres

Et pour supprimer, c'est exactement l'inverse : on commence par la dernière créée.

5Modifier une table existante

Vous avez oublié une colonne ? Inutile de tout recommencer.

-- Ajouter une colonne
ALTER TABLE auteurs ADD COLUMN date_naissance DATE;

-- Supprimer une colonne
ALTER TABLE auteurs DROP COLUMN date_naissance;

-- Renommer une colonne
ALTER TABLE auteurs RENAME COLUMN nationalite TO pays;

-- Changer le type d'une colonne
ALTER TABLE auteurs ALTER COLUMN nom TYPE VARCHAR(80);

-- Rendre une colonne obligatoire
ALTER TABLE auteurs ALTER COLUMN prenom SET NOT NULL;

Le double tiret -- introduit un commentaire en SQL. Tout ce qui suit sur la ligne est ignoré.

6Supprimer

-- Supprimer la table entière : structure ET données
DROP TABLE emprunts;

-- Vider une table mais garder sa structure
TRUNCATE TABLE emprunts;

Attention

DROP et TRUNCATE ne demandent aucune confirmation.

Il n'y a pas de corbeille, pas d'annulation, pas de « êtes-vous sûr ? ». La commande part, les données disparaissent.

Relisez toujours deux fois avant d'exécuter un DROP. Et faites des sauvegardes — c'est l'objet de la fiche 12.

Pour éviter une erreur quand on ne sait pas si la table existe :

DROP TABLE IF EXISTS emprunts;
CREATE TABLE IF NOT EXISTS auteurs ( ... );

À retenir

Le script de création complet, en un seul bloc :

DROP TABLE IF EXISTS emprunts;
DROP TABLE IF EXISTS livres;
DROP TABLE IF EXISTS lecteurs;
DROP TABLE IF EXISTS auteurs;

CREATE TABLE auteurs ( ... );
CREATE TABLE lecteurs ( ... );
CREATE TABLE livres ( ... );
CREATE TABLE emprunts ( ... );

On efface tout, puis on recrée. Ce script devient rejouable à volonté : très pratique quand on se trompe et qu'on veut repartir propre.

Notez l'ordre : les DROP à l'envers des CREATE.

→Exercices

1Créer les deux premières tablesRecopier

Dans votre base bibliotheque, exécutez ce script.

CREATE TABLE auteurs (
    num_auteur   INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    nom          VARCHAR(50) NOT NULL,
    prenom       VARCHAR(50),
    nationalite  VARCHAR(40)
);

CREATE TABLE lecteurs (
    num_lecteur       INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    nom               VARCHAR(50) NOT NULL,
    prenom            VARCHAR(50) NOT NULL,
    adresse           VARCHAR(120),
    telephone         VARCHAR(20),
    date_inscription  DATE DEFAULT CURRENT_DATE
);
  1. Rafraîchissez l'arborescence et vérifiez que les deux tables sont là.
  2. Déployez les colonnes de lecteurs. Quels types voyez-vous ?
  3. À votre avis, que fait CURRENT_DATE ?

Solution

1. Clic droit sur Tables → Refresh. Les deux tables apparaissent.

2. pgAdmin affiche integer, character varying(50), date. Remarquez que VARCHAR est le nom court de character varying : les deux écritures sont équivalentes.

3. CURRENT_DATE est la date du jour, calculée par PostgreSQL au moment de l'insertion.

Combiné à DEFAULT, cela veut dire : si on ne précise pas de date d'inscription, mets celle d'aujourd'hui. C'est exactement ce qu'on veut pour une inscription en bibliothèque.

À tester : essayez d'insérer un auteur sans nom.

INSERT INTO auteurs (prenom) VALUES ('Émile');
ERROR: null value in column "nom" violates not-null constraint

La contrainte NOT NULL a fait son travail.

2Ajouter les contraintes manquantesModifier

Voici une table mal conçue.

CREATE TABLE membres (
    num_membre INTEGER,
    nom VARCHAR(50),
    email VARCHAR(100),
    age INTEGER,
    cotisation NUMERIC(6,2)
);
  1. Repérez les quatre problèmes.
  2. Réécrivez la table correctement.
  3. Ajoutez une contrainte qui empêche une cotisation négative.

Solution

1. Les quatre problèmes

  • Aucune clé primaire. Rien n'empêche deux membres d'avoir le même numéro, ni de laisser le numéro vide.
  • Le numéro n'est pas auto-incrémenté. Il faudrait le gérer à la main, avec un risque de doublon.
  • Le nom peut être vide. Un membre sans nom n'a aucun sens.
  • L'email n'est pas unique. Deux membres pourraient partager la même adresse, ce qui empêcherait de s'en servir pour les retrouver.

2 et 3. La version corrigée

CREATE TABLE membres (
    num_membre  INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    nom         VARCHAR(50) NOT NULL,
    email       VARCHAR(100) NOT NULL UNIQUE,
    age         INTEGER CHECK (age >= 0 AND age < 130),
    cotisation  NUMERIC(6,2) DEFAULT 0 CHECK (cotisation >= 0)
);

Ce qui a été ajouté :

  • PRIMARY KEY GENERATED ALWAYS AS IDENTITY sur le numéro ;
  • NOT NULL sur le nom et l'email ;
  • UNIQUE sur l'email ;
  • un CHECK sur l'âge, pour écarter les saisies absurdes ;
  • un CHECK sur la cotisation, plus un DEFAULT 0.

Remarque sur l'âge : stocker un âge est en réalité une mauvaise idée — il change tous les ans. On stocke une date_naissance et on calcule l'âge quand on en a besoin. C'est une erreur de conception classique.

3Le script complet de votre baseCréer

Reprenez votre dossier d'analyse de la fiche 4.

  1. Écrivez le script de création complet, avec les DROP TABLE IF EXISTS au début, dans le bon ordre.
  2. Pour l'instant, ne mettez pas les clés étrangères : elles font l'objet de la fiche 7. Créez juste les colonnes.
  3. Ajoutez au moins un NOT NULL, un UNIQUE et un CHECK.
  4. Exécutez le script deux fois de suite. Doit-il fonctionner les deux fois ?

Solution

Exemple avec le cabinet médical.

DROP TABLE IF EXISTS consultations;
DROP TABLE IF EXISTS medecins;
DROP TABLE IF EXISTS patients;

CREATE TABLE patients (
    num_patient     INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    nom             VARCHAR(50) NOT NULL,
    prenom          VARCHAR(50) NOT NULL,
    date_naissance  DATE NOT NULL,
    telephone       VARCHAR(20),
    numero_national VARCHAR(15) UNIQUE
);

CREATE TABLE medecins (
    num_medecin   INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    nom           VARCHAR(50) NOT NULL,
    prenom        VARCHAR(50),
    specialite    VARCHAR(60) NOT NULL,
    numero_inami  VARCHAR(20) NOT NULL UNIQUE
);

CREATE TABLE consultations (
    num_consultation INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    date_consult     DATE NOT NULL DEFAULT CURRENT_DATE,
    heure            TIME,
    motif            TEXT,
    montant          NUMERIC(6,2) CHECK (montant >= 0),
    num_patient      INTEGER,
    num_medecin      INTEGER
);

Réponse à la question 4 : oui, il doit fonctionner les deux fois.

C'est tout l'intérêt du DROP TABLE IF EXISTS. Sans lui, la deuxième exécution échouerait avec relation already exists.

Attention à l'ordre des DROP. Ici consultations vient en premier parce qu'elle dépendra des deux autres dès la fiche 7. Créez toujours vos scripts avec les DROP dans l'ordre inverse des CREATE, même avant d'avoir ajouté les clés étrangères : vous n'aurez pas à y revenir.

Un point à noter : les colonnes num_patient et num_medecin existent déjà, mais ce ne sont pour l'instant que de simples entiers. Rien n'empêche encore d'y mettre n'importe quoi. C'est la fiche 7 qui va les transformer en vraies clés étrangères.

Je sais faire

Une case qui coince ? Relisez la partie concernée avant de passer à la fiche suivante.

← Fiche 5 Fiche 7 : Les clés étrangères →