// Bases de données

Les clés étrangères SQL : relations et intégrité

Une base relationnelle éclate l’information en plusieurs tables : les clients d’un côté, leurs commandes de l’autre. Encore faut-il garantir que chaque commande pointe vers un client qui existe réellement, et qu’aucune commande ne reste orpheline. C’est le rôle de la clé étrangère (FOREIGN KEY) : elle relie deux tables et fait respecter par la base elle-même la cohérence de ces liens, ce qu’on appelle l’intégrité référentielle.

Ce guide couvre la déclaration d’une clé étrangère, les relations un-à-plusieurs et plusieurs-à-plusieurs, les actions ON DELETE et ON UPDATE, et les schémas concrets qui illustrent chaque cas.

Qu’est-ce qu’une clé étrangère ?

Une clé étrangère est une colonne (ou un ensemble de colonnes) d’une table qui référence la clé primaire d’une autre table. Elle établit un lien logique et impose une règle : la valeur de la clé étrangère doit correspondre à une ligne existante dans la table référencée, ou être NULL.

Prenons deux tables, clients et commandes. La colonne client_id de commandes est la clé étrangère qui pointe vers clients.id.

CREATE TABLE clients (
    id   INTEGER PRIMARY KEY,
    nom  TEXT NOT NULL
);

CREATE TABLE commandes (
    id         INTEGER PRIMARY KEY,
    client_id  INTEGER,
    montant    NUMERIC,
    FOREIGN KEY (client_id) REFERENCES clients (id)
);

Désormais, insérer une commande dont le client_id n’existe pas dans clients est refusé par la base :

INSERT INTO commandes (id, client_id, montant) VALUES (1, 999, 50);
-- Erreur : violation de contrainte de clé étrangère (aucun client 999)

C’est cette barrière automatique qui distingue une vraie base relationnelle d’un simple ensemble de tableaux : la cohérence n’est pas laissée à la bonne volonté du code applicatif, elle est imposée par le moteur.

Déclarer une clé étrangère

Deux syntaxes coexistent. La forme en ligne, concise quand la clé porte sur une seule colonne, et la forme séparée (contrainte de table), obligatoire pour les clés composées.

En ligne

CREATE TABLE commandes (
    id         INTEGER PRIMARY KEY,
    client_id  INTEGER REFERENCES clients (id),
    montant    NUMERIC
);

En contrainte de table nommée

Nommer la contrainte (CONSTRAINT fk_...) facilite les messages d’erreur et la suppression ultérieure.

CREATE TABLE commandes (
    id         INTEGER PRIMARY KEY,
    client_id  INTEGER,
    montant    NUMERIC,
    CONSTRAINT fk_commande_client
        FOREIGN KEY (client_id) REFERENCES clients (id)
);

Ajouter une clé à une table existante

ALTER TABLE commandes
ADD CONSTRAINT fk_commande_client
FOREIGN KEY (client_id) REFERENCES clients (id);

Un point d’attention propre à SQLite : les clés étrangères y sont désactivées par défaut et doivent être activées à chaque connexion avec PRAGMA foreign_keys = ON;. MySQL (moteur InnoDB) et PostgreSQL les appliquent nativement.

La relation un-à-plusieurs (1-N)

C’est la relation la plus fréquente : un client possède plusieurs commandes, mais chaque commande appartient à un seul client. On la modélise en plaçant la clé étrangère du côté « plusieurs », c’est-à-dire dans la table commandes.

CREATE TABLE clients (
    id   INTEGER PRIMARY KEY,
    nom  TEXT NOT NULL
);

CREATE TABLE commandes (
    id         INTEGER PRIMARY KEY,
    client_id  INTEGER NOT NULL,
    date_cmd   DATE,
    FOREIGN KEY (client_id) REFERENCES clients (id)
);

Ajouter NOT NULL à client_id renforce la règle : toute commande doit avoir un client, on interdit les commandes anonymes. Pour retrouver le client d’une commande, on rapproche ensuite les tables par une jointure ; le sujet est détaillé dans notre guide des jointures SQL.

La relation plusieurs-à-plusieurs (N-N)

Certaines relations sont bidirectionnelles : un article peut porter plusieurs étiquettes, et une étiquette peut être posée sur plusieurs articles. On ne peut pas exprimer cela avec une simple colonne. La solution est une table de liaison (aussi appelée table pivot ou d’association), qui contient deux clés étrangères.

CREATE TABLE articles (
    id     INTEGER PRIMARY KEY,
    titre  TEXT
);

CREATE TABLE etiquettes (
    id   INTEGER PRIMARY KEY,
    nom  TEXT
);

-- Table de liaison
CREATE TABLE article_etiquette (
    article_id    INTEGER,
    etiquette_id  INTEGER,
    PRIMARY KEY (article_id, etiquette_id),
    FOREIGN KEY (article_id)   REFERENCES articles (id),
    FOREIGN KEY (etiquette_id) REFERENCES etiquettes (id)
);

La clé primaire composée (article_id, etiquette_id) garantit qu’un même couple ne peut être enregistré deux fois : un article ne reçoit pas deux fois la même étiquette. Chaque ligne de cette table représente une association entre un article et une étiquette.

-- L'article 1 reçoit les étiquettes 10 et 20
INSERT INTO article_etiquette VALUES (1, 10), (1, 20);

Les actions ON DELETE et ON UPDATE

Que se passe-t-il quand on supprime un client qui possède encore des commandes ? Par défaut, la base refuse la suppression pour ne pas créer d’orphelins. Mais on peut préciser le comportement souhaité avec les clauses ON DELETE et ON UPDATE.

RESTRICT / NO ACTION : refuser

C’est le comportement par défaut. Supprimer une ligne référencée est bloqué tant que des enfants existent.

FOREIGN KEY (client_id) REFERENCES clients (id)
    ON DELETE RESTRICT;

CASCADE : propager

Avec ON DELETE CASCADE, supprimer un client supprime automatiquement toutes ses commandes. Avec ON UPDATE CASCADE, modifier l’id d’un client répercute la nouvelle valeur dans les commandes liées. Puissant, mais à manier avec prudence : une suppression peut en entraîner beaucoup d’autres.

CREATE TABLE commandes (
    id         INTEGER PRIMARY KEY,
    client_id  INTEGER NOT NULL,
    FOREIGN KEY (client_id) REFERENCES clients (id)
        ON DELETE CASCADE
        ON UPDATE CASCADE
);

-- Supprimer le client 5 efface aussi toutes ses commandes
DELETE FROM clients WHERE id = 5;

SET NULL : détacher

ON DELETE SET NULL conserve les commandes mais met leur client_id à NULL. Utile quand la ligne enfant garde un sens sans son parent (une commande archivée dont le client a été effacé). La colonne doit évidemment autoriser NULL.

FOREIGN KEY (client_id) REFERENCES clients (id)
    ON DELETE SET NULL;

Tableau récapitulatif

Action      | Effet à la suppression du parent
------------+-------------------------------------------
RESTRICT    | Suppression refusée s'il reste des enfants
CASCADE     | Les enfants sont supprimés aussi
SET NULL    | La clé étrangère des enfants passe à NULL
SET DEFAULT | La clé étrangère prend sa valeur par défaut

L’intégrité référentielle en pratique

L’intégrité référentielle est la garantie qu’aucun lien ne pointe dans le vide. Concrètement, la base empêche trois choses : insérer un enfant sans parent valide, supprimer un parent qui a des enfants (selon l’action choisie), et modifier une clé primaire référencée sans propagation.

Voici un schéma complet mettant en scène ces règles, celui d’un petit blog.

CREATE TABLE auteurs (
    id   INTEGER PRIMARY KEY,
    nom  TEXT NOT NULL
);

CREATE TABLE billets (
    id         INTEGER PRIMARY KEY,
    auteur_id  INTEGER NOT NULL,
    titre      TEXT NOT NULL,
    FOREIGN KEY (auteur_id) REFERENCES auteurs (id)
        ON DELETE RESTRICT      -- on ne perd pas un billet par erreur
);

CREATE TABLE commentaires (
    id         INTEGER PRIMARY KEY,
    billet_id  INTEGER NOT NULL,
    contenu    TEXT,
    FOREIGN KEY (billet_id) REFERENCES billets (id)
        ON DELETE CASCADE       -- effacer un billet efface ses commentaires
);

Ce schéma exprime deux politiques différentes selon le sens du lien : on protège les billets contre la suppression d’un auteur (RESTRICT), mais on accepte que supprimer un billet emporte ses commentaires (CASCADE), car un commentaire n’a aucun sens sans son billet.

Bonnes pratiques

  • Indexez vos clés étrangères. La colonne portant la clé étrangère est très souvent utilisée en jointure et en filtre ; l’indexer accélère les lectures et les vérifications de contrainte. MySQL crée cet index automatiquement, PostgreSQL non.
  • Nommez vos contraintes. Un nom explicite (fk_billet_auteur) rend les erreurs lisibles et la maintenance plus simple.
  • Réfléchissez aux actions avant de coder. Le choix entre RESTRICT, CASCADE et SET NULL traduit une décision métier, pas un détail technique.
  • Activez les clés étrangères sous SQLite. N’oubliez jamais le PRAGMA foreign_keys = ON;, sinon vos contraintes sont ignorées silencieusement.

En résumé

La clé étrangère relie deux tables et confie à la base le soin de préserver la cohérence des liens. On la place du côté « plusieurs » pour une relation 1-N, et dans une table de liaison pour une relation N-N. Les actions ON DELETE et ON UPDATERESTRICT, CASCADE, SET NULL — décident du sort des lignes liées lors d’une suppression ou d’une mise à jour. Bien pensées, ces contraintes rendent impossibles les données incohérentes et allègent d’autant le code applicatif.

À lire ensuite