← Back to Home

Database Enum Guide

Data Modeling · Migration · Integrity

Complete Database Enum Guide

Using enums in databases ensures data integrity by restricting fields to specific values. PostgreSQL and MySQL both support native enum types, but their implementation approaches and flexibility differ. This guide will help you choose the right solution and keep it synchronized with application code.

This guide covers PostgreSQL's CREATE TYPE, MySQL's ENUM fields, lookup table design, migration strategies, and integration with ORMs like Prisma, SQLAlchemy, Rails, etc.

PostgreSQL Enum: Enterprise Best Practices

PostgreSQL's enum types are true database objects that require creating the type first, then using it in table fields. This provides strong type safety and performance advantages, but modifications require special attention.

Creating Enum Types

-- Create enum type
CREATE TYPE order_status AS ENUM (
  'pending',
  'processing', 
  'shipped',
  'delivered',
  'cancelled'
);

-- Use in table
CREATE TABLE orders (
  id          BIGSERIAL PRIMARY KEY,
  status      order_status NOT NULL DEFAULT 'pending',
  created_at  TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Query examples
SELECT * FROM orders WHERE status = 'pending';
SELECT status, COUNT(*) FROM orders GROUP BY status;

Enum types provide type safety, high storage efficiency (internally uses integers), and good query performance.

Adding Enum Values

-- Add new value at the end
ALTER TYPE order_status ADD VALUE 'refunded';

-- Add at specified position
ALTER TYPE order_status ADD VALUE 'confirmed' 
  AFTER 'pending';

ALTER TYPE order_status ADD VALUE 'failed' 
  BEFORE 'cancelled';

-- Note: ADD VALUE cannot be executed in a transaction
-- Needs to be run separately in migration script

PostgreSQL 9.1+ supports ADD VALUE, but it cannot be executed within a transaction block.

Renaming or Deleting Enum Values (Complex Operation)

PostgreSQL does not support directly deleting or renaming enum values. This requires a multi-step migration:

-- Step 1: Create new enum type
CREATE TYPE order_status_new AS ENUM (
  'pending',
  'confirmed',  -- New name
  'processing',
  'shipped',
  'delivered'
  -- Removed 'cancelled'
);

-- Step 2: Add temporary column
ALTER TABLE orders 
  ADD COLUMN status_new order_status_new;

-- Step 3: Migrate data
UPDATE orders 
SET status_new = status::text::order_status_new
WHERE status::text IN ('pending', 'processing', 'shipped', 'delivered');

-- Step 4: Drop old column, rename new column
ALTER TABLE orders DROP COLUMN status;
ALTER TABLE orders RENAME COLUMN status_new TO status;

-- Step 5: Drop old type
DROP TYPE order_status;
ALTER TYPE order_status_new RENAME TO order_status;

This process requires careful planning, recommend validating in a test environment first.

Query All Enum Types

-- List all enum types
SELECT n.nspname AS schema,
       t.typname AS enum_type,
       e.enumlabel AS enum_value
FROM pg_type t 
JOIN pg_enum e ON t.oid = e.enumtypid  
JOIN pg_namespace n ON t.typnamespace = n.oid
ORDER BY schema, enum_type, e.enumsortorder;

Check Data Consistency

-- Check for invalid values (should not exist in theory)
SELECT DISTINCT status 
FROM orders 
WHERE status NOT IN (
  SELECT enumlabel::order_status 
  FROM pg_enum 
  WHERE enumtypid = 'order_status'::regtype
);

MySQL & MariaDB: Enum vs Lookup Table

MySQL's ENUM type is simple to use but not flexible for modifications. Many teams choose to use independent lookup tables with foreign keys, which adds JOIN operations but is easier to maintain.

MySQL Native Enum

-- Define enum when creating table
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  status ENUM(
    'pending',
    'processing',
    'shipped',
    'delivered',
    'cancelled'
  ) NOT NULL DEFAULT 'pending',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Query examples
SELECT * FROM orders WHERE status = 'pending';
SELECT status, COUNT(*) FROM orders GROUP BY status;

MySQL ENUM is stored internally as integers (1, 2, 3...), with small storage footprint and good performance.

Modifying Enum Definition

-- Add new value (redefine complete list)
ALTER TABLE orders MODIFY status ENUM(
  'pending',
  'processing',
  'shipped',
  'delivered',
  'cancelled',
  'refunded'  -- New addition
) NOT NULL DEFAULT 'pending';

-- Note: ALTER TABLE on large tables will lock the table
-- Recommend using tools like pt-online-schema-change

Modifying enums requires rewriting the entire definition, be especially careful on large tables.

Lookup Table Pattern (Recommended)

Use an independent table to store enum values, providing better flexibility and maintainability.

-- Create lookup table
CREATE TABLE order_statuses (
  code VARCHAR(20) PRIMARY KEY,
  label VARCHAR(100) NOT NULL,
  description TEXT,
  sort_order INT NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insert enum values
INSERT INTO order_statuses (code, label, sort_order) VALUES
  ('pending', 'Pending', 1),
  ('processing', 'Processing', 2),
  ('shipped', 'Shipped', 3),
  ('delivered', 'Delivered', 4),
  ('cancelled', 'Cancelled', 5);

-- Main table uses foreign key
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  status VARCHAR(20) NOT NULL DEFAULT 'pending',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (status) REFERENCES order_statuses(code)
);

-- Query (with JOIN)
SELECT o.*, s.label AS status_label
FROM orders o
JOIN order_statuses s ON o.status = s.code
WHERE o.status = 'pending';

-- Get all available statuses
SELECT code, label FROM order_statuses 
WHERE is_active = TRUE 
ORDER BY sort_order;

Lookup Table advantages: Easy to add descriptions, sorting, multilingual support, soft delete, etc.

ENUM vs Lookup Table Comparison

Feature ENUM Lookup Table
Storage Space Small (1-2 bytes) Large (string)
Query Performance Fast (no JOIN) Requires JOIN
Modification Flexibility Low (requires ALTER TABLE) High (INSERT/UPDATE)
Extensibility Poor (values only) Good (can add fields)
Type Safety Database level Foreign key constraint
Use Cases Fixed values, rarely changed Need descriptions, frequently changed

SQLite & SQL Server

SQLite does not support native enum types, and SQL Server doesn't have ENUM either. Typically use CHECK constraints or Lookup Tables.

SQLite CHECK Constraint

-- Use CHECK constraint to simulate enum
CREATE TABLE orders (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK(status IN (
      'pending',
      'processing',
      'shipped',
      'delivered',
      'cancelled'
    )),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Attempting to insert invalid value will fail
INSERT INTO orders (status) VALUES ('invalid');
-- Error: CHECK constraint failed

CHECK constraints provide basic value validation, but are not as powerful as true enum types.

SQL Server CHECK Constraint

-- SQL Server also uses CHECK constraint
CREATE TABLE orders (
  id INT IDENTITY(1,1) PRIMARY KEY,
  status VARCHAR(20) NOT NULL DEFAULT 'pending'
    CONSTRAINT CK_order_status 
    CHECK (status IN (
      'pending',
      'processing',
      'shipped',
      'delivered',
      'cancelled'
    )),
  created_at DATETIME2 DEFAULT GETDATE()
);

-- Modify constraint (add new value)
ALTER TABLE orders DROP CONSTRAINT CK_order_status;
ALTER TABLE orders ADD CONSTRAINT CK_order_status
  CHECK (status IN (
    'pending', 'processing', 'shipped', 
    'delivered', 'cancelled', 'refunded'
  ));

ORM & Framework Integration

ORM frameworks enable automatic synchronization between database enums and application code. Tools like Prisma, SQLAlchemy, Rails, TypeORM all provide enum support, avoiding the need to manually maintain two sets of definitions.

Prisma (TypeScript)

// schema.prisma
enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

model Order {
  id        Int          @id @default(autoincrement())
  status    OrderStatus  @default(PENDING)
  createdAt DateTime     @default(now())
}

// Generated TypeScript types
// export enum OrderStatus {
//   PENDING = "PENDING",
//   PROCESSING = "PROCESSING",
//   SHIPPED = "SHIPPED",
//   DELIVERED = "DELIVERED",
//   CANCELLED = "CANCELLED"
// }

// Usage example
const order = await prisma.order.create({
  data: {
    status: OrderStatus.PENDING
  }
});

// Query
const pending = await prisma.order.findMany({
  where: { status: OrderStatus.PENDING }
});

Prisma automatically generates TypeScript types, ensuring type safety between frontend and backend.

SQLAlchemy (Python)

from enum import Enum
from sqlalchemy import Column, Integer, Enum as SQLEnum
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class OrderStatus(str, Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class Order(Base):
    __tablename__ = 'orders'
    
    id = Column(Integer, primary_key=True)
    status = Column(
        SQLEnum(OrderStatus),
        nullable=False,
        default=OrderStatus.PENDING
    )

# Usage example
order = Order(status=OrderStatus.PENDING)
session.add(order)
session.commit()

# Query
pending_orders = session.query(Order).filter(
    Order.status == OrderStatus.PENDING
).all()

SQLAlchemy supports native enums for PostgreSQL and MySQL.

Rails (Ruby)

# app/models/order.rb
class Order < ApplicationRecord
  enum status: {
    pending: 0,
    processing: 1,
    shipped: 2,
    delivered: 3,
    cancelled: 4
  }
end

# Auto-generated methods
order.pending?       # Check status
order.processing!    # Set status
Order.pending        # Query scope
Order.statuses       # Get all statuses

# Migration file
# db/migrate/xxx_create_orders.rb
class CreateOrders < ActiveRecord::Migration[7.0]
  def change
    create_table :orders do |t|
      t.integer :status, default: 0, null: false
      t.timestamps
    end
    
    add_index :orders, :status
  end
end

Rails enum uses integer storage, providing convenient query and status check methods.

TypeORM (TypeScript)

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

export enum OrderStatus {
  PENDING = "pending",
  PROCESSING = "processing",
  SHIPPED = "shipped",
  DELIVERED = "delivered",
  CANCELLED = "cancelled"
}

@Entity()
export class Order {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    type: "enum",
    enum: OrderStatus,
    default: OrderStatus.PENDING
  })
  status: OrderStatus;

  @Column()
  createdAt: Date;
}

// Usage example
const order = new Order();
order.status = OrderStatus.PENDING;
await orderRepository.save(order);

// Query
const pending = await orderRepository.find({
  where: { status: OrderStatus.PENDING }
});

TypeORM supports native enum types for PostgreSQL and MySQL.

Migration Strategies & Best Practices

Enum value changes require careful handling, especially for deletion or renaming operations. Following proper migration procedures can avoid data loss and system downtime.

Adding Enum Values

Risk: Low

Backward compatible, safe to deploy

  1. 1. Add new enum value in database
  2. 2. Deploy application code update
  3. 3. Verify new value is available

Renaming Enum Values

Risk: Medium

Requires phased deployment

  1. 1. Add enum value with new name
  2. 2. Deploy code compatible with both old and new values
  3. 3. Migrate database data
  4. 4. Remove old code references
  5. 5. Delete old enum value

Deleting Enum Values

Risk: High

Must ensure no data references

  1. 1. Check usage in database
  2. 2. Migrate or delete related data
  3. 3. Remove code references
  4. 4. Delete enum value from database
  5. 5. Verify system operates normally

Migration Script Template

-- === Safe Enum Migration Process ===

-- Phase 1: Check current usage
SELECT status, COUNT(*) as count
FROM orders
GROUP BY status;

-- Phase 2: Add new enum value (if needed)
ALTER TYPE order_status ADD VALUE IF NOT EXISTS 'refunded';

-- Phase 3: Migrate data (if renaming or deleting)
UPDATE orders 
SET status = 'cancelled'
WHERE status = 'old_status_name';

-- Phase 4: Verify migration result
SELECT status, COUNT(*) as count
FROM orders
WHERE status = 'old_status_name';
-- Should return 0

-- Phase 5: Create backup point
-- CHECKPOINT; or export data

-- Phase 6: Execute delete operation (if needed)
-- Note: PostgreSQL does not support directly deleting enum values
-- Need to use the multi-step process mentioned earlier

Database Enum Playbook

Cheat Sheet

PostgreSQL

  • • CREATE TYPE name AS ENUM (values)
  • • ALTER TYPE name ADD VALUE 'new_value'
  • • Does not support deleting values, need to rebuild type

MySQL

  • • ENUM('val1', 'val2') as column type
  • • ALTER TABLE ... MODIFY COLUMN to modify
  • • Large table changes will lock table, watch performance

General Recommendations

  • • String enums better than numeric indices
  • • Consider Lookup Table for frequent changes
  • • Always validate in test environment

Team Code Review Checklist

Type Safety

  • • Database enums consistent with application code definitions
  • • Use NOT NULL constraint
  • • Set reasonable DEFAULT values

Migration Safety

  • • Check data usage before migration
  • • Phased deployment, support rollback
  • • Use online tools for large table changes

Performance Optimization

  • • Create index on enum columns
  • • Avoid frequent type conversions
  • • Cache Lookup Table appropriately

Anti-pattern: Directly deleting enum values

-- Dangerous: Delete without checking data
ALTER TYPE order_status DROP VALUE 'old_status';
-- PostgreSQL does not support this operation

-- Or directly MODIFY in MySQL
ALTER TABLE orders MODIFY status 
  ENUM('new', 'values', 'only');
-- Will cause existing data to become empty string

Deleting enum values without checking existing data may cause data loss or inconsistency.

Correct: Safe migration process

-- 1. Check usage
SELECT status, COUNT(*) FROM orders 
WHERE status = 'old_status';

-- 2. Migrate data
UPDATE orders 
SET status = 'new_status'
WHERE status = 'old_status';

-- 3. Verify no leftover data
-- 4. Update application code
-- 5. Finally delete type definition

Complete migration process ensures data safety and system stability.

Test Template

-- Data integrity test suite

-- Test 1: Verify enum constraint
INSERT INTO orders (status) VALUES ('invalid_status');
-- Should fail

-- Test 2: Verify default value
INSERT INTO orders DEFAULT VALUES;
SELECT status FROM orders WHERE id = LAST_INSERT_ID();
-- Should return default value 'pending'

-- Test 3: Verify all enum values available
INSERT INTO orders (status) VALUES ('pending');
INSERT INTO orders (status) VALUES ('processing');
INSERT INTO orders (status) VALUES ('shipped');
-- All should succeed

-- Test 4: Verify query performance
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
-- Check if index is used

-- Test 5: Verify ORM synchronization
-- Check in application code
-- OrderStatus.values() == database_enum_values

Framework Integration Examples

Django + PostgreSQL
# models.py
from django.db import models

class OrderStatus(models.TextChoices):
    PENDING = 'pending', 'Pending'
    PROCESSING = 'processing', 'Processing'
    SHIPPED = 'shipped', 'Shipped'
    DELIVERED = 'delivered', 'Delivered'
    CANCELLED = 'cancelled', 'Cancelled'

class Order(models.Model):
    status = models.CharField(
        max_length=20,
        choices=OrderStatus.choices,
        default=OrderStatus.PENDING
    )
    
# migrations/xxxx_create_enum.py
from django.db import migrations

class Migration(migrations.Migration):
    operations = [
        migrations.RunSQL(
            "CREATE TYPE order_status AS ENUM "
            "('pending', 'processing', 'shipped', 'delivered', 'cancelled')"
        )
    ]
Laravel + MySQL
// Migration
Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->enum('status', [
        'pending',
        'processing',
        'shipped',
        'delivered',
        'cancelled'
    ])->default('pending');
    $table->timestamps();
});

// Model
class Order extends Model
{
    protected $casts = [
        'status' => OrderStatus::class
    ];
}

// Enum (PHP 8.1+)
enum OrderStatus: string
{
    case PENDING = 'pending';
    case PROCESSING = 'processing';
    case SHIPPED = 'shipped';
    case DELIVERED = 'delivered';
    case CANCELLED = 'cancelled';
}

Database Enum Toolkit

Practical tools and resources to help you better manage database enums.

Migration Tools

  • Enum Migration Planner - Interactive migration planning tool
  • • Flyway / Liquibase - Database version management
  • • pt-online-schema-change - MySQL online schema change
  • • Alembic - Python SQLAlchemy migrations

Check Scripts

  • Enum Serializer Checker - Serialization compatibility check
  • • Data consistency validation SQL
  • • Enum synchronization check scripts
  • • Performance benchmark tests

Database Enum FAQ

What is an enum in SQL?

An enum in SQL is a set of constants that restrict column values. PostgreSQL and MySQL natively support enum types, while other databases implement similar functionality through CHECK constraints or lookup tables.

Can PostgreSQL delete enum values?

PostgreSQL does not support directly deleting enum values. You need to create a new type, migrate data, and drop the old type—a multi-step process. We recommend using our Migration Planner tool.

MySQL enum vs string column, how to choose?

Enums save storage space and have built-in constraint checks, but changes are costly; string columns are more flexible and can be used with CHECK constraints. Use ENUM if enum values rarely change and performance is critical; use string + constraint or Lookup Table if frequent modifications are needed.

How to keep database enums synchronized in ORM?

Use ORM migration tools (Prisma Migrate, Alembic, Rails migrations) to manage changes. Add checks in CI to ensure application enum definitions match the database. Tools like Prisma can automatically generate types, reducing manual synchronization.

What advantages does Lookup Table have over native enums?

Lookup Table provides better flexibility: can add fields like descriptions, sorting, multilingual support, soft delete; modifications don't require ALTER TABLE; supports complex business logic. The cost is needing JOINs in queries, but performance impact is minimal for most applications.

How to integrate database enums with API documentation?

Use the enum field in OpenAPI/Swagger specifications to list all available values. Many ORMs (like Prisma, TypeORM) can automatically generate API schemas. Use JSON Schema to validate request data, ensuring clients pass valid enum values.