Migration Patterns
ALTER TABLE Operations
- Add column:
ALTER TABLE t ADD COLUMN col Type [DEFAULT expr] [AFTER existing_col] - Drop column:
ALTER TABLE t DROP COLUMN col - Modify type:
ALTER TABLE t MODIFY COLUMN col NewType(must be compatible) - Rename:
ALTER TABLE t RENAME COLUMN old TO new - These are metadata-only operations — instant for most changes
Engine Changes
- Cannot ALTER engine directly
- Pattern: create new table → insert from old → rename
CREATE TABLE t_new ENGINE = ReplacingMergeTree() ORDER BY id AS SELECT * FROM t_old;
RENAME TABLE t_old TO t_backup, t_new TO t_old;
- For large tables: use
INSERT INTO ... SELECTwith batching
EXCHANGE TABLES (v22.5+)
- Atomic swap without RENAME chain:
EXCHANGE TABLES t_old AND t_new - Simpler and safer than
RENAME TABLE t_old TO t_backup, t_new TO t_old - Both tables must exist and be in the same database
Zero-Downtime Migrations
- Create new table with desired schema
- Create materialized view to capture new inserts:
CREATE MATERIALIZED VIEW mv TO t_new AS SELECT ... FROM t_old - Backfill historical data:
INSERT INTO t_new SELECT ... FROM t_old - Verify data consistency
- Switch application to new table
- Drop old table and materialized view
Data Backfill Patterns
- Batch by partition:
INSERT INTO new SELECT * FROM old WHERE toYYYYMM(date) = 202301 - Use
max_insert_block_sizeandmax_threadsfor throughput control - Monitor with
system.processesandsystem.merges - Verify row counts match after backfill
Lightweight Mutations
ALTER TABLE t UPDATE col = expr WHERE condition— async by default (mutations_sync = 0)- Track progress:
SELECT * FROM system.mutations WHERE table = 't' ALTER TABLE t DELETE WHERE condition— rewrites affected parts- Throttle impact: set
max_rows_per_mutationto limit rows per mutation batch - Always schedule heavy mutations off-peak; monitor
system.mutationsfor completion
Cross-Server Migration
- Use
remote()table function to copy between servers:
INSERT INTO local_db.t SELECT * FROM remote('source_host:9000', 'db', 't', 'user', 'pass')
- For large tables, batch by partition or use
clickhouse-localoffline approach
clickhouse-local Offline Migrations
- Run migrations without a running server:
clickhouse-local --file migration.sql - Useful for schema changes on cold data or CI/CD validation
- Can operate directly on data files:
clickhouse-local -S 'col1 Type1, col2 Type2' --input-format Native < data.bin
Schema Migration Versioning
- Track applied migrations with a dedicated table:
CREATE TABLE _schema_migrations (name String, applied_at DateTime DEFAULT now()) ENGINE = TinyLog;
- Insert a row after each successful migration; check before applying
- Integrate with deployment scripts for idempotent migration runs
Partition Operations
ALTER TABLE t ATTACH PARTITION id FROM other_table— zero-copy if same structureALTER TABLE t REPLACE PARTITION id FROM other_table— atomic swapALTER TABLE t MOVE PARTITION id TO TABLE other_table— move data
Common Pitfalls
- Nullable to non-Nullable requires default value for existing NULLs
- Changing ORDER BY requires table recreation
- Mutations (UPDATE/DELETE) rewrite all parts — schedule off-peak
- Test migrations on staging with production data volumes
EXCHANGE TABLESfails if either table is replicated with different replica paths