Online PostgreSQL Column Type Migration
Scenario
In the lifecycle of a database, there’s a common type of requirement: modifying column types. For example:
- Using
INTas a primary key, only to discover that business is booming and the 2.1 billion limit of INT32 isn’t enough, wanting to upgrade toBIGINT - Using
BIGINTto store ID numbers, only to discover there’s anXin them requiring change toTEXTtype - Using
FLOATto store currency, discovering precision loss and wanting to change to Decimal - Using
TEXTto store JSON fields, wanting to use PostgreSQL’s JSON features and change to JSONB type
So how do we handle this kind of requirement?
Conventional Approach
Typically, ALTER TABLE can be used to modify column types.
Modifying column types usually rewrites the entire table. As a special case, if the modified type is binary compatible with the previous type, the table rewrite process can be skipped, but if there are indexes on the column, indexes still need to be rebuilt. Binary compatible conversions can be listed with the following query:
Excluding PostgreSQL internal types, binary compatible type conversions are as follows:
Common binary compatible type conversions are basically these two types:
varchar(n1) → varchar(n2) (n2 ≥ n1) (quite common, expanding length constraints won’t rewrite, shrinking will rewrite)
varchar ↔ text (synonymous conversion, basically useless)
This means all other type conversions involve table rewriting. Large table rewrites are slow, potentially taking minutes to tens of hours. Once rewriting occurs, the table will have AccessExclusiveLock, blocking all concurrent access.
If it’s a toy database, or the business hasn’t gone live yet, or the business doesn’t care about downtime duration, then the full table rewrite approach is certainly fine. But most of the time, business simply cannot accept such downtime. Therefore, we need an online upgrade method. Complete column type transformation without downtime.
Basic Approach
The basic principle of online column modification is as follows:
Create a new temporary column with the new type
Synchronize data from old column to new temporary column
- Stock synchronization: batch updates
- Incremental synchronization: update triggers
Handle column dependencies: indexes
Execute the switch
Handle column dependencies: constraints, default values, partitions, inheritance, triggers
Complete old/new column switching through column renaming
Online transformation addresses lock granularity splitting, equivalently replacing one long-term heavy lock operation with multiple instantaneous light lock operations.
The original ALTER TYPE rewrite process would acquire AccessExclusiveLock, blocking all concurrent access for minutes to days.
- Add new column: instant completion:
AccessExclusiveLock - Sync new column-incremental: create trigger, instant completion, low lock level
- Sync new column-stock: batch UPDATE, small amounts frequently, each can complete quickly, low lock level
- Old/new switching: lock table, instant completion
Let’s use pgbench’s default use case to illustrate the basic principle of online column modification. Suppose we want to modify the abalance field type from INT to BIGINT in pgbench_accounts while it’s being accessed, how should we handle this?
- First, create a new column named
abalance_tmpwith typeBIGINTforpgbench_accounts. - Write and create column synchronization trigger, which will sync from old column
abalanceto
Details are as follows:
Considerations
- MVCC safety of ALTER TABLE
- What if there are constraints on the column? (PrimaryKey, ForeignKey, Unique, NotNULL)
- What if there are indexes on the column?
- Primary-replica replication lag caused by ALTER TABLE
