症状
停机有精确形状:对一张表的写入冻结、读取照常、错误日志填满对单一 relation 的锁等待。原因是一条语句:普通 CREATE INDEX 取 SHARE 模式——建完之前不允许任何 INSERT/UPDATE/DELETE,大表上就是几分钟的写停机。修复方式的两种失败同样好认:“CREATE INDEX CONCURRENTLY cannot run inside a transaction block”(被迁移工具的事务包装挡住)与 “relation already exists”(上次失败的 INVALID 残留挡住同名重建)。
诊断思路
先在测试运行时查 pg_locks 确认锁模式与获取阶段的长查询等待;再查 pg_index 的 indisvalid=false 找失败 CONCURRENTLY 留下的 INVALID 残留;最后核对迁移工具的事务语义——多数工具默认把每步包进事务块,必须显式配置为该步自动提交。
根因
普通 CREATE INDEX 为保证新结构一致性,在整个构建期间锁表禁写。大表上这把维护操作变成应用停机。CONCURRENTLY 避免写锁,但不能在事务块内运行,且被中断时会留下无效索引。
修复与验证
任何接收活跃写入的表都用 CREATE INDEX CONCURRENTLY 且在事务块外执行:Flyway 配置、Rails 用 disable_ddl_transaction!、Alembic 用 autocommit_block 包裹。失败残留先清理:查 indisvalid=false、DROP INDEX CONCURRENTLY 删掉无效索引再重试。验证时在预发写入负载下构建:写入全程无停顿、索引结束态 indisvalid 为 true、清理后同名重试成功。注意 CONCURRENTLY 更慢更耗资源,同表两个并发构建可能互相死锁——按表串行化索引构建。
代码示例
CREATE INDEX orders_created_idx ON orders (created_at);
ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
ERROR: relation "orders_created_idx" already exists <- INVALID leftover
CREATE INDEX CONCURRENTLY orders_created_idx ON orders (created_at);
with op.get_context().autocommit_block():
op.create_index(
"orders_created_idx", "orders", ["created_at"],
postgresql_concurrently=True,
)
SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;
DROP INDEX CONCURRENTLY orders_created_idx; -- the INVALID one