CREATE INDEX CONCURRENTLY:线上建索引不阻塞 DML 的代价与坑

CREATE INDEX CONCURRENTLY:线上建索引不阻塞 DML 的代价与坑

标签:#PostgreSQL #索引 #运维

CREATE INDEX CONCURRENTLY:线上建索引不阻塞 DML 的代价与坑

标签:#PostgreSQL #索引 #运维

一、前言

你有没有遇到过:上线前想给一张千万级大表加个索引,CREATE INDEX 一执行,业务方立刻告警------写入超时、消息积压、订单量暴跌?

这不是偶发事故,而是 PostgreSQL 的默认行为 :普通 CREATE INDEX 会锁住表的写操作,直到索引构建完成。大表建索引动辄几十分钟甚至几小时,期间所有 INSERT / UPDATE / DELETE 全部排队。解法就在关键字 CONCURRENTLY 里,但它不是免费的,还有几个必须知道的坑。

二、核心开发规范

  1. 生产环境大表加索引必须用 CONCURRENTLY :普通 CREATE INDEX 会阻塞 DML(写操作),CONCURRENTLY 全程不阻塞写入;
  2. 接受它的代价CONCURRENTLY 需要两次扫描全表、等待旧事务结束,耗时显著更长,构建期间额外的 CPU / I/O 负载可能拖慢其他操作;
  3. 失败会留下 invalid 索引 :构建失败后索引以 INVALID 状态残留,仍持续消耗更新开销,必须 DROP 后重试。

三、底层原理通俗讲解

  • 普通 CREATE INDEX(默认) :在表上持有排他级别的表锁,锁写不锁读 ------其他事务仍可 SELECT,但 INSERT / UPDATE / DELETE 全部阻塞到构建完成。整个过程单次扫描全表,速度快,但大表会锁住写入很久;
  • CREATE INDEX CONCURRENTLY :全程不持有阻塞写入的锁。实现上分 3 个事务:第一个事务先把索引以 invalid 状态登记进系统目录,后两个事务各做一次全表扫描 ;每次扫描前等待修改过该表的事务结束,第二次扫描后还要等待快照更早的事务结束,最后标记 valid 才正式可用;
  • 为什么更慢:两次扫描 + 多次等待 + 构建期间 DML 还在并发进行(新写入的行也要进索引),总工作量比普通构建大得多,耗时显著变长;
  • 不是完全免费:构建期间产生的额外 CPU 和 I/O 负载,可能让其他查询变慢------选业务低峰期执行更稳妥。

四、实战错误案例&优化方案

场景1:线上大表直接 CREATE INDEX(最高频事故)

表设计:orders 表,千万级行,user_id BIGINT

❌ 错误写法(不加 CONCURRENTLY,写操作全被锁住)

sql 复制代码
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- 构建期间:orders 表所有 INSERT / UPDATE / DELETE 全部阻塞
-- 大表可能阻塞几十分钟到几小时,业务写入超时、积压、告警

✅ 正确写法(加 CONCURRENTLY,DML 无感知)

sql 复制代码
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- 构建期间 DML 正常执行,索引构建完成后自动标记 valid 生效

关键结论 :生产环境大表加索引,CONCURRENTLY 不是可选项,是必选项SELECT 不阻塞但 DML 阻塞的普通构建,只适合小表或可接受停写的场景。

场景2:CONCURRENTLY 失败,留下 invalid 索引

❌ 危险现象:构建过程中出现死锁 / 唯一索引冲突,命令报错

sql 复制代码
CREATE UNIQUE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- ERROR: deadlock detected(或其他失败)
-- 但索引已以 INVALID 状态残留:
-- 查询系统目录可以看到 INVALID 状态:
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders';
--   Indexes:
--       "idx_orders_user_id" btree (user_id) INVALID

⚠️ 残留的 invalid 索引:查询会忽略它,但每次 DML 仍会更新它------白白消耗开销,必须清理。

✅ 正确处理:先 DROP 清理,再重试

sql 复制代码
DROP INDEX IF EXISTS idx_orders_user_id;                    -- 清理 invalid 索引
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);  -- 重试
-- 或者直接用:REINDEX INDEX CONCURRENTLY idx_orders_user_id(PG 12+)

关键结论CONCURRENTLY 失败不是"没建成",而是"留了个累赘"------上线脚本里务必带上失败后的清理步骤,否则 invalid 索引会持续吃掉更新性能。

场景3:事务块内使用 CONCURRENTLY(直接报错)

❌ 错误写法(放在 BEGIN ... COMMIT 里)

sql 复制代码
BEGIN;
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
COMMIT;

✅ 正确写法(单独执行,走自动提交;普通 CREATE INDEX 才允许在事务块内)

sql 复制代码
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);

关键结论CONCURRENTLY 内部依赖多个事务完成构建,不能放进事务块;把它当作独立的一条命令执行。

场景4:同表并发构建 & 分区表限制

❌ 误区:一张表同时发起两个 CONCURRENTLY

sql 复制代码
-- 会话 A
CREATE INDEX CONCURRENTLY idx_a ON orders (user_id);
-- 会话 B:同一张表的第二个并发构建会一直等待
CREATE INDEX CONCURRENTLY idx_b ON orders (status);

✅ 正确做法:同一张表同一时间只允许一个并发构建,逐个执行;普通(非并发)构建可以多个并行,但生产大表请勿使用。

⚠️ 分区表限制:对分区表本身不支持 CONCURRENTLY(PG 13 及以下),正确姿势是逐个分区并发构建,最后在父表上非并发建一次(仅元数据操作)。

关键结论:并发建索引是"单车道"------同一张表同时只能有一个;分区表要逐个分区处理。

五、绝对禁止的写法汇总

  • 生产大表不加 CONCURRENTLY 直接 CREATE INDEX(DML 全阻塞);
  • 事务块内 执行 CREATE INDEX CONCURRENTLY(直接报错);
  • 并发构建失败后不清理 invalid 索引(持续消耗更新开销);
  • 同一张表同时发起多个 CREATE INDEX CONCURRENTLY(互相等待);
  • 分区表 直接 CREATE INDEX CONCURRENTLY(不支持,应逐分区处理)。

六、最终评审口诀(记住不踩坑)

线上大表建索引,CONCURRENTLY 不能省;

失败留下 invalid,DROP 重来才算成。

七、总结

  • 要不要并发 :生产大表加索引一律 CREATE INDEX CONCURRENTLY,小表 / 可停写场景才用普通构建;
  • 代价要清楚:两次扫描、耗时显著更长、额外 CPU / I/O 负载------选低峰期执行,别在业务高峰硬来;
  • 坑要提前排:不能进事务块、同表只能一个并发、分区表逐分区处理、失败必清理 invalid 索引;
  • 监控手段 :构建期间可用 pg_stat_progress_create_index 视图查看进度,做到心里有数。

标签:PostgreSQL 数据库 运维


参考来源

一、前言

你有没有遇到过:上线前想给一张千万级大表加个索引,CREATE INDEX 一执行,业务方立刻告警------写入超时、消息积压、订单量暴跌?

这不是偶发事故,而是 PostgreSQL 的默认行为 :普通 CREATE INDEX 会锁住表的写操作,直到索引构建完成。大表建索引动辄几十分钟甚至几小时,期间所有 INSERT / UPDATE / DELETE 全部排队。解法就在关键字 CONCURRENTLY 里,但它不是免费的,还有几个必须知道的坑。

二、核心开发规范

  1. 生产环境大表加索引必须用 CONCURRENTLY :普通 CREATE INDEX 会阻塞 DML(写操作),CONCURRENTLY 全程不阻塞写入;
  2. 接受它的代价CONCURRENTLY 需要两次扫描全表、等待旧事务结束,耗时显著更长,构建期间额外的 CPU / I/O 负载可能拖慢其他操作;
  3. 失败会留下 invalid 索引 :构建失败后索引以 INVALID 状态残留,仍持续消耗更新开销,必须 DROP 后重试。

三、底层原理通俗讲解

  • 普通 CREATE INDEX(默认) :在表上持有排他级别的表锁,锁写不锁读 ------其他事务仍可 SELECT,但 INSERT / UPDATE / DELETE 全部阻塞到构建完成。整个过程单次扫描全表,速度快,但大表会锁住写入很久;
  • CREATE INDEX CONCURRENTLY :全程不持有阻塞写入的锁。实现上分 3 个事务:第一个事务先把索引以 invalid 状态登记进系统目录,后两个事务各做一次全表扫描 ;每次扫描前等待修改过该表的事务结束,第二次扫描后还要等待快照更早的事务结束,最后标记 valid 才正式可用;
  • 为什么更慢:两次扫描 + 多次等待 + 构建期间 DML 还在并发进行(新写入的行也要进索引),总工作量比普通构建大得多,耗时显著变长;
  • 不是完全免费:构建期间产生的额外 CPU 和 I/O 负载,可能让其他查询变慢------选业务低峰期执行更稳妥。

四、实战错误案例&优化方案

场景1:线上大表直接 CREATE INDEX(最高频事故)

表设计:orders 表,千万级行,user_id BIGINT

❌ 错误写法(不加 CONCURRENTLY,写操作全被锁住)

sql 复制代码
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- 构建期间:orders 表所有 INSERT / UPDATE / DELETE 全部阻塞
-- 大表可能阻塞几十分钟到几小时,业务写入超时、积压、告警

✅ 正确写法(加 CONCURRENTLY,DML 无感知)

sql 复制代码
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- 构建期间 DML 正常执行,索引构建完成后自动标记 valid 生效

关键结论 :生产环境大表加索引,CONCURRENTLY 不是可选项,是必选项SELECT 不阻塞但 DML 阻塞的普通构建,只适合小表或可接受停写的场景。

场景2:CONCURRENTLY 失败,留下 invalid 索引

❌ 危险现象:构建过程中出现死锁 / 唯一索引冲突,命令报错

sql 复制代码
CREATE UNIQUE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- ERROR: deadlock detected(或其他失败)
-- 但索引已以 INVALID 状态残留:
-- 查询系统目录可以看到 INVALID 状态:
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders';
--   Indexes:
--       "idx_orders_user_id" btree (user_id) INVALID

⚠️ 残留的 invalid 索引:查询会忽略它,但每次 DML 仍会更新它------白白消耗开销,必须清理。

✅ 正确处理:先 DROP 清理,再重试

sql 复制代码
DROP INDEX IF EXISTS idx_orders_user_id;                    -- 清理 invalid 索引
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);  -- 重试
-- 或者直接用:REINDEX INDEX CONCURRENTLY idx_orders_user_id(PG 12+)

关键结论CONCURRENTLY 失败不是"没建成",而是"留了个累赘"------上线脚本里务必带上失败后的清理步骤,否则 invalid 索引会持续吃掉更新性能。

场景3:事务块内使用 CONCURRENTLY(直接报错)

❌ 错误写法(放在 BEGIN ... COMMIT 里)

sql 复制代码
BEGIN;
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);
-- ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
COMMIT;

✅ 正确写法(单独执行,走自动提交;普通 CREATE INDEX 才允许在事务块内)

sql 复制代码
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders (user_id);

关键结论CONCURRENTLY 内部依赖多个事务完成构建,不能放进事务块;把它当作独立的一条命令执行。

场景4:同表并发构建 & 分区表限制

❌ 误区:一张表同时发起两个 CONCURRENTLY

sql 复制代码
-- 会话 A
CREATE INDEX CONCURRENTLY idx_a ON orders (user_id);
-- 会话 B:同一张表的第二个并发构建会一直等待
CREATE INDEX CONCURRENTLY idx_b ON orders (status);

✅ 正确做法:同一张表同一时间只允许一个并发构建,逐个执行;普通(非并发)构建可以多个并行,但生产大表请勿使用。

⚠️ 分区表限制:对分区表本身不支持 CONCURRENTLY(PG 13 及以下),正确姿势是逐个分区并发构建,最后在父表上非并发建一次(仅元数据操作)。

关键结论:并发建索引是"单车道"------同一张表同时只能有一个;分区表要逐个分区处理。

五、绝对禁止的写法汇总

  • 生产大表不加 CONCURRENTLY 直接 CREATE INDEX(DML 全阻塞);
  • 事务块内 执行 CREATE INDEX CONCURRENTLY(直接报错);
  • 并发构建失败后不清理 invalid 索引(持续消耗更新开销);
  • 同一张表同时发起多个 CREATE INDEX CONCURRENTLY(互相等待);
  • 分区表 直接 CREATE INDEX CONCURRENTLY(不支持,应逐分区处理)。

六、最终评审口诀(记住不踩坑)

线上大表建索引,CONCURRENTLY 不能省;

失败留下 invalid,DROP 重来才算成。

七、总结

  • 要不要并发 :生产大表加索引一律 CREATE INDEX CONCURRENTLY,小表 / 可停写场景才用普通构建;
  • 代价要清楚:两次扫描、耗时显著更长、额外 CPU / I/O 负载------选低峰期执行,别在业务高峰硬来;
  • 坑要提前排:不能进事务块、同表只能一个并发、分区表逐分区处理、失败必清理 invalid 索引;
  • 监控手段 :构建期间可用 pg_stat_progress_create_index 视图查看进度,做到心里有数。

标签:PostgreSQL 数据库 运维


参考来源

相关推荐
小蒜学长1 小时前
大学生健康饮食的智慧管理系统(代码+数据库+LW)
java·后端·springboot·大学生·健康饮食
guo_wen_qiang1 小时前
mysql中有哪些日志
数据库·mysql
小蒜学长1 小时前
基于Java的论坛数据可视化分析系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·数据可视化·论坛系统
青山木2 小时前
RocketMQ 入门到原理(一):整体架构与消息的生命周期
java·分布式·后端·中间件·架构·rocketmq
黑马程序员毕设2 小时前
基于微信小程序的二手交易平台设计与实现报告
前端·人工智能·spring boot·后端·考研
达梦数据2 小时前
达梦数据复制软件DMDRS搭建部署示例:源数据库DM8到目标数据库DM8数据迁移
数据库
沪上企服通2 小时前
旧账系统的信创迁移工程:从 MySQL/Oracle 到国产库、国密与电子会计档案闭环
数据库·笔记·数据库架构