PostgreSQL分表需用BEFORE INSERT触发器拦截并转发数据,返回NULL跳过主表插入,子表须继承主表且带CHECK约束以支持查询裁剪,动态表名须用format('%I')防SQL注入。

INSERT 语句不会自动重定向到子表,必须靠触发器显式拦截并转发 —— 这是核心前提。PostgreSQL 原生不支持“分库”,只支持“分表”(即同一数据库内多物理表),所谓“分库分表逻辑”在触发器层面只能做到分表 + 跨 schema 插入,无法跨数据库(database)操作;跨库需应用层路由或中间件。
触发器必须用 BEFORE INSERT + RETURN NULL
这是最关键的控制点:只有 BEFORE INSERT 触发器才能在数据写入主表前截获 NEW,并手动插入到目标子表;AFTER 或 INSTEAD OF(仅对视图有效)都不适用。
必须返回 NULL,否则主表仍会执行默认插入 —— 这是绝大多数人踩坑的地方。
-
RETURN NEW→ 主表照常插入(触发器白写) -
RETURN NULL→ 主表跳过插入,只执行你写的INSERT INTO 子表 - 函数末尾漏写
RETURN NULL→ PostgreSQL 报错:control reached end of function without RETURN
子表名拼接和动态执行要防 SQL 注入
用 NEW.year 拼表名时,如果该字段来自用户输入(如 HTTP 参数直传),直接字符串拼接 || 极易被注入。例如 NEW.year = '2025); DROP TABLE info_2024; --' 就会触发灾难。
- 安全做法:用
format()+%I占位符强制转义标识符:format('INSERT INTO %I SELECT $1.*', my_tbname) - 禁止用
quote_ident()后再拼接字符串,容易漏括号或空格 - 若分区键是时间戳,务必先
to_char(NEW.ts, 'YYYYMM')标准化,避免'2025-04-01 10:20:30'这类非法表名
子表必须继承主表且带 CHECK 约束
否则查询主表时无法自动裁剪分区,SELECT * FROM info WHERE year = '2025' 仍会扫全表(含无约束的子表)。
- 建子表必须写
INHERITS (info),且加CHECK (year = '2025')或CHECK (year >= '2025' AND year - 主表本身应设为
UNLOGGED或留空(不存数据),否则它会成为性能瓶颈和数据一致性隐患 - 所有子表需单独建索引,主表上的索引无效 ——
CREATE INDEX ON info_2025(year)不能省
自动建表的异常处理要区分 undefined_table 和其他错误
触发器里 EXECUTE 动态 SQL 失败时,默认抛出 undefined_table,但其他错误(如权限不足、磁盘满)也得兜底,否则整条 INSERT 事务中断。
- 必须用
EXCEPTION WHEN undefined_table THEN ...单独捕获建表场景 - 建表后再次
EXECUTE插入时,建议再套一层BEGIN ... EXCEPTION WHEN OTHERS THEN ... END,避免建表成功但插入失败导致数据丢失 - 不要在触发器里调用
COMMIT或ROLLBACK—— 触发器运行在父事务中,强行提交会报ERROR: cannot commit while a cursor is open
触发器分表看着灵活,实际线上最易出问题的是分区裁剪失效和建表并发冲突:两个会话同时插入同一年份数据,可能都触发建表,第二个会因表已存在而报错。真要稳定,优先用 pg_partman 或原生声明式分区(PARTITION BY RANGE),触发器只适合低频、可控、过渡期场景。

















