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

SQL 防火墙

最近更新时间:2026-10-27 13:07:31
我的收藏
SQL 防火墙是云数据库 PostgreSQL 的自研内核能力,基于自研插件 pg_savior 实现,支持 pg_savior 的全部已有能力,用于防止误操作导致的数据丢失和高风险变更。加载后对实例内执行的语句可进行以下防护:
拦截无 WHERE 条件的全表 UPDATE/DELETE:包括 CTE(WITH 子句)中的修改语句,防止误清空业务表。
限制批量变更影响行数:优化器预估影响行数超过阈值的 UPDATE/DELETE 将被拒绝。
强制在线创建索引:拦截非 CONCURRENTLY 的建索引语句,避免锁表。
防护大表 DDL:对超过行数阈值的表,拦截 TRUNCATE、DROP TABLE 及可能触发整表重写的 ALTER TABLE 操作。
拦截高代价 SQL:规划器预估执行代价超过阈值的语句在执行前被拒绝。
限制查询返回行数:语句输出行数超过上限即报错中止,防止大结果集影响实例。
拦截删除数据库:DROP DATABASE 语句将被拒绝。

支持版本

PostgreSQL 版本
支持版本
PostgreSQL 17
v17.10_r1.19及以上
PostgreSQL 18
v18.4_r1.10及以上
说明:
您可在控制台实例详情页或执行 SHOW tencentdb_version; 查看当前实例的内核版本。

注意事项

预加载插件:使用前请先在控制台参数设置页找到 shared_preload_libraries 参数,在其参数运行值中勾选 pg_savior,保存后重启实例。
加载后默认开启:插件加载后防护即默认生效(pg_savior.enabled 默认为 on),无需额外操作。
防护范围:防护对实例内所有用户的语句生效,包括管理员(tencentdb_superuser)与普通用户。
建表带约束会被拦截:CREATE TABLE 中带 PRIMARY KEY 或 UNIQUE 约束时,同样按“非并发建索引”防护拦截(临时表除外),处理方式见 常见问题。
行数判定基于统计信息:影响行数与大表判定使用优化器估算值,若与实际偏差较大,可先执行 ANALYZE 更新统计信息。

参数说明

以下参数均为 user 级(可在会话中直接 SET),也可在控制台参数设置中修改。
参数
默认值
说明
pg_savior.enabled
on
防护总开关,off 时关闭全部防护
pg_savior.bypass
off
会话级放行开关,on 时当前会话跳过全部防护
pg_savior.max_rows_affected
0
UPDATE/DELETE 预估影响行数上限,超过即拦截;0表示不限制
pg_savior.large_table_threshold_rows
1000000
大表判定阈值(行数),超过该行数的表受 DDL 防护
pg_savior.max_query_cost
0
语句预估执行代价(total_cost)上限,超过即拦截;0表示不限制
pg_savior.max_result_rows
0
语句返回行数上限,超过即报错中止;0表示不限制

使用方法

以下示例以管理员 root(tencentdb_superuser 成员)账号执行,在本节创建的示例表,您可直接复现。

1. 开启防火墙

1.2 在实例列表,单击实例 ID 或操作列的管理,进入实例详情页面。
1.3 选择参数设置。
1.4 搜索参数 shared_preload_libraries,在其参数运行值中勾选 pg_savior,保存并重启实例。
重启后,可确认插件已加载:
postgres=> SHOW shared_preload_libraries;
shared_preload_libraries
--------------------------------------------------------------------------------------------------------
pg_textsearch, pg_cron, pg_squeeze, timescaledb, pg_jieba, pg_bigm, tencentdb_pwdcheck, pg_savior
(1 row)
输出中包含 pg_savior 即已加载。查看防护参数状态:
postgres=> SELECT name, setting, context FROM pg_settings WHERE name LIKE 'pg_savior%' ORDER BY name;
name | setting | context
--------------------------------------+---------+---------
pg_savior.bypass | off | user
pg_savior.enabled | on | user
pg_savior.large_table_threshold_rows | 1000000 | user
pg_savior.max_rows_affected | 0 | user
(4 rows)
context 为 user 表示所有参数均可在会话中通过 SET 直接调整,无需重启实例。

2. 拦截无 WHERE 的 UPDATE/DELETE

创建示例表并插入数据:
postgres=> CREATE TABLE orders (
id int,
customer text,
amount numeric
);
CREATE TABLE
postgres=> INSERT INTO orders VALUES (1, 'alice', 100.00), (2, 'bob', 250.50), (3, 'carol', 78.90);
INSERT 0 3
postgres=> ANALYZE orders;
ANALYZE
不带 WHERE 条件的 UPDATE/DELETE 会被拦截:
postgres=> DELETE FROM orders;
ERROR: pg_savior: DELETE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.
postgres=> UPDATE orders SET amount = 0;
ERROR: pg_savior: UPDATE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.
CTE(WITH 子句)中不带 WHERE 的修改语句同样被拦截:
postgres=> WITH moved AS (DELETE FROM orders RETURNING *) SELECT count(*) FROM moved;
ERROR: pg_savior: DELETE without WHERE clause is blocked
HINT: Add a WHERE clause, or set pg_savior.bypass = on for this session.
带 WHERE 条件的语句正常执行:
postgres=> DELETE FROM orders WHERE id = 0;
DELETE 0

3. 限制批量变更的影响行数

设置 pg_savior.max_rows_affected 后,优化器预估影响行数超过该值的 UPDATE/DELETE 会被拦截。以下将上限设为2,示例表 orders 中有3行数据:
root=> SET pg_savior.max_rows_affected = 2; DELETE FROM orders WHERE id > 0;
SET
ERROR: pg_savior: DELETE estimated to affect 3 rows, exceeds pg_savior.max_rows_affected (2)
HINT: Refine the WHERE clause, raise pg_savior.max_rows_affected, or set pg_savior.bypass = on. Run ANALYZE if the estimate looks wrong.
预估影响行数未超限时正常执行:
root=> SET pg_savior.max_rows_affected = 2; UPDATE orders SET amount = 0 WHERE id <= 2;
SET
UPDATE 2
root=> SET pg_savior.max_rows_affected = 0;
SET
说明:
该参数默认值为0表示不限制。
该参数仅当前会话生效,需与被检查的语句在同一会话中执行;如需全实例默认限制,可在控制台参数设置中修改。

4. 强制在线创建索引

非 CONCURRENTLY 的建索引语句会长时间锁表,将被拦截:
postgres=> CREATE INDEX idx_orders_customer ON orders (customer);
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 在线创建即可:
postgres=> CREATE INDEX CONCURRENTLY idx_orders_customer ON orders (customer);
CREATE INDEX
说明:
CREATE INDEX CONCURRENTLY 不能在事务块中执行。
临时表上的非并发建索引不锁其他会话,不受该防护限制。

5. 防护大表 DDL

行数超过 pg_savior.large_table_threshold_rows(默认 1000000)的表视为“大表”,对其执行 TRUNCATE、DROP TABLE 及可能触发整表重写的 ALTER TABLE 会被拦截。以下创建一张100万行以上的示例表:
postgres=> CREATE TABLE big_orders AS SELECT g AS id, 'customer_' || g AS customer, g::numeric AS amount FROM generate_series(1, 1000100) g;
SELECT 1000100
postgres=> ANALYZE big_orders;
ANALYZE
对大表执行以下操作将被拦截:
postgres=> TRUNCATE big_orders;
ERROR: pg_savior: TRUNCATE on a large table "big_orders" (1000100 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.
postgres=> DROP TABLE big_orders;
ERROR: pg_savior: DROP TABLE on a large table "big_orders" (1000100 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.
postgres=> ALTER TABLE big_orders ADD COLUMN memo text DEFAULT 'N';
ERROR: pg_savior: ALTER TABLE ADD COLUMN with DEFAULT on a large table (1000100 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.
postgres=> ALTER TABLE big_orders ALTER COLUMN amount TYPE numeric(12,2);
ERROR: pg_savior: ALTER TABLE ALTER COLUMN TYPE on a large table (1000100 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 不受影响:
postgres=> ALTER TABLE big_orders ADD COLUMN memo text;
ALTER TABLE
未达到阈值的表不受 DDL 防护限制:
postgres=> CREATE TABLE junk (id int);
CREATE TABLE
postgres=> DROP TABLE junk;
DROP TABLE
说明:
可能被拦截的 ALTER TABLE 操作包括:带 DEFAULT 的 ADD COLUMN 与 ALTER COLUMN TYPE,这两类操作可能重写整表。
分区表、继承表的行数按其全部子表累计计算;TRUNCATE ... CASCADE 关联的外键引用表也会纳入检查。

6. 拦截高代价 SQL

设置 pg_savior.max_query_cost 后,规划器预估执行代价(total_cost)超过该值的语句在执行前被拦截。以下示例将上限设为100,示例表 big_orders(100万行以上)上的一次全表统计查询将被拦截:
root=> SET pg_savior.max_query_cost = 100; SELECT count(*) FROM big_orders;
SET
ERROR: pg_savior: statement estimated cost 13569 exceeds pg_savior.max_query_cost (100)
HINT: Refine the query, raise pg_savior.max_query_cost, or set pg_savior.bypass = on. Check the estimate with EXPLAIN.
EXPLAIN 只生成执行计划、不实际执行语句,不受该防护限制,可用来查看预估代价:
root=> SET pg_savior.max_query_cost = 100; EXPLAIN SELECT count(*) FROM big_orders;
SET
QUERY PLAN
----------------------------------------------------------------------------------------------
Finalize Aggregate (cost=13569.07..13569.08 rows=1 width=8)
-> Gather (cost=13568.85..13569.06 rows=2 width=8)
Workers Planned: 2
-> Partial Aggregate (cost=12568.85..12568.86 rows=1 width=8)
-> Parallel Seq Scan on big_orders (cost=0.00..11527.08 rows=416708 width=0)
(5 rows)

root=> SET pg_savior.max_query_cost = 0;
SET
说明:
默认值为0表示不限制;该参数仅当前会话生效。
代价为优化器预估值,可通过 EXPLAIN 查看。
EXPLAIN ANALYZE 会实际执行语句,同样受该防护检查。

7. 限制查询返回行数

设置 pg_savior.max_result_rows 后,语句输出行数超过该值即报错中止。以下示例将上限设为2,示例表 orders 中有3行数据:
root=> SET pg_savior.max_result_rows = 2; SELECT id FROM orders;
SET
ERROR: pg_savior: statement returned more than 2 rows
HINT: Add a LIMIT, raise pg_savior.max_result_rows, or set pg_savior.bypass = on.
为查询添加 LIMIT 后正常返回:
root=> SET pg_savior.max_result_rows = 2; SELECT id FROM orders LIMIT 2;
SET
id
----
1
2
(2 rows)

root=> SET pg_savior.max_result_rows = 0;
SET

说明:
默认值为0表示不限制;该参数仅当前会话生效。
超过上限时语句直接报错中止,不会返回被截断的结果集;请为查询添加 LIMIT 或调整上限后重试。

8. 拦截删除数据库

已存在的数据库不可删除,语句会被直接拦截(示例中使用 postgres 库,语句不会产生任何实际影响):
postgres=> DROP DATABASE postgres;
ERROR: pg_savior: DROP DATABASE "postgres" is blocked
HINT: Set pg_savior.bypass = on for this session if you really mean it.
对不存在的数据库执行 DROP DATABASE IF EXISTS 属于幂等清理脚本用法,正常跳过:
postgres=> DROP DATABASE IF EXISTS savior_demo_never;
NOTICE: database "savior_demo_never" does not exist, skipping
DROP DATABASE

9. 会话级关闭与放行

确认操作无误后,可临时关闭整个防火墙(仅当前会话)。例如执行全表 DELETE:
root=> SET pg_savior.enabled = off; DELETE FROM orders;
SET
DELETE 3
root=> INSERT INTO orders VALUES (1, 'alice', 100.00), (2, 'bob', 250.50), (3, 'carol', 78.90);
INSERT 0 3
root=> SET pg_savior.enabled = on;
SET
也可仅放行当前会话,不影响其他会话的防护状态:
root=> SET pg_savior.bypass = on; DELETE FROM orders;
SET
DELETE 3
root=> SET pg_savior.bypass = off;
SET
说明:
如需长期关闭防护,可在控制台参数设置中将 pg_savior.enabled 的参数运行值修改为 off。
bypass 与 enabled 的会话级修改不影响其他会话。

常见问题

Q1:CREATE TABLE 带 PRIMARY KEY 或 UNIQUE 约束时报错「CREATE INDEX without CONCURRENTLY is blocked」如何处理?

A:主键/唯一约束会随建表创建内部索引,同样按“非并发建索引”防护拦截(临时表除外):
postgres=> CREATE TABLE t3 (id int PRIMARY KEY, v text);
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.
可改为在线方式分步创建:
postgres=> CREATE TABLE t3 (id int, v text);
CREATE TABLE
postgres=> CREATE UNIQUE INDEX CONCURRENTLY t3_id_uidx ON t3(id);
CREATE INDEX
postgres=> ALTER TABLE t3 ADD CONSTRAINT t3_pkey PRIMARY KEY USING INDEX t3_id_uidx;
NOTICE: ALTER TABLE / ADD CONSTRAINT USING INDEX will rename index "t3_id_uidx" to "t3_pkey"
ALTER TABLE
postgres=> DROP TABLE t3;
DROP TABLE
如需一次性完成建表,也可在当前会话临时放行防护后执行。

Q2:为什么报错提示的行数与实际行数不一致?

A:防护使用优化器的预估值,基于表统计信息。数据大量变更后可先执行 ANALYZE 更新统计信息,再重试。

Q3:为什么小表的操作也被按“大表”拦截了?

A:请检查会话或控制台中 pg_savior.large_table_threshold_rows 的设置,确认阈值是否被调整为小于表行数的值。

Q4:如何临时执行一条被拦截的语句?

A:确认操作无误后,在当前会话执行 SET pg_savior.bypass = on; 放行,执行完成后建议改回 off。放行仅对当前会话有效。

Q5:维护窗口内需要批量执行大表变更,如何操作?

A:可将 pg_savior.enabled 在会话中临时设为 off,或在控制台参数设置中修改为 off。变更完成后建议重新开启防护。