云数据库 PostgreSQL SQL 防火墙

SQL 防火墙是云数据库 PostgreSQL 的自研内核能力,基于自研插件 pg_savior 实现,支持 pg_savior 的全部已有能力,用于防止误操作导致的数据丢失和高风险变更。加载后对实例内执行的语句可进行以下防护。

相关产品
云数据库 PostgreSQL 产品页,含版本规格、价格与购买入口。

·拦截无 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.1 登录 云数据库 PostgreSQL 控制台。

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 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

注意:说明:

八、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

注意:说明:

九、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

注意:说明:

十、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;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

注意:说明:

十一、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;SET id—- 1 2(2 rows)root=> SET pg_savior.max_result_rows = 0;SET

注意:说明:

十二、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

注意:说明:

十四、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。变更完成后建议重新开启防护。

赞 (0)
上一篇 4天前
下一篇 4天前