Créer les tables
Traduire le dossier d'analyse en vraies tables PostgreSQL.
Après cette fiche, vous saurez :
- écrire une commande CREATE TABLE
- déclarer une clé primaire auto-incrémentée
- ajouter des contraintes NOT NULL, UNIQUE et CHECK
- modifier ou supprimer une table
1Le SQL et ses familles
SQL se divise en quatre familles de commandes. Vous n'en utiliserez vraiment que deux.
| Famille | Sert à | Commandes | Quand ? |
|---|---|---|---|
| DDL | Définir la structure | CREATE, ALTER, DROP | Cette fiche |
| DML | Manipuler les données | INSERT, UPDATE, DELETE, SELECT | Fiches 8 à 11 |
| DCL | Gérer les droits | GRANT, REVOKE | Culture générale |
| TCL | Gérer les transactions | COMMIT, ROLLBACK | Culture 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)
);| Morceau | Ce qu'il fait |
|---|---|
auteurs | Le nom de la table, au pluriel et en minuscules |
INTEGER | Le type : un nombre entier |
PRIMARY KEY | Cette colonne identifie chaque ligne |
GENERATED ALWAYS AS IDENTITY | PostgreSQL remplit le numéro tout seul : 1, 2, 3… |
VARCHAR(50) | Du texte, 50 caractères au maximum |
NOT NULL | Cette 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.
| Contrainte | Garantit que… | Exemple |
|---|---|---|
PRIMARY KEY | la colonne identifie chaque ligne | num_livre INTEGER PRIMARY KEY |
NOT NULL | la colonne n'est jamais vide | titre VARCHAR(150) NOT NULL |
UNIQUE | aucune valeur n'apparaît deux fois | isbn VARCHAR(20) UNIQUE |
CHECK | la valeur respecte une condition | annee INTEGER CHECK (annee > 1400) |
DEFAULT | une valeur est mise si on n'en donne pas | disponible 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 ».
date_retour = NULL→ le livre n'est pas encore rendu.date_retour = '2026-09-30'→ il est rendu.
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 :
auteursetlecteurs— elles ne dépendent de personnelivres— elle pointe versauteursemprunts— elle pointe verslecteursetlivres
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
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
);- Rafraîchissez l'arborescence et vérifiez que les deux tables sont là.
- Déployez les colonnes de
lecteurs. Quels types voyez-vous ? - À 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.
Voici une table mal conçue.
CREATE TABLE membres (
num_membre INTEGER,
nom VARCHAR(50),
email VARCHAR(100),
age INTEGER,
cotisation NUMERIC(6,2)
);- Repérez les quatre problèmes.
- Réécrivez la table correctement.
- 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 IDENTITYsur le numéro ;NOT NULLsur le nom et l'email ;UNIQUEsur l'email ;- un
CHECKsur l'âge, pour écarter les saisies absurdes ; - un
CHECKsur la cotisation, plus unDEFAULT 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.
Reprenez votre dossier d'analyse de la fiche 4.
- Écrivez le script de création complet, avec les
DROP TABLE IF EXISTSau début, dans le bon ordre. - Pour l'instant, ne mettez pas les clés étrangères : elles font l'objet de la fiche 7. Créez juste les colonnes.
- Ajoutez au moins un
NOT NULL, unUNIQUEet unCHECK. - 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
- Je sais écrire un CREATE TABLE complet.
- Je sais déclarer une clé primaire qui se remplit toute seule.
- Je connais les contraintes NOT NULL, UNIQUE et CHECK.
- Je sais modifier une table existante avec ALTER TABLE.
Une case qui coince ? Relisez la partie concernée avant de passer à la fiche suivante.