PgDeck

Manage PostgreSQL table columns

Adding, renaming, and dropping PostgreSQL columns.

Every ALTER TABLE looks equally simple in SQL, and they are not equally safe. Some finish instantly on any table size; others rewrite every row while holding a lock that blocks reads and writes.

Short answer: ADD COLUMN with a constant default is instant on modern PostgreSQL. RENAME COLUMN is instant. DROP COLUMN is instant but does not reclaim the space. ALTER COLUMN TYPE usually rewrites the whole table under an ACCESS EXCLUSIVE lock, which is the one that causes outages. Always set lock_timeout before altering a busy table.

PgDeck PostgreSQL desktop client with schema navigation, table filtering, row editing, and SQL tools

Why developers use PgDeck

PgDeck is shaped around PostgreSQL tasks that happen every day: connect, inspect, filter, edit, query, manage structure, copy, and export.

ADD COLUMN with a constant default is instant; a volatile default is not
ALTER COLUMN TYPE usually rewrites the table under an exclusive lock
A pending ALTER queues every read behind it, not just writes
lock_timeout turns a potential outage into a retryable error
DROP COLUMN does not reclaim disk space until rows are rewritten

Secure connections

Save PostgreSQL connection details with encrypted passwords, SSL settings, URL import, testing, search, edit, and delete actions.

Schema and data grid

Browse schemas and tables, search the table list, pin favorites, filter rows, sort columns, resize columns, and paginate results.

SQL workspace

Run SELECT and non-SELECT statements from multi-tab SQL editors with syntax highlighting and interactive query results.

Staged editing

Edit cells, add rows, duplicate rows, set NULL, stage deletes, review pending changes, and commit everything in one transaction.

How to change PostgreSQL table columns safely

  1. Add a column

    Since PostgreSQL 11, adding a column with a constant default no longer rewrites the table — the default is stored as metadata and applied on read. This is instant regardless of table size. A volatile default such as now() or random() still forces a full rewrite, because each row needs its own value.

    -- Instant on any table size
    ALTER TABLE users ADD COLUMN country text;
    ALTER TABLE users ADD COLUMN plan text NOT NULL DEFAULT 'free';
    
    -- Rewrites every row: volatile default
    ALTER TABLE users ADD COLUMN token uuid NOT NULL DEFAULT gen_random_uuid();
  2. Set a lock timeout first on anything busy

    ALTER TABLE needs an ACCESS EXCLUSIVE lock. If a long-running query holds the table, your ALTER waits — and every query arriving after it queues behind your pending lock, including reads. That is how a one-second migration takes a site down. A short lock_timeout makes the ALTER fail fast instead.

    SET lock_timeout = '3s';
    ALTER TABLE orders ADD COLUMN note text;
    -- If it cannot get the lock in 3s it errors out, and you retry later
  3. Rename a column

    RENAME is a catalog change only, so it is instant on any table size. The risk is not the database, it is the application: every query referencing the old name breaks the moment it commits. For zero-downtime, add the new column, write to both, migrate reads, then drop the old one.

    ALTER TABLE users RENAME COLUMN signup_date TO created_at;
  4. Change a column type carefully

    Most type changes rewrite the entire table and hold an exclusive lock for the duration — minutes or hours on a large table. Some widening conversions are exempt, such as varchar(50) to varchar(100) or to text. When a cast is not implicit, USING tells PostgreSQL how to convert.

    -- No rewrite: widening within the same type
    ALTER TABLE users ALTER COLUMN name TYPE text;
    
    -- Full rewrite under ACCESS EXCLUSIVE: test the duration first
    ALTER TABLE events ALTER COLUMN payload TYPE jsonb USING payload::jsonb;
  5. Add NOT NULL without a full scan

    SET NOT NULL normally scans the whole table to verify no nulls exist. On PostgreSQL 12 and later you can avoid that: add a validated CHECK constraint first — NOT VALID then VALIDATE, which takes only a share lock — and SET NOT NULL will then use it as proof and skip the scan.

    ALTER TABLE users ADD CONSTRAINT users_country_nn
      CHECK (country IS NOT NULL) NOT VALID;
    ALTER TABLE users VALIDATE CONSTRAINT users_country_nn;  -- no exclusive lock
    ALTER TABLE users ALTER COLUMN country SET NOT NULL;     -- now instant
    ALTER TABLE users DROP CONSTRAINT users_country_nn;
  6. Drop a column

    DROP COLUMN is instant because PostgreSQL only marks the column dropped in the catalog — the data stays in each row until that row is rewritten. Disk space is reclaimed gradually by normal updates, or immediately by VACUUM FULL, which takes an exclusive lock and needs free space equal to the table.

    ALTER TABLE users DROP COLUMN legacy_flag;
    
    -- Space is only reclaimed on rewrite:
    VACUUM FULL users;   -- ACCESS EXCLUSIVE, needs room for a full copy

The lock queue is what actually causes outages

People assume a slow ALTER only blocks other writers. It is worse than that: when your ALTER waits for an ACCESS EXCLUSIVE lock, every subsequent query on that table queues behind your request, including plain SELECTs. One long-running report can therefore turn a trivial ALTER into a total stall on that table. Setting lock_timeout means you fail and retry instead of taking the application down.

Which operations rewrite the table

Rewrites: most ALTER COLUMN TYPE changes, and ADD COLUMN with a volatile default. No rewrite: ADD COLUMN with a constant default or no default, RENAME COLUMN, DROP COLUMN, SET DEFAULT, DROP NOT NULL, and widening conversions within a type family such as varchar to text. Knowing which list an operation is on is the entire difference between a safe migration and a risky one.

Renaming is a deployment problem, not a database one

The rename itself is instant and safe. What breaks is every deployed application instance still sending the old column name. The standard zero-downtime pattern is expand then contract: add the new column, write to both from the application, backfill, switch reads, deploy, then drop the old column in a later release. Renaming in place is fine in development and rarely fine in production.

Backfilling large tables in batches

A single UPDATE touching millions of rows holds locks and generates a huge amount of WAL. Batch it: update a bounded set of rows at a time in separate transactions, with a short pause between batches. It takes longer overall and it does not block anything, which is the trade you want on a production database.

Structure changes deserve a preview

ALTER TABLE and DROP COLUMN execute as readily as a SELECT and cannot be corrected by another statement afterwards. Reading the exact SQL before it runs, and taking a backup when the table matters, is the whole safety procedure. Clients that show the generated SQL and require confirmation for structure actions exist because this is a common way to lose data.

Related PostgreSQL guides

More on choosing, comparing, and running a PostgreSQL desktop client.

How do I add a column to a PostgreSQL table?

ALTER TABLE tablename ADD COLUMN colname type. On PostgreSQL 11 and later this is instant even with a constant default, because the default is stored as metadata rather than written to every row. A volatile default such as gen_random_uuid() does force a full table rewrite.

Does ALTER TABLE lock the table?

Yes, it takes an ACCESS EXCLUSIVE lock. If another query is holding the table, your ALTER waits — and every query arriving afterwards queues behind it, including reads. Set lock_timeout to a few seconds so the ALTER fails fast rather than stalling the table.

How do I change a column type without downtime?

Most type changes rewrite the whole table under an exclusive lock, so test the duration on a copy first. Widening within a type family, such as varchar(50) to text, does not rewrite. For a genuinely large table, the safe pattern is adding a new column, backfilling in batches, switching the application, then dropping the old one.

Does DROP COLUMN free up disk space?

Not immediately. PostgreSQL marks the column dropped in the catalog and leaves the data in place until each row is rewritten by a normal update. Space is reclaimed gradually, or all at once with VACUUM FULL, which takes an exclusive lock and needs free space equal to the table size.

How do I add a NOT NULL column to a large table?

Add the column with a constant default, which is instant. If you need NOT NULL on an existing column, add a CHECK constraint as NOT VALID, VALIDATE it under a share lock, then SET NOT NULL — PostgreSQL 12 and later uses the validated constraint as proof and skips the full table scan.