帮你快速理解、总结文档立即下载

安全更新插件 pg_savior

最近更新时间:2026-07-21 09:53:01

我的收藏
云数据库 PostgreSQL 提供安全更新插件 pg_savior,本文为您介绍关于安全更新插件 pg_savior 的说明及使用方法。

概述

pg_savior 是一个用于云数据库 PostgreSQL / PostgreSQL 的安全更新保护插件。该插件通过内核 Hook 在 SQL 执行前拦截高风险 DML 和 DDL 操作,防止因误执行无条件 DELETE、无条件 UPDATE、大表 TRUNCATE、大表 DROP TABLE、非并发建索引等操作造成数据丢失、长时间锁表或业务不可用。

支持版本

v17.10_r1.19、v18.4_r1.10及以上版本。

使用限制

CREATE EXTENSION pg_savior 仅用于在当前数据库注册扩展信息,不会单独激活保护能力。保护能力由 pg_savior 共享库中的 Hook 实现,必须通过 shared_preload_libraries、session_preload_libraries 或 LOAD 'pg_savior' 加载共享库后生效。
如需对所有会话生效,建议将 pg_savior 配置到 shared_preload_libraries 并重启数据库。
pg_savior.enabled 和 pg_savior.bypass 均为会话级参数,变更后只影响当前会话。
pg_savior.max_rows_affected 使用优化器估算行数进行判断,不是实际影响行数。估算结果依赖统计信息,表数据变化较大时建议先执行 ANALYZE。
pg_savior.large_table_threshold_rows 使用 pg_class.reltuples 判断表大小,判断结果同样依赖统计信息。
分区表或继承表场景下,插件会检查父表及其所有子表,取最大 reltuples 作为判断依据。
TRUNCATE ... CASCADE 场景下,插件会检查被级联影响的外键关联表,避免通过小表级联清空大表。
该插件用于防误操作,不替代权限管理、SQL 审核、备份恢复、灰度发布等安全治理能力。

功能概览

pg_savior 当前支持如下保护能力:
保护类型
默认行为
说明
DELETE 无 WHERE
阻断
防止整表删除。
UPDATE 无 WHERE
阻断
防止整表更新。
modifying CTE 中的 DELETE / UPDATE 无 WHERE
阻断
支持识别 WITH ... DELETE/UPDATE ... 场景。
DELETE / UPDATE 影响行数超过阈值
可配置阻断
通过 pg_savior.max_rows_affected 开启。
CREATE INDEX 未指定 CONCURRENTLY
阻断
临时表除外。
大表 ALTER TABLE ADD COLUMN ... DEFAULT
阻断
保守判断,避免潜在全表改写。
大表 ALTER TABLE ALTER COLUMN TYPE
阻断
避免全表改写。
大表 TRUNCATE
阻断
支持多表、分区/继承表和 CASCADE 场景。
大表 DROP TABLE
阻断
仅拦截表,DROP VIEW 等不受影响。
DROP DATABASE
阻断
DROP DATABASE IF EXISTS 在数据库存在时仍阻断,不存在时允许无操作通过。
临时绕过
支持
通过 pg_savior.bypass = on 临时放行。
全局开关
支持
通过 pg_savior.enabled = off 关闭当前会话保护。

创建插件

加载共享库

检查 shared_preload_libraries 是否已包含 pg_savior:
SHOW shared_preload_libraries;

创建扩展

共享库加载完成后,在需要使用保护能力的数据库中创建扩展:
CREATE EXTENSION pg_savior;
检查扩展是否创建成功:
SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_savior';
示例输出:
extname | extversion
-----------+------------
pg_savior | 0.1.0
(1 row)
说明:
pg_savior--0.1.0.sql 中不创建额外 SQL 对象,保护逻辑全部由共享库 Hook 完成。

参数说明

pg_savior 提供如下配置参数:
参数
默认值
类型
生效范围
说明
pg_savior.enabled
on
boolean
当前会话
总开关。设置为 off 后,当前会话不执行任何 pg_savior 检查。
pg_savior.bypass
off
boolean
当前会话
临时绕过开关。设置为 on 后,当前会话所有检查均放行。
pg_savior.max_rows_affected
0
integer
当前会话
DML 行数估算阈值。为0时关闭行数阈值检查;大于0时,阻断优化器估算影响行数超过该阈值的 DELETE / UPDATE。
pg_savior.large_table_threshold_rows
1000000
integer
当前会话
大表阈值。表或其分区/继承子表的 reltuples 超过该值时,视为大表并启用高风险 DDL 保护。
查看当前参数:
SHOW pg_savior.enabled;
SHOW pg_savior.bypass;
SHOW pg_savior.max_rows_affected;
SHOW pg_savior.large_table_threshold_rows;
修改参数示例:
SET pg_savior.max_rows_affected = 10000;
SET pg_savior.large_table_threshold_rows = 500000;
在事务内临时设置:
BEGIN;
SET LOCAL pg_savior.bypass = on;
-- 执行确认无误的高风险操作
COMMIT;

功能说明

阻断 DELETE 无 WHERE

pg_savior 会在 parse analyze 阶段检查 DELETE 语句是否包含 WHERE 条件。无 WHERE 条件的 DELETE 会被直接阻断,事务不会执行到执行器阶段。
示例:
CREATE TABLE emp (id int);
INSERT INTO emp VALUES (1), (2), (3);

DELETE FROM emp;
返回结果:
ERROR: pg_savior: DELETE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.
带 WHERE 条件的 DELETE 可以正常执行:
DELETE FROM emp WHERE id = 1;
说明:
包含子查询的 WHERE 条件同样允许执行,例如:
DELETE FROM emp WHERE id IN (SELECT id FROM dept);

阻断 UPDATE 无 WHERE

pg_savior 会在 parse analyze 阶段检查 UPDATE 语句是否包含 WHERE 条件。无 WHERE 条件的 UPDATE 会被阻断。
示例:
CREATE TABLE emp (id int);
INSERT INTO emp VALUES (1), (2), (3);

UPDATE emp SET id = 99;
返回结果:
ERROR: pg_savior: UPDATE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.
带 WHERE 条件的 UPDATE 可以正常执行:
UPDATE emp SET id = 99 WHERE id = 1;

阻断 CTE 中的无条件 DELETE / UPDATE

pg_savior 支持检查 modifying CTE 中的 DELETE / UPDATE。如果 CTE 内部存在无 WHERE 的 DELETE 或 UPDATE,即使外层语句本身不是无条件写操作,也会被阻断。

CTE 中 DELETE 无 WHERE

WITH d AS (DELETE FROM emp RETURNING id)
SELECT count(*) FROM d;
返回结果:
ERROR: pg_savior: DELETE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.

CTE 中 UPDATE 无 WHERE

WITH u AS (UPDATE emp SET id = 99 RETURNING id)
SELECT count(*) FROM u;
返回结果:
ERROR: pg_savior: UPDATE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.

CTE 中带 WHERE

WITH d AS (DELETE FROM emp WHERE id = 1 RETURNING id)
SELECT count(*) FROM d;
该语句可以正常执行。

配置 DELETE / UPDATE 影响行数阈值

默认情况下,pg_savior.max_rows_affected = 0,表示不检查 DELETE / UPDATE 的估算影响行数。
开启阈值后,pg_savior 会在 ExecutorStart 阶段读取执行计划中 ModifyTable 子计划的 plan_rows。当优化器估算影响行数超过阈值时,语句会被阻断。

设置阈值

SET pg_savior.max_rows_affected = 10;

DELETE 超过阈值

CREATE TABLE emp (id int);
INSERT INTO emp SELECT generate_series(1, 1000);
ANALYZE emp;

DELETE FROM emp WHERE id > 0;
返回结果:
ERROR: pg_savior: DELETE estimated to affect 1000 rows, exceeds pg_savior.max_rows_affected (10)
HINT: Refine the WHERE clause, raise pg_savior.max_rows_affected, or set pg_savior.bypass = on. Run ANALYZE if the estimate looks wrong.

UPDATE 超过阈值

UPDATE emp SET id = id + 10000 WHERE id > 0;
返回结果:
ERROR: pg_savior: UPDATE estimated to affect 1000 rows, exceeds pg_savior.max_rows_affected (10)
HINT: Refine the WHERE clause, raise pg_savior.max_rows_affected, or set pg_savior.bypass = on. Run ANALYZE if the estimate looks wrong.
说明:
该能力依赖优化器估算行数,不保证与实际影响行数完全一致。
对近期大量写入或删除的表,建议执行 ANALYZE 后再依赖该保护能力。
将 pg_savior.max_rows_affected 设置为0可关闭该检查。

阻断非并发 CREATE INDEX

pg_savior 会阻断未指定 CONCURRENTLY 的 CREATE INDEX,避免在业务表上长时间持有阻塞写入的锁。
示例:
CREATE TABLE emp (id int);
CREATE INDEX emp_id_idx ON emp (id);
返回结果:
ERROR: pg_savior: CREATE INDEX without CONCURRENTLY is blocked
HINT: Use CREATE INDEX CONCURRENTLY (it cannot run in a transaction block), or set pg_savior.bypass = on for this session.
推荐写法:
CREATE INDEX CONCURRENTLY emp_id_idx ON emp (id);
说明:
临时表上的普通 CREATE INDEX 只影响当前会话,pg_savior 会放行临时表上的非并发建索引。

阻断大表 ADD COLUMN DEFAULT

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断:
ALTER TABLE ... ADD COLUMN ... DEFAULT ...
示例:
SET pg_savior.large_table_threshold_rows = 10;

CREATE TABLE big_emp (id int);
INSERT INTO big_emp SELECT generate_series(1, 100);
ANALYZE big_emp;

ALTER TABLE big_emp ADD COLUMN status text DEFAULT 'active';
返回结果:
ERROR: pg_savior: ALTER TABLE ADD COLUMN with DEFAULT on a large table (100 rows) is blocked
HINT: This operation may rewrite the whole table (it is blocked conservatively). Plan a batched migration; raise pg_savior.large_table_threshold_rows; or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.
推荐迁移方式:
ALTER TABLE big_emp ADD COLUMN status text;

-- 分批回填
UPDATE big_emp SET status = 'active' WHERE id BETWEEN 1 AND 10000;
UPDATE big_emp SET status = 'active' WHERE id BETWEEN 10001 AND 20000;

-- 如需默认值,回填完成后再设置
ALTER TABLE big_emp ALTER COLUMN status SET DEFAULT 'active';
说明:
当前实现是保守策略:只要大表 ADD COLUMN 带 DEFAULT 就会阻断,不区分默认值是否会真实触发表重写。

阻断大表 ALTER COLUMN TYPE

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断:
ALTER TABLE ... ALTER COLUMN ... TYPE ...
示例:
SET pg_savior.large_table_threshold_rows = 10;

CREATE TABLE big_emp (id int, name text);
INSERT INTO big_emp SELECT generate_series(1, 100), 'x';
ANALYZE big_emp;

ALTER TABLE big_emp ALTER COLUMN id TYPE bigint;
返回结果:
ERROR: pg_savior: ALTER TABLE ALTER COLUMN TYPE on a large table (100 rows) is blocked
HINT: This operation may rewrite the whole table (it is blocked conservatively). Plan a batched migration; raise pg_savior.large_table_threshold_rows; or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.
说明:
同一个 ALTER TABLE 中如果包含多个子命令,只要其中任意子命令属于危险操作,就会触发拦截。例如:
ALTER TABLE big_emp ADD COLUMN extra int, ALTER COLUMN name TYPE varchar(50);

阻断大表 TRUNCATE

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断 TRUNCATE。
示例:
SET pg_savior.large_table_threshold_rows = 10;

CREATE TABLE big_emp (id int);
INSERT INTO big_emp SELECT generate_series(1, 100);
ANALYZE big_emp;

TRUNCATE big_emp;
返回结果:
ERROR: pg_savior: TRUNCATE on a large table "big_emp" (100 rows) is blocked
HINT: Verify the target, raise pg_savior.large_table_threshold_rows, or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.
说明:
多表 TRUNCATE 中只要任意目标表超过阈值,整个语句会被阻断。
对分区表或继承表,未指定 ONLY 时会检查所有实际受影响的子表。
对 TRUNCATE ... CASCADE,会检查外键级联涉及的所有表。

阻断大表 DROP TABLE

对于超过 pg_savior.large_table_threshold_rows 的表,pg_savior 会阻断 DROP TABLE。
示例:
SET pg_savior.large_table_threshold_rows = 10;

CREATE TABLE big_emp (id int);
INSERT INTO big_emp SELECT generate_series(1, 100);
ANALYZE big_emp;

DROP TABLE big_emp;
返回结果:
ERROR: pg_savior: DROP TABLE on a large table "big_emp" (100 rows) is blocked
HINT: Verify the target, raise pg_savior.large_table_threshold_rows, or set pg_savior.bypass = on. Run ANALYZE if the row estimate looks wrong.
说明:
多表 DROP TABLE 中只要任意目标表超过阈值,整个语句会被阻断。
DROP TABLE IF EXISTS 作用于不存在的表时,插件会放行,由 PostgreSQL 标准流程处理。
DROP VIEW 等非表对象不属于该保护范围。

阻断 DROP DATABASE

pg_savior 默认阻断 DROP DATABASE,防止误删数据库。
示例:
DROP DATABASE production_db;
返回结果:
ERROR: pg_savior: DROP DATABASE "production_db" is blocked
HINT: Set pg_savior.bypass = on for this session if you really mean it.
说明:
DROP DATABASE IF EXISTS dbname 当数据库存在时同样会被阻断。
DROP DATABASE IF EXISTS dbname 当数据库不存在时是无害 no-op,pg_savior 会放行。

临时绕过保护

当确认需要执行高风险操作时,可以在当前会话开启绕过:
SET pg_savior.bypass = on;
执行完成后建议立即关闭:
RESET pg_savior.bypass;
推荐在事务中使用 SET LOCAL,避免绕过状态泄露到后续操作:
BEGIN;
SET LOCAL pg_savior.bypass = on;
TRUNCATE big_emp;
COMMIT;
说明:
pg_savior.bypass 为 USERSET 参数,普通用户也可以设置。生产环境如需强管控,应配合权限治理、审计和变更流程使用。

临时关闭保护

如果需要在当前会话关闭所有 pg_savior 检查,可执行:
SET pg_savior.enabled = off;
恢复保护:
SET pg_savior.enabled = on;
说明:
pg_savior.enabled = off 会关闭当前会话中所有 DML 和 DDL 保护能力。生产环境不建议长期关闭。

典型使用流程

开启实例级保护

1. 在业务数据库中创建扩展:
CREATE EXTENSION pg_savior;
2. 按业务规模设置阈值:
ALTER DATABASE postgres SET pg_savior.max_rows_affected = 100000;
ALTER DATABASE postgres SET pg_savior.large_table_threshold_rows = 1000000;
3. 新建连接后确认参数:
SHOW pg_savior.enabled;
SHOW pg_savior.max_rows_affected;
SHOW pg_savior.large_table_threshold_rows;

执行一次确认的高风险变更

BEGIN;
SET LOCAL pg_savior.bypass = on;
ALTER TABLE big_emp ADD COLUMN status text DEFAULT 'active';
COMMIT;
建议在执行前确认:
SELECT relname, reltuples FROM pg_class WHERE relname = 'big_emp';
如统计信息不准确,先执行:
ANALYZE big_emp;

实现原理

pg_savior 通过三个 PostgreSQL Hook 实现保护能力。

post_parse_analyze_hook

在 SQL parse analyze 后执行,用于检查:
DELETE 是否缺少 WHERE。
UPDATE 是否缺少 WHERE。
modifying CTE 中的 DELETE / UPDATE 是否缺少 WHERE。
该阶段早于执行计划生成,能够在语句执行前快速失败。

ExecutorStart_hook

在执行器启动前执行,用于检查:
DELETE / UPDATE 的优化器估算影响行数是否超过 pg_savior.max_rows_affected。
如果超过阈值,会在触碰任何数据前阻断。

ProcessUtility_hook

用于检查 DDL / Utility 语句,包括:
CREATE INDEX 未指定 CONCURRENTLY。
大表 ALTER TABLE ADD COLUMN ... DEFAULT。
大表 ALTER TABLE ALTER COLUMN TYPE。
大表 TRUNCATE。
大表 DROP TABLE。
DROP DATABASE。
大表判断使用 pg_class.reltuples。对于分区表、继承表、级联截断等场景,会检查实际受影响的关系集合。

常见问题

为什么执行 CREATE EXTENSION 后没有生效?

CREATE EXTENSION pg_savior 只注册扩展,保护逻辑由共享库 Hook 实现。

为什么带 WHERE 的 DELETE 也被阻断?

如果设置了 pg_savior.max_rows_affected 且优化器估算影响行数超过阈值,带 WHERE 的 DELETE / UPDATE 也会被阻断。
可检查当前阈值:
SHOW pg_savior.max_rows_affected;

为什么小表被识别为大表?

插件依赖 pg_class.reltuples,该值来自统计信息。请执行:
ANALYZE table_name;
然后重试。

如何确认表是否超过大表阈值?

SELECT relname, reltuples
FROM pg_class
WHERE relname = 'table_name';
对于分区表或继承表,需要同时检查子表:
SELECT c.relname, c.reltuples
FROM pg_class c
JOIN pg_inherits i ON i.inhrelid = c.oid
WHERE i.inhparent = 'parent_table'::regclass;

CREATE INDEX 为什么被阻断?

pg_savior 默认要求持久表使用并发建索引:
CREATE INDEX CONCURRENTLY idx_name ON table_name (column_name);
临时表上的普通 CREATE INDEX 会被放行。

如何执行确认无误的 DROP DATABASE?

在当前会话开启绕过后执行:
SET pg_savior.bypass = on;
DROP DATABASE target_db;
RESET pg_savior.bypass;
建议仅在变更窗口内执行,并确保已完成备份和确认。