Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 9 – Creating tables and constraints

ALTER TABLE and safe schema migrations

Change tables with ALTER TABLE in PostgreSQL, MySQL and SQLite, rebuild SQLite tables safely, and ship schema changes without downtime by expand and contract.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: What ALTER TABLE can do in each engine · Schema changes and transactions · Safe schema migrations · Your turn. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Add, rename and drop columns and constraints with ALTER TABLE
  • Explain the limits of ALTER TABLE in SQLite and rebuild a table safely
  • Predict how schema changes behave inside a transaction in each engine
  • Plan a change without downtime with expand and contract and batched backfills

Before you start

What you’ll learn · Before you start

What you will learn

  • Add, rename and drop columns and constraints with ALTER TABLE
  • Explain the limits of ALTER TABLE in SQLite and rebuild a table safely
  • Predict how schema changes behave inside a transaction in each engine
  • Plan a change without downtime with expand and contract and batched backfills

Before you start

On this page

ALTER TABLE changes a table that already exists: it adds, renames and drops columns, adds and removes constraints, and changes a column’s type. On a laptop each change takes a moment. On a production table that thousands of requests read every second, the same statement can lock the table, rewrite every row, or break the version of the application that is still running. Safe schema migrations avoid that with three habits: every change is a versioned file that is applied once, in order; each step leaves a schema that both the old and the new code can use; and slow work, such as filling a new column, happens in small batches. This lesson shows what ALTER TABLE can do in each engine, where SQLite needs a table rebuild instead, how each engine treats schema changes inside a transaction, and the expand-and-contract plan that makes changes without downtime. Its examples use SQLite 3.49.1 as packaged in sql.js 1.14.2, so each one runs right here.

What ALTER TABLE can do in each engine

Five changes SQLite makes in place, and four it refuses SQL · alter_basics.sql
-- The four changes SQLite 3.49.1 can make in place, and four it refuses.
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT, fax TEXT);
INSERT INTO customers (name, city) VALUES ('Asha Kulkarni', 'Pune'), ('Vikram Nair', 'Kochi');

ALTER TABLE customers ADD COLUMN phone TEXT;                              -- a new, empty column
ALTER TABLE customers ADD COLUMN tier TEXT NOT NULL DEFAULT 'basic';      -- required, so it needs a default
ALTER TABLE customers RENAME COLUMN city TO town;
ALTER TABLE customers DROP COLUMN fax;
ALTER TABLE customers RENAME TO clients;

ALTER TABLE clients ADD COLUMN state_code TEXT NOT NULL;                  -- refused: required without a default
ALTER TABLE clients ADD COLUMN joined_at TEXT DEFAULT CURRENT_TIMESTAMP;  -- refused: the default is not constant
ALTER TABLE clients ADD COLUMN email TEXT UNIQUE;                         -- refused: UNIQUE needs a new table
ALTER TABLE clients ALTER COLUMN phone SET NOT NULL;                      -- no ALTER COLUMN before SQLite 3.53.0

SELECT * FROM clients;
SELECT name, type, "notnull", dflt_value FROM pragma_table_info('clients');

Output (exit status 1)

┌─────────────┬───────────────┬───────┬───────┬───────┐
│ customer_id │     name      │ town  │ phone │ tier  │
├─────────────┼───────────────┼───────┼───────┼───────┤
│ 1           │ Asha Kulkarni │ Pune  │       │ basic │
│ 2           │ Vikram Nair   │ Kochi │       │ basic │
└─────────────┴───────────────┴───────┴───────┴───────┘
┌─────────────┬─────────┬─────────┬────────────┐
│    name     │  type   │ notnull │ dflt_value │
├─────────────┼─────────┼─────────┼────────────┤
│ customer_id │ INTEGER │ 0       │            │
│ name        │ TEXT    │ 1       │            │
│ town        │ TEXT    │ 0       │            │
│ phone       │ TEXT    │ 0       │            │
│ tier        │ TEXT    │ 1       │ 'basic'    │
└─────────────┴─────────┴─────────┴────────────┘

Printed as an error (standard error)

Runtime error near line 11: Cannot add a NOT NULL column with default value NULL
Runtime error near line 12: Cannot add a column with non-constant default
Parse error near line 13: Cannot add a UNIQUE column
Parse error near line 14: near "ALTER": syntax error

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < alter_basics.sql

SQLite added two columns, renamed a column and the table, and dropped a column. It refused a required column without a default, a default that is not constant, a UNIQUE column, and ALTER COLUMN, which SQLite added only in version 3.53.0, for SET NOT NULL and DROP NOT NULL. Everything SQLite cannot change in place is done by building a new table and copying the rows across, a procedure of twelve steps that the SQLite manual spells out. PostgreSQL and MySQL change most of these in place:

What ALTER TABLE changes in place
Criterion SQLite 3.49PostgreSQL 18MySQL 8.4
ADD COLUMN, RENAME COLUMN, DROP COLUMN, RENAME TO Yes, with limits on what a new column may declareYesYes
Add a NOT NULL column to a table with rows Only with a constant defaultYes; with a default, existing rows get itYes; with a default, existing rows get it
Add or drop a UNIQUE, CHECK or FOREIGN KEY constraint No: rebuild the tableYes: ADD CONSTRAINT and DROP CONSTRAINTYes, as ADD CONSTRAINT or ADD INDEX
Change a column type or NOT NULL No: rebuild the table (NOT NULL from 3.53.0)Yes: ALTER COLUMNYes: MODIFY COLUMN
Schema changes inside a transaction Rolled back with itRolled back with itEach one commits at once

Schema changes and transactions

A migration rolled back as a whole SQL · ddl_rollback.sql
-- In SQLite, as in PostgreSQL, schema changes are part of the transaction.
CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL);

BEGIN;
ALTER TABLE customers ADD COLUMN loyalty_points INTEGER NOT NULL DEFAULT 0;
CREATE TABLE loyalty_events (event_id INTEGER PRIMARY KEY, customer_id INTEGER, points INTEGER);
INSERT INTO customers (name) VALUES ('Asha Kulkarni');
ROLLBACK;   -- the migration failed a check later on: undo all of it

SELECT name FROM sqlite_schema WHERE type = 'table';
SELECT name FROM pragma_table_info('customers');
SELECT count(*) AS customers FROM customers;

Output

┌───────────┐
│   name    │
├───────────┤
│ customers │
└───────────┘
┌─────────────┐
│    name     │
├─────────────┤
│ customer_id │
│ name        │
└─────────────┘
┌───────────┐
│ customers │
├───────────┤
│ 0         │
└───────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < ddl_rollback.sql

The rollback took back the new column, the new table and the inserted row together, so the database looked as if the migration had never started. PostgreSQL behaves the same way, which lets a migration run as one transaction that either finishes or leaves nothing behind; on PostgreSQL 18.6 a rolled-back ADD COLUMN and CREATE TABLE left no trace. MySQL does not: a CREATE TABLE or an ALTER TABLE commits the open transaction first. On MySQL 8.4.11 an INSERT made before a CREATE TABLE survived the ROLLBACK, and so did an INSERT made after it, because once the transaction has been committed the session is back in autocommit mode. A MySQL migration that fails halfway therefore stays half applied, which is why such migrations make one schema change per step.

Safe schema migrations

Migration tools differ, but safe schema migrations share one shape. Each change is a numbered file, such as 0042_add_state_code.sql, kept in the same repository as the code. The database records which files it has applied, in a table of its own or, in SQLite, in PRAGMA user_version, so every environment applies the same files in the same order exactly once. A change that the running code cannot survive, such as a rename, is split into steps that each keep both versions of the code working:

Expand and contract in five steps: add the new column, backfill it, switch reads, switch writes, then drop the old column.1. Expandadd the column town,and copy every new writeof city into it2. Backfillfill town for old rows,in small batches3. Switch readsdeploy code thatreads town4. Switch writesdeploy code thatwrites only town5. Contractdrop the copyingand the column cityold codestill workscheck: no rowdiffersold and new codeboth worknothing usescity any more

Renaming a column in five safe steps

Text description of the diagram

The diagram shows how to rename the column city of customers to town without stopping the application, from top to bottom. Each step is a separate migration or deploy.

  1. Expand: add the new column town, and copy every new write of city into it, for example with a trigger. The code that is running still reads and writes city, so it keeps working.
  2. Backfill: fill town for the rows that existed before, in small batches. Before going on, check that no row has a town that differs from its city.
  3. Switch reads: deploy code that reads town. Both the old and the new code keep working, because both columns hold the same values.
  4. Switch writes: deploy code that writes only town.
  5. Contract: when nothing reads or writes city any more, drop the copying and then the column city.

Each step can be deployed and, if something goes wrong, reversed on its own. The cost is time: a rename becomes five releases instead of one, and the database holds both columns for a while.

Two more rules keep the history of migrations trustworthy. Never edit a migration that has run anywhere, even on a colleague’s laptop: correct it with a new one, or the environments drift apart without anyone noticing. And write the reverse of each step where one exists, while treating the steps that destroy data, such as dropping a column, as one-way: they run last, after a backup, when nothing reads the old data any more.

Your turn

Exercise · Medium · SQL

A migration that adds a required column

Each test creates a table customers (customer_id, name, city) with some rows, and a lookup table city_states (city, state_code) that gives the two-letter state code of every city in it.

Write a migration for SQLite that adds a column state_code to customers:

  • it is required: SQLite must record it as NOT NULL;
  • every existing customer gets the state code of their city from city_states;
  • a customer whose city is not in city_states, or has no city, gets 'XX', so that someone can fix it later.

Leave the last statement of the starter code as it is: it returns every customer with their state code and, from SQLite's catalog, whether the column is NOT NULL (1) or not (0).

Starter code · migration.sql

-- The migration: add the required column state_code and fill it in for every existing customer.


-- Leave this as it is: every customer, their state code, and whether SQLite records the column as NOT NULL.
SELECT customer_id, state_code,
       (SELECT "notnull" FROM pragma_table_info('customers') WHERE name = 'state_code') AS required
FROM customers
ORDER BY customer_id;
The sample tests · tests.yaml
# Sample tests: each one creates customers and city_states, runs your migration and compares the rows it returns, in
# order.
tests:
  - name: fills the state code from the city, and marks the column NOT NULL
    ordered: true
    setup: |
      CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT);
      CREATE TABLE city_states (city TEXT PRIMARY KEY, state_code TEXT NOT NULL);
      INSERT INTO customers VALUES (1, 'Asha Kulkarni', 'Pune'), (2, 'Vikram Nair', 'Kochi'), (3, 'Meera Rathore', 'Jaipur');
      INSERT INTO city_states VALUES ('Pune', 'MH'), ('Kochi', 'KL'), ('Jaipur', 'RJ');
    expected:
      columns: [customer_id, state_code, required]
      rows:
        - [1, 'MH', 1]
        - [2, 'KL', 1]
        - [3, 'RJ', 1]
  - name: gives XX to an unknown city and to a missing one
    ordered: true
    setup: |
      CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT);
      CREATE TABLE city_states (city TEXT PRIMARY KEY, state_code TEXT NOT NULL);
      INSERT INTO customers VALUES (1, 'Tenzin Dorjee', 'Gangtok'), (2, 'Ira Sen', 'Atlantis'), (3, 'Kabir Das', NULL);
      INSERT INTO city_states VALUES ('Gangtok', 'SK'), ('Pune', 'MH');
    expected:
      columns: [customer_id, state_code, required]
      rows:
        - [1, 'SK', 1]
        - [2, 'XX', 1]
        - [3, 'XX', 1]
A hint

SQLite adds a NOT NULL column only together with a non-NULL default, so the ADD COLUMN can give 'XX' as the default. Then an UPDATE with a subquery on city_states replaces it wherever the city is known.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 Which of these statements does SQLite 3.49.1 accept on a table that already holds rows?

    Choose one answer.

    Show the answer to question 1

    Answer: ALTER TABLE customers ADD COLUMN tier TEXT NOT NULL DEFAULT 'basic'

    A NOT NULL column needs a non-NULL constant default, which 'basic' is. SQLite refuses a required column without a default, a UNIQUE column and a default that is not constant, such as CURRENT_TIMESTAMP; those changes need a rebuilt table.

  2. Question 2 of 3 A MySQL session runs START TRANSACTION, an INSERT, a CREATE TABLE, a second INSERT, and then ROLLBACK. What is left afterwards?

    Choose one answer.

    Show the answer to question 2

    Answer: Both inserted rows and the new table

    CREATE TABLE commits the open transaction before it runs, so the first INSERT is committed. The session is then back in autocommit mode, so the second INSERT commits on its own, and ROLLBACK has nothing left to undo.

  3. Question 3 of 3 Put the steps of renaming a column without downtime in order.

    Give each item its position, from 1 (first).

    Show the answer to question 3

    Answer:

    1. Add the new column, and copy every new write of the old column into it
    2. Fill the new column for the old rows, in batches
    3. Deploy code that reads the new column
    4. Deploy code that writes only the new column
    5. Drop the copying and the old column

    Expand first, so that the running code keeps working; backfill and check; switch reads, then writes; and contract only when no deployed code uses the old column. Every step leaves a schema that the code before and after it can use.

SQL Formatter Lay out a long migration script so that every statement and its clauses are easy to read.

The SQLite table rebuild, step by step, and the shortcut that breaks it Pro: opens with a Pro, Premium or Ultimate pass

SQLite changes everything beyond its few in-place forms by building a new table and moving the data across. The order of the steps matters, and the obvious shortcut quietly breaks the foreign keys of other tables.

In this part:

  • Twelve steps, run on a real table
  • Renaming the old table first, the failure reproduced
  • Recording the version in the same transaction
  • What one rebuild costs

3 runnable examples (SQL) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Changes without downtime: locks, costs and expand and contract, with numbers Pro: opens with a Pro, Premium or Ultimate pass

On a busy server the danger of a schema change is rarely the change itself; it is the lock it waits for, the rows it rewrites, and the code that still expects the old schema. This part measures each and works through a rename from start to finish.

In this part:

  • The lock queue, reproduced
  • What each change costs
  • Expand and contract, worked through

2 runnable examples (Python, SQL) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Mid to senior

    You must rename orders.amount to amount_paise in a PostgreSQL database that serves traffic day and night, with several application servers that are deployed one by one. Plan the change so that no request fails at any point.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A migration that only added a nullable column to a busy PostgreSQL table took the site down for a minute: every query on that table hung until the migration finished. Adding such a column rewrites nothing, so what happened, and how do you run migrations like it safely?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A MySQL migration file adds a column, fills it with an UPDATE, then creates a unique index that fails on a duplicate. The tool ran the file in a transaction and rolled back on the error, yet the column exists and is filled. Why, and how should MySQL migrations be written instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • ALTER TABLE adds, renames and drops columns everywhere, and adds constraints and changes types in PostgreSQL and MySQL; SQLite rebuilds the table for most other changes.
  • Schema changes are transactional in PostgreSQL and SQLite, while in MySQL each one commits at once.
  • Safe schema migrations are versioned files applied once, in order, whose every step works with the old and the new code.
  • Expand and contract turns a breaking change, such as a rename, into steps that can each be deployed and undone.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.