Migrations Basics
7 examples for schema migration fundamentals - 5 basic and 2 intermediate. Versioned SQL in git is the single source of truth for what production looks like.
Search across all documentation pages
7 examples for schema migration fundamentals - 5 basic and 2 intermediate. Versioned SQL in git is the single source of truth for what production looks like.
mkdir -p db/migrations
git init # migrations live beside application codeflyway_schema_history, liquibase.databasechangelog, or custom).-- db/migrations/V001__init_schema.sql
CREATE SCHEMA app AUTHORIZATION app_owner;
CREATE TABLE app.users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now()
);V001, timestamp prefix, or tool convention).IF NOT EXISTS) are tool-specific - Flyway versioned migrations are not re-run.CREATE TABLE IF NOT EXISTS app.schema_migrations (
version text PRIMARY KEY,
applied_at timestamptz NOT NULL DEFAULT now(),
checksum text
);Related: Flyway & Liquibase - enterprise runners
CREATE ROLE app_owner NOLOGIN;
CREATE ROLE app_migrator LOGIN PASSWORD 'vault';
GRANT app_owner TO app_migrator;
-- Migration session
SET ROLE app_owner;
CREATE TABLE app.orders (id bigint PRIMARY KEY);
RESET ROLE;app_api should not own tables or hold DDL rights.SET ROLE lines belong in migration wrapper or connection init for migrator only.BEGIN;
CREATE TABLE app.feature_flags (
key text PRIMARY KEY,
enabled boolean NOT NULL DEFAULT false
);
INSERT INTO app.feature_flags (key) VALUES ('new_checkout');
COMMIT;CREATE INDEX CONCURRENTLY, DROP INDEX CONCURRENTLY, VACUUM - cannot run inside transaction block.CREATE TABLE app.products (id bigint PRIMARY KEY, sku text NOT NULL);
GRANT SELECT ON app.products TO app_readers;
GRANT SELECT, INSERT, UPDATE, DELETE ON app.products TO app_writers;ALTER DEFAULT PRIVILEGES from bootstrap migration.-- Expand: add nullable column (safe deploy)
ALTER TABLE app.orders ADD COLUMN discount_cents integer;
-- App v2 writes column; backfill job fills values
-- Contract (later migration): enforce NOT NULL after backfill
ALTER TABLE app.orders ALTER COLUMN discount_cents SET NOT NULL;Related: Expand/Contract Pattern - full workflow
# pipeline excerpt
services:
postgres:
image: postgres:18.4
steps:
- run: flyway migrate -url=jdbc:postgresql://postgres:5432/test
- run: psql $TEST_URL -f ci/assert_schema.sqlStack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 19, 2026