Skip to content

Changing a table without downtime

How to change a big table while the app keeps running: add the new column first, move the data in small steps, and remove the old column last.

  • Lesson 25 of 125
  • 12 min read
  • Depth
  • Free to read, no account
On this page
  1. Why interviewers ask this
  2. The idea in plain words
  3. Pause and predict
  4. Worked example
  5. Trade-offs
  6. Common misconceptions
  7. How to say it in the interview
  8. Check your understanding
  9. Answers
  10. Sources

What you need first

This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.

01Why interviewers ask this

Interviewers ask: "The orders table has 40 million rows and the site must stay up. How do you change a column?" They check that you know a change can block users.

A weak answer says "run it at night". A strong answer uses small steps. Each is safe, and all but the last can be undone.

02The idea in plain words

Think about a shop that must stay open while you rebuild it. You build a new door beside the old one, move the customers across, then close the old door.

A migration is a change to the structure of a table, such as a new column. It is online when users keep working. Two dangers make it risky:

  1. A lock and a rewrite. Some changes take a strong lock: other queries on the table must wait. Some also rewrite the table: the database copies every row. In Postgres, a strong lock blocks even reads, and a change of column type normally rewrites the table. The time grows with the rows.
  2. Old and new code together. You update the servers one at a time. This is a release. For a short time old and new code both run, and both must work with the table. A column rename breaks the old code at once.

The fix is expand, then contract:

  1. Expand. Add the new column beside the old one. Nothing that the old code uses changes.
  2. Move. New code writes to both columns. A backfill copies the old rows into the new column in small groups of rows, called batches, with a pause between them. Then new code reads the new column.
  3. Contract. When no code uses the old column, remove it.

Each step is safe alone. Only the last cannot be undone, so it comes last. Cheap and costly changes in Postgres:

ChangeCost
Add a column, with no default or a fixed defaultFast
Drop a columnFast: it is only hidden
Change a column typeUsually a rewrite and a strong lock
Add an indexBlocks writes, unless CONCURRENTLY (lesson 20)
Set NOT NULLUsually scans the whole table under a strong lock

Also set a lock wait limit (in Postgres, lock_timeout), so the change stops instead of waiting forever.

Stuck on something? The AI tutor has read this lesson. Ask it with a free account

03Pause and predict

Pause. Guess first.

A type change rewrites 40,000,000 rows at 50,000 rows per second and locks the table. How long is it blocked? How many new orders, at 120 per second, arrive meanwhile?

Write your guess before you read on. The answer is in the worked example.

04Worked example

The orders table of lesson 20 has 40,000,000 rows. At the peak, 120 new orders arrive per second. The column total is an integer. We want bigger totals, so we move to bigint.

Assumptions (made up to teach with): a rewrite copies 50,000 rows per second. A batch of 10,000 rows takes 200 ms, and we pause 200 ms after it. A bigint takes 8 bytes.

Step 1: change in place. 40,000,000 / 50,000 = 800 seconds, which is 13 minutes 20 seconds, with the table locked. In that time 120 x 800 = 96,000 orders wait or fail. This answers the predict box.

Step 2: expand, then contract.

sql
-- Expand (milliseconds; gives up if it cannot get the lock in 2 s)
SET lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN total_big bigint;

-- Release A: code writes total and total_big, and still reads total.

-- Backfill: run again and again, one id range at a time
UPDATE orders SET total_big = total
 WHERE id BETWEEN 1 AND 10000 AND total_big IS NULL;

-- Check: rows where the two columns differ; expect 0 (a read-only scan)
SELECT count(*) FROM orders WHERE total_big IS DISTINCT FROM total;

-- Release B: code reads total_big. Release C: code stops writing total.
ALTER TABLE orders DROP COLUMN total;

Step 3: the numbers.

  • Batches: 40,000,000 / 10,000 = 4,000.
  • Time: 4,000 x (200 + 200) ms = 1,600 seconds, about 27 minutes. That is twice Step 1. But each batch locks only 10,000 rows for 0.2 seconds.
  • Extra space during the move: 40,000,000 x 8 bytes = 320 MB. Each update leaves a dead row (lesson 20), which vacuum cleans in the pauses. Dropping a column does not return its space at once.

If Release B shows a fault, go back to Release A. The old column is still correct.

05Trade-offs

  • Expand, then contract solves a blocked table and a change that cannot be undone. It costs three releases, more time, code that writes twice, and 320 MB of extra space. I would use a plain change for a small table, or when a short stop is fine.

06Common misconceptions

"A rename is instant, so it is safe." The database part is instant. Old servers still ask for the old name.

"One big UPDATE is the quick way to backfill." It locks every row it changes until it ends. It also leaves 40,000,000 dead rows for vacuum (lesson 20). Use batches.

"Drop the old column as soon as the switch works." A rollback needs it. Drop it when no code uses it.

07How to say it in the interview

Say it like this

I would add the new column first, write to both, copy old rows in small batches, switch the reads, and drop the old column last. It costs three releases and some extra space, but no step blocks users for long, and I can go back until the last step.

08Check your understanding

  1. Name the three phases of expand then contract. Why is a one-step rename risky?
  2. A table has 10,000,000 rows. Batches have 5,000 rows, 100 ms of work and 100 ms of pause. A rewrite copies 50,000 rows per second. (a) How many batches? (b) How long is the backfill? (c) How long does a rewrite block the table?
  3. A team switches reads to the new column while the backfill is half done. What goes wrong, and what check prevents it?

09Answers

Show the answers
  1. Expand (add the column), move (write to both, backfill, read the new column), contract (remove the old one). Old code still runs and asks for the old name, so it fails.
  2. (a) 10,000,000 / 5,000 = 2,000 batches. (b) 2,000 x 200 ms = 400 seconds. (c) 10,000,000 / 50,000 = 200 seconds. The backfill is twice as long, but blocks nobody.
  3. Rows not yet copied have an empty new column, so users see wrong totals. Count the rows that differ, and switch only when the count is 0.

10Sources

Try it now, no sign-up

Ask the AI tutor about this lesson

The tutor has read this lesson. Ask in your own words, any time. Free account, 20 questions a day.

Reading never needs an account. The account is for the tutor, the quiz, your notes and your progress.

All lessons in Module 4: Databases: first steps