在PostgreSQL数据库运维中,给一张正在高并发读写的大表添加索引是一个高风险操作。默认情况下,CREATE INDEX命令会获取表的SHARE锁,阻塞所有写入和大部分读操作,直到索引构建完成。对于数据量达到千万级别以上的表,这个过程可能持续几分钟甚至更久,直接造成业务中断。为了解决这个问题,PostgreSQL引入了并发建索引机制,允许在不长时间阻塞表的情况下完成索引创建。

并发建索引的底层原理与锁行为
理解CREATE INDEX CONCURRENTLY的关键在于认识它和普通建索引在MVCC(多版本并发控制)实现上的区别。普通CREATE INDEX会在开始时就对表加SHARE锁,并在单一事务中扫描全表、排序、写入索引文件,期间其他会话无法获得ROW EXCLUSIVE锁,也就是不能执行INSERT、UPDATE、DELETE。而并发建索引分为两个阶段扫描表,每个阶段都只获取短暂的SHARE UPDATE EXCLUSIVE锁,平时则退化为不阻塞读写的弱锁模式。
具体来说,并发建索引先启动一个事务扫描当前快照下的可见行,建立第一版索引;然后等待所有早于该事务的快照释放,再启动第二个事务扫描第一阶段之后发生变更的行,并通过系统日志补齐增量。正因为要两轮扫描和等待事务结束,它比普通建索引慢得多,且不能在事务块内执行。很多人在测试环境用得很顺,到了线上却因长事务未提交导致第二阶段一直等待,反而拖慢了整体发布节奏。
需要注意的是,并发建索引在命令刚开始和切换扫描阶段时,仍会请求SHARE UPDATE EXCLUSIVE锁,这会阻塞其他并发建索引以及VACUUM、ALTER TABLE等运维命令,但并不会阻塞普通的DML操作。因此严格来说它并非零锁,只是锁的粒度与持有时间被极大缩短。规划时应当避开已有VACUUM FULL或模式变更窗口,减少锁等待。
标准操作流程与失败处理方法
使用并发建索引的基本语法非常简单,只需在普通建索引语句后加上CONCURRENTLY关键字。例如为orders表的user_id字段建立索引,可以写成如下形式。该语句执行期间,线上交易依然可以正常写入,应用层基本无感知。
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
然而并发建索引有一个重要特性:如果中途因为连接断开、取消查询或唯一约束冲突而失败,它不会自动回滚清理已创建的索引对象,而是会在系统目录中留下一个标记为INVALID的索引。这时候如果直接重新执行同名CREATE INDEX CONCURRENTLY,会报关系已存在的错误。正确做法是先使用DROP INDEX CONCURRENTLY删除无效索引,再重新构建。
我们可以通过查询pg_index视图来识别无效索引。下面的SQL能列出当前库中状态为无效的索引名称,方便运维人员批量检查。发现后务必用CONCURRENTLY方式删除,避免普通DROP INDEX带来的短暂排他锁。
SELECT indexrelid::regclass AS index_name,
indrelid::regclass AS table_name
FROM pg_index
WHERE NOT indisvalid
AND NOT indisready;
在重试建索引时,建议通过psql的timing参数观察耗时,并在服务端日志中开启log_lock_waits,以便捕捉是否有长事务阻塞了第二阶段扫描。对于特别大的表,还可以配合降低并发建索引时的maintenance_work_mem设置,防止排序操作占用过多内存影响其他后台进程。
性能对比与业务场景选型
为了直观体现两种方式的差异,我们可以从锁阻塞、耗时、失败成本三个维度进行对比。普通建索引适合离线维护窗口或全新小表,而并发建索引则是线上大表优化的首选。下面的表格总结了核心区别。
| 对比项 | 普通CREATE INDEX | CREATE INDEX CONCURRENTLY |
|---|---|---|
| 表锁类型 | SHARE(阻塞DML) | SHARE UPDATE EXCLUSIVE(仅阻塞部分DDL) |
| 构建耗时 | 较短,单次扫描 | 较长,两次扫描加等待 |
| 失败清理 | 自动回滚 | 残留INVALID索引需手动删 |
| 事务块内 | 允许 | 禁止 |
从实际业务看,交易类系统的核心表几乎不能接受秒级以上的写阻塞,因此哪怕并发建索引要多花两三倍时间,也必须选用它。而像每天凌晨批量导入后才查询的报表表,则可以在导入完成后用普通建索引快速建立,提升后续统计效率。
还有一个常被忽略的场景是:如果你要建的是UNIQUE索引,并发模式同样支持,但第二阶段扫描时若发现唯一冲突会直接失败并留下无效索引。因此建唯一并发索引前,应当先用SQL校验是否存在重复值,避免无功而返。例如执行SELECT user_id, count(*) FROM orders GROUP BY user_id HAVING count(*) > 1,确认无重复后再动手。
综合来看,无影响地完成PostgreSQL索引添加的核心就是善用CONCURRENTLY语法、避开长事务、处理好失败残留。只要遵循这些原则,即便在日均百万写入的实例上,也能平滑地完成索引演进,不需要停机或切库。
postgresql并发建索引create_index_concurrently修改时间:2026-08-18 13:22:27