server/channels/db/migrations/README.md
Enforce these rules to prevent table locks and performance degradation on large production databases (100M+ posts).
EXPLAIN ANALYZE that looks fine on 12M posts may behave very differently at 100M posts.Migration files follow the convention {sequence_number}_{description}.{up|down}.sql and live in db/migrations/{driver_name}/. After creating migration files, run make migrations-extract to update db/migrations/migrations.list. Merge upstream before submitting a PR to avoid sequence number collisions.
Never write a migration that locks an entire table. Common operations that acquire table-level locks:
ALTER COLUMN (type change) — rewrites the tableCREATE INDEX without CONCURRENTLYADD FOREIGN KEY without NOT VALIDLOCK TABLEAs noted above, ALTER COLUMN (type change) rewrites the entire table and blocks concurrent DML: strongly avoid. A real-world example took 8+ hours on a large customer database. When unavoidable, use a multi-release phased approach:
Never do a full-table UPDATE in a migration. Process data in batches in a job at runtime:
UPDATE foo SET col = value
WHERE id IN (
SELECT id FROM foo WHERE id > :offset ORDER BY id ASC LIMIT 100
);
Store the offset and resume across job runs.
Do not add a unique constraint directly. Create the index concurrently first, then attach it:
CREATE UNIQUE INDEX CONCURRENTLY constraint_name ON foo (bar);
ALTER TABLE foo ADD UNIQUE USING INDEX constraint_name;
Note that this cannot be done inside a transaction block, so it must be in a separate migration file. Use the -- morph:nontransactional comment to disable transactions for that migration.
Avoid foreign key constraints. Adding a foreign key takes a SHARE ROW EXCLUSIVE lock, limiting concurrent activity to SELECT queries only. If truly needed, consider adding the constraint with NOT VALID to avoid the full-table scan under lock.