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 | userpg_savior.enabled | on | userpg_savior.large_table_threshold_rows | 1000000 | userpg_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 TABLEpostgres=> INSERT INTO orders VALUES (1, 'alice', 100.00), (2, 'bob', 250.50), (3, 'carol', 78.90);INSERT 0 3postgres=> ANALYZE orders;ANALYZE
不带 WHERE 条件的 UPDATE/DELETE 会被拦截:
postgres=> DELETE FROM orders;ERROR: pg_savior: DELETE without WHERE clause is blockedHINT: 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 blockedHINT: 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 blockedHINT: 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;SETERROR: 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;SETUPDATE 2root=> 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 blockedHINT: 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 1000100postgres=> ANALYZE big_orders;ANALYZE
对大表执行以下操作将被拦截:
postgres=> TRUNCATE big_orders;ERROR: pg_savior: TRUNCATE on a large table "big_orders" (1000100 rows) is blockedHINT: 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 blockedHINT: 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 blockedHINT: 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 blockedHINT: 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 TABLEpostgres=> 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;SETERROR: 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;SETQUERY 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;SETERROR: pg_savior: statement returned more than 2 rowsHINT: 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;SETid----12(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 blockedHINT: 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, skippingDROP DATABASE
9. 会话级关闭与放行
确认操作无误后,可临时关闭整个防火墙(仅当前会话)。例如执行全表 DELETE:
root=> SET pg_savior.enabled = off; DELETE FROM orders;SETDELETE 3root=> INSERT INTO orders VALUES (1, 'alice', 100.00), (2, 'bob', 250.50), (3, 'carol', 78.90);INSERT 0 3root=> SET pg_savior.enabled = on;SET
也可仅放行当前会话,不影响其他会话的防护状态:
root=> SET pg_savior.bypass = on; DELETE FROM orders;SETDELETE 3root=> 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 blockedHINT: 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 TABLEpostgres=> CREATE UNIQUE INDEX CONCURRENTLY t3_id_uidx ON t3(id);CREATE INDEXpostgres=> 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 TABLEpostgres=> 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。变更完成后建议重新开启防护。