← Blog
Trois formes arrondies en céramique, une verte et deux ivoire, alignées dans un support sur une surface lavande.
Publié le
2026-11-11

Enums PostgreSQL : créer, trier et modifier les valeurs

Un enum PostgreSQL est un type de données nommé qui contient un ensemble ordonné de libellés autorisés. Créez-le avec CREATE TYPE ... AS ENUM, puis utilisez ce type pour une colonne. Il convient à un vocabulaire limité et stable, comme un statut de commande, si vous acceptez de gérer ses modifications comme des changements de schéma.

Ce guide construit un exemple de statuts et vérifie son comportement avec PostgreSQL 18.6. Nos 29 tests locaux couvrent les valeurs acceptées et refusées, le tri, NULL, les transactions, le renommage, les valeurs par défaut et les dépendances. Ils vérifient le fonctionnement dans une base isolée, sans constituer une migration de production ni un test de performances.

Créer un enum et l’utiliser dans une table

Utilisez une base jetable nommée enum_demo, avec les droits nécessaires pour créer un schéma. Elle ne doit pas déjà contenir enum_lab. Enregistrez chaque bloc numéroté dans le fichier SQL indiqué, puis exécutez les fichiers dans l’ordre, dans des invocations psql distinctes. Par exemple : psql -X -qAt -v ON_ERROR_STOP=1 -d enum_demo -f 01-create.sql. Ajoutez les options de connexion nécessaires.

Les résultats utilisent le format non aligné de psql, sans en-têtes, avec une barre verticale entre les colonnes. Les fichiers 03 et 04 échouent volontairement ; poursuivez avec le fichier suivant dans une nouvelle invocation. Ne regroupez pas tout l’exercice dans une transaction et n’utilisez pas l’option --single-transaction de psql.

Enregistrez ce bloc dans 01-create.sql :

CREATE SCHEMA enum_lab;
CREATE TYPE enum_lab.order_status AS ENUM ('new', 'paid', 'shipped');
CREATE TABLE enum_lab.orders (
    id integer PRIMARY KEY,
    status enum_lab.order_status NOT NULL DEFAULT 'new'
);
INSERT INTO enum_lab.orders (id, status)
VALUES (1, 'shipped'), (2, 'new'), (3, 'paid');
INSERT INTO enum_lab.orders (id) VALUES (4);
SELECT id, status FROM enum_lab.orders ORDER BY id;
1|shipped
2|new
3|paid
4|new

Le type appartient à enum_lab : la définition de la colonne utilise donc son nom qualifié. La colonne status accepte new, paid et shipped. La ligne 4 omet status et reçoit new par défaut. La clé primaire et la contrainte NOT NULL restent des règles distinctes ; un enum n’impose pas à lui seul la présence d’une valeur.

La documentation PostgreSQL sur les enums présente leurs libellés ordonnés et leurs restrictions. Conservez des libellés stables dans la base et gérez les traductions d’affichage dans l’application. Le libellé paid peut représenter le même état, quelle que soit la langue de l’interface.

Examiner les valeurs autorisées et leur ordre

Exécutez 02-order.sql :

SELECT id, status FROM enum_lab.orders ORDER BY status, id;
SELECT id, status FROM enum_lab.orders
ORDER BY status::text COLLATE "C", id;
SELECT min(status), max(status) FROM enum_lab.orders;
SELECT enum_range(NULL::enum_lab.order_status);
2|new
4|new
3|paid
1|shipped
2|new
4|new
3|paid
1|shipped
new|shipped
{new,paid,shipped}

La première requête trie les valeurs de l’enum. La deuxième les convertit en texte et applique la collation C pour rendre la comparaison reproductible. Les résultats coïncident pour cet ensemble initial. L’ajout de packed entre paid et shipped rendra ensuite la différence visible.

Le minimum est new et le maximum shipped. La dernière expression liste tout le vocabulaire, même si certaines valeurs ne sont utilisées par aucune ligne. Le NULL typé indique à enum_range quel enum examiner ; il n’ajoute pas NULL à ses libellés. La référence des fonctions pour enums décrit aussi enum_first, enum_last et les plages délimitées.

Vérifiez que cet ordre a un sens avant de vous appuyer sur une comparaison comme status > 'paid'. Un statut annulé ou remboursé ne s’inscrit pas nécessairement dans une progression linéaire. L’enum limite les valeurs individuelles, mais ne contrôle pas les transitions autorisées entre deux statuts.

Refuser les libellés invalides et distinguer NULL

Exécutez 03-invalid.sql dans une invocation distincte :

INSERT INTO enum_lab.orders (id, status) VALUES (5, 'Paid');

Notre test refuse Paid avec le SQLSTATE 22P02 : le libellé déclaré est paid. D’autres tests refusent paid suivi d’une espace et le libellé non déclaré cancelled. Les quatre lignes existantes restent inchangées.

L’insertion de NULL dans orders.status échoue avec le SQLSTATE 23502 à cause de NOT NULL. Une autre colonne nullable utilisant le même enum accepte NULL. Décidez explicitement si un statut manquant est pertinent : le type et la contrainte de colonne répondent à des besoins différents.

Si les données proviennent d’un autre service, définissez leur normalisation à l’entrée et refusez les états inconnus. Ne transformez pas silencieusement toute faute de saisie en new. Pour un fichier à importer, contrôlez les valeurs sources avant le chargement ; une erreur de conversion doit être examinée plutôt que masquée.

Ajouter une valeur et respecter la validation de transaction

Le fichier 04-unsafe-add.sql échoue volontairement pour montrer un piège de migration :

BEGIN;
ALTER TYPE enum_lab.order_status ADD VALUE 'packed' BEFORE 'shipped';
INSERT INTO enum_lab.orders (id, status) VALUES (5, 'packed');
COMMIT;

L’ajout de packed au type existant réussit dans la transaction, mais l’INSERT suivant échoue avec le SQLSTATE 55P04 : le nouveau libellé n’est pas encore utilisable. Avec ON_ERROR_STOP, psql quitte avant COMMIT. La fermeture de la connexion annule la transaction. Notre connexion suivante confirme que packed n’existe pas et que l’ID 5 n’a pas été inséré. Dans une session interactive, exécutez ROLLBACK après l’erreur avant de poursuivre.

Exécutez maintenant 05-add.sql. Le changement de schéma est validé avant l’insertion du nouveau libellé :

BEGIN;
ALTER TYPE enum_lab.order_status ADD VALUE 'packed' BEFORE 'shipped';
COMMIT;
INSERT INTO enum_lab.orders (id, status) VALUES (5, 'packed');
SELECT enum_range(NULL::enum_lab.order_status);
SELECT id, status FROM enum_lab.orders ORDER BY status, id;
{new,paid,packed,shipped}
2|new
4|new
3|paid
5|packed
1|shipped

L’ordre déclaré devient new, paid, packed, shipped. L’ID 5 se place entre paid et shipped. Notre requête distincte de tri textuel place packed avant paid avec la collation C : l’ordre de l’enum diffère donc de celui des libellés textuels.

La référence ALTER TYPE documente ADD VALUE, le placement BEFORE/AFTER et la nécessité de valider la transaction. Sans indication de position, la valeur est ajoutée à la fin. Le changement de schéma doit être effectué par le propriétaire du type ou un rôle administratif autorisé.

Vérifiez comment votre outil de migration regroupe les instructions. Séparer deux fichiers ne suffit pas si l’outil les exécute dans une seule transaction. Validez d’abord l’ajout, puis utilisez la valeur dans une transaction ultérieure. Coordonnez les versions de l’application pour que les lecteurs reconnaissent le nouveau libellé avant que les producteurs de données l’envoient.

Un test supplémentaire avec ADD VALUE IF NOT EXISTS pour packed, déjà présent, laisse sa position inchangée même si l’instruction demande AFTER shipped. Cette option évite l’erreur d’ajout en double ; elle ne déplace pas une valeur existante.

Renommer un libellé et contrôler les données existantes

Exécutez 06-rename.sql :

ALTER TYPE enum_lab.order_status RENAME VALUE 'new' TO 'pending';
SELECT id, status FROM enum_lab.orders ORDER BY id;
SELECT enum_range(NULL::enum_lab.order_status);
INSERT INTO enum_lab.orders (id) VALUES (6);
SELECT id, status FROM enum_lab.orders WHERE id = 6;
1|shipped
2|pending
3|paid
4|pending
5|packed
{pending,paid,packed,shipped}
6|pending

Les lignes 2 et 4 affichent désormais pending, sans mise à jour individuelle. Le libellé conserve sa position et son identité interne dans l’enum. La valeur par défaut existante produit aussi pending pour la ligne 6. Nos tests vérifient directement ces résultats.

L’ancienne orthographe new n’est plus acceptée, et renommer paid en pending échoue puisque ce libellé existe déjà. Mettez à jour les constantes applicatives, les validations, les filtres de rapports et les contrats d’intégration avec le schéma. Le nouvel affichage des lignes existantes ne garantit pas qu’une ancienne version de l’application le comprendra.

Deux enums créés séparément restent des types distincts, même si leurs libellés correspondent. Notre comparaison directe échoue avec le SQLSTATE 42883. Une conversion explicite des deux côtés en texte permet de comparer les libellés, en abandonnant volontairement la distinction entre les deux types.

Choisir entre enum, contrainte CHECK et table de référence

  • Choisissez un enum pour un vocabulaire limité et stable, lorsque plusieurs colonnes doivent partager un type nommé et que son ordre déclaré est pertinent.
  • Envisagez du texte avec une contrainte CHECK si la restriction concerne une seule table et doit être gérée comme une règle de colonne. Prévoyez la validation des lignes existantes lors d’une modification de contrainte.
  • Envisagez une table de référence avec clé étrangère si chaque choix possède des descriptions, des traductions, des dates d’activation ou d’autres attributs modifiables comme des données. Les références et les règles de suppression restent à définir.

PostgreSQL ne propose pas de simple DROP VALUE pour un enum ni de commande réordonnant ses libellés existants. Remplacer un type nécessite de planifier la migration des colonnes et objets dépendants, notamment la correspondance des valeurs existantes. Ne modifiez pas les catalogues système pour forcer le changement. Avant une migration importante, préparez et vérifiez la récupération avec notre guide de sauvegarde et de restauration PostgreSQL, en anglais.

L’ajout d’un statut ne donne pas non plus le droit de le modifier. Gérez séparément les règles d’accès par utilisateur ou locataire. Ne choisissez pas un enum sur la base d’un gain de vitesse supposé : cet exercice vérifie le comportement, pas les performances d’une charge réelle.

Supprimer les objets de l’exercice

Une fois l’exercice terminé, exécutez 07-cleanup.sql dans la base jetable :

DROP TABLE enum_lab.orders;
DROP TYPE enum_lab.order_status;
DROP SCHEMA enum_lab;

La table est supprimée avant son type. Notre test négatif confirme que DROP TYPE seul refuse de supprimer le type tant que la colonne orders en dépend. Le nettoyage nomme explicitement les objets et n’utilise pas CASCADE.

L’archive de vérification contient les scripts SQL, les sorties capturées et le programme de test. Ce programme crée son propre cluster local protégé par mot de passe, supprime les objets de l’exercice, arrête le serveur et efface le fichier de mot de passe temporaire. Il ne se connecte pas à une base applicative existante. Pour un environnement d’apprentissage distinct dans un conteneur, consultez notre guide PostgreSQL avec Docker Compose, en anglais.

Votre prochaine charge de travail sur Hivenet.

Choisissez une charge de travail d’IA, de calcul ou de stockage à tester sur Hivenet, ou échangez avec notre équipe sur sa mise en production.