DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Alembic 建索引报 cannot run inside a transaction block

Alembic 迁移中并发建索引被 PostgreSQL 事务块限制拒绝时,用 autocommit_block 划分执行区段,并核对提前提交、失败遗留无效索引及降级边界。

给大表添加索引,在 Alembic 迁移里执行 CREATE INDEX CONCURRENTLY,迁移却立即停止,异常末尾是:

CREATE INDEX CONCURRENTLY cannot run inside a transaction block

这个错误说明建索引语句仍处于数据库事务块中。PostgreSQL 允许普通建索引放在事务里,却要求并发建索引在事务块之外执行。只把 SQL 加上 CONCURRENTLY,不会改变 Alembic 已经建立的事务边界。

下面适用于 Alembic 连接 PostgreSQL 的在线迁移,以及普通非分区表上的非唯一索引。项目需要具备 MigrationContext.autocommit_block() 接口;自定义连接、外部事务包装或异步迁移环境,还要确认实际连接的事务由谁管理。不要把这一处理直接套到其他数据库或分区父表上。

从报错定位到迁移事务

找到失败 revision 中真正发送的建索引语句,再看 env.py 的在线迁移配置。常见结构会在 context.begin_transaction() 中调用 context.run_migrations();在 PostgreSQL 的事务型 DDL 环境下,迁移中的 SQL 因而落在事务内。

这时扩大超时时间不能解决限制,把建索引包进另一层事务也不能使它离开事务。若日志实际是等待锁、连接超时或重复索引名,应按对应错误处理,不能仅因使用了并发索引就认定是这里的问题。

执行前先保留数据库备份、当前迁移版本和待发布脚本。在本机隔离数据库验证迁移与回退路径后,再安排正式变更;并发建索引仍会增加 CPU、I/O 工作量,也可能等待已有事务结束,不代表立即完成或完全没有运行影响。

把索引操作放进自动提交区段

建议把这个索引放在单独的 revision 中。保留项目生成的 revision、down_revision 等标识,只把其中的升级和降级函数写成下面的结构。示例假定 public.orders 和 created_at 已存在,ix_orders_created_at 尚不存在;这些名称需要按实际表结构替换。

from alembic import op


def upgrade():
    with op.get_context().autocommit_block():
        op.execute(
            "CREATE INDEX CONCURRENTLY ix_orders_created_at "
            "ON public.orders (created_at)"
        )


def downgrade():
    with op.get_context().autocommit_block():
        op.execute(
            "DROP INDEX CONCURRENTLY public.ix_orders_created_at"
        )

autocommit_block() 会切换到 SQLAlchemy 的自动提交隔离模式,让区段内的语句在 DBAPI 事务之外执行。降级也使用它,因为 DROP INDEX CONCURRENTLY 同样不能放在事务块中。

进入该区段会先无条件提交之前的事务。假如同一迁移前面已经改了字段或写了数据,后面建索引失败,那些已提交的改动不会随整次迁移一起回滚。因此,不要把必须共同成功或共同失败的数据修正与并发建索引塞进同一个 revision。

这里没有添加 IF NOT EXISTS。它只根据同名关系是否存在决定跳过,不会保证那个对象就是所需索引,也不会验证索引是否有效;失败后留下的同名无效索引需要先辨认。

按 revision 划分事务,但不要省掉自动提交区段

包含自动提交区段的迁移环境,可以在现有 env.py 在线配置中启用 transaction_per_migration=True。下面展示配置所在位置,项目已有的其他参数要保留:

with connectable.connect() as connection:
    context.configure(
        connection=connection,
        target_metadata=target_metadata,
        transaction_per_migration=True,
    )

    with context.begin_transaction():
        context.run_migrations()

这个选项让 Alembic 按单个迁移脚本划分事务,避免把一串 revision 当作同一个事务来理解。它本身不会让 CREATE INDEX CONCURRENTLY 脱离事务,建索引处仍需要前面的自动提交区段。

如果项目把已经开启事务的外部连接交给 Alembic,先梳理调用方与 env.py 的连接生命周期。不要在业务共用连接上随意执行 COMMIT,也不要为这一个索引把整个应用连接池改成自动提交。

完成后检查索引状态和迁移版本

迁移命令正常结束后,除了核对项目的迁移版本已到达目标 revision,还应检查实际索引。连接到同一数据库,按示例名称查询:

SELECT indexrelid::regclass AS index_name,
       indisvalid,
       indisready,
       pg_get_indexdef(indexrelid) AS index_definition
FROM pg_index
WHERE indexrelid = to_regclass('public.ix_orders_created_at');

没有返回行,说明这个名称没有匹配到索引,需要核对数据库、schema 和名称。返回行时,确认定义确实指向目标表与列,且 indisvalid、indisready 均为真。前者表示索引对查询有效,后者表示它已准备好接收插入更新;仅看到一个同名对象不足以判断成功。

这项检查也不能保证每条查询都会选择该索引。是否使用索引还取决于查询条件、统计信息和执行计划,不应把“迁移成功”直接写成“查询一定变快”。

中途失败后的重试与回退

并发构建在扫描阶段遇到错误时,可能留下无效索引。遇到失败先保留异常并确认建索引任务已经结束,再执行上述状态查询;不要在另一个仍运行的构建旁边立即清理对象。

如果确认留下的是本次创建失败的普通非唯一索引,且没有约束或其他用途依赖它,可以由索引所有者在事务块之外单独删除,再修正失败原因、重新运行迁移:

DROP INDEX CONCURRENTLY public.ix_orders_created_at;

执行这条清理语句的客户端不能替它包上 BEGIN。若通过 Alembic 编排清理,同样需要自动提交区段。不要添加 CASCADE;并发删除也不支持它。

若索引已经有效,而迁移版本仍未推进,可能是后续步骤失败,应该对照脚本与数据库状态处理,不能直接重跑创建语句或盲目把版本标记成完成。

正式回退时,上面的 downgrade() 会删除该索引,表中数据仍保留,但依赖它加速的查询可能变慢。这个示例不能用于删除支撑主键或唯一约束的索引,也不能用于分区表索引;这些对象需要另外设计变更顺序。自动提交区段之前已经提交的数据或结构改动,则必须按各自迁移的恢复方案处理。