← 中文资源

简体中文版本

PostgreSQL — CREATE INDEX 锁表:该用 CONCURRENTLY

修复 PostgreSQL 建索引期间的写入停顿,理解普通 CREATE INDEX 的 ACCESS EXCLUSIVE 锁,并掌握 CONCURRENTLY 及其注意事项。

阅读英文原文

症状

  停机有精确形状:对一张表的写入冻结、读取照常、错误日志填满对单一 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