GBase 8c 表结构变更前的对象依赖排查
GBase 8c 表结构变更前的对象依赖排查
我最近看 GBase 8c 资料时,一个感受越来越明显:很多表结构变更失败,并不是 DDL 语法本身有问题,而是变更前没有把对象依赖和影响面查清楚。
现场里很常见的一类情况是:测试库里 ALTER TABLE 很顺,到了生产库却突然报依赖错误,或者虽然语句执行成功了,但后面的视图、触发器、过程、报表任务开始连锁异常。再往后查,往往不是数据库“脾气古怪”,而是对象之间本来就有引用关系,只是平时没把这张依赖网摊开看。
我自己理解下来,GBase 8c 的表结构变更,真正难的不是把 DDL 写出来,而是回答下面这几个问题:
- 这个表现在被哪些对象引用了。
- 这些引用是“能独立删”的,还是“跟着宿主一起删”的。
- 这次变更是纯元数据动作,还是会引发表重写、空间占用和更长窗口期。
- catalog 里能查到的依赖,和业务代码里手写的动态 SQL,分别要怎么确认。
所以我现在做 8c 结构变更,基本都会先做一轮“对象依赖体检”,再决定到底是直接改、分阶段改,还是新旧对象并行切换。
先把风险分层,不要一上来就执行 DDL
我最近整理下来觉得,表结构变更至少可以分成下面几类,处理思路完全不一样。
| 变更动作 | 现场最容易踩的点 | 我更倾向的处理方式 |
|---|---|---|
| 新增列 | 看起来简单,但默认值、非空约束可能触发额外扫描或重写 | 先加可空列,再分批回填,再补约束 |
| 删除列 | 依赖视图、规则、过程;删得快但空间不会立刻回来 | 先查依赖,再确认是否真要马上删 |
| 修改列类型 | 可能重写整表,窗口期和磁盘占用都要重估 | 热表优先走“新列替换” |
| 重命名表/列 | 业务 SQL、存储过程、外部任务脚本容易漏改 | 先扫数据库对象,再扫应用脚本 |
| 删除表 | 影响范围最大,CASCADE 一旦用错很难止损 |
先把依赖对象列清楚,尽量不用盲删 |
我实际排查时一般先看两件事:
- 这次变更会不会触发表重写。
- 这张表上游下游挂了多少对象。
原因很简单。前者决定变更窗口,后者决定回滚难度。
我会先看的几个系统对象
GBase 8c 的好处是,很多对象关系并不是“黑盒”,系统表和系统视图里能看到相当多线索。我自己更关注下面这几组。
| 排查入口 | 主要用途 | 我通常怎么看 |
|---|---|---|
PG_TABLES |
看表是否有触发器、创建时间、最后 DDL 时间 | 先确认对象近期有没有被改过 |
PG_OBJECT |
看对象创建人与修改时间 | 判断对象最近是否被二次加工过 |
PG_DEPEND |
看依赖链的核心入口 | 判断是普通依赖、自动依赖还是内部依赖 |
PG_REWRITE |
视图/规则类依赖的重要线索 | 查视图对基表的引用不能只看名字 |
PG_VIEWS |
查视图定义 | 快速做定义级确认 |
PG_TRIGGER |
查表上的触发器、触发函数、是否启用 | 结构改动前必须过一遍 |
PG_PROC |
查函数/过程定义、参数和源码线索 | 动态 SQL 场景我会二次扫源码 |
其中我最近最常用的是 PG_DEPEND、PG_OBJECT 和 PG_VIEWS 这一组。
PG_DEPEND适合做“谁依赖谁”的主干排查。PG_OBJECT适合补“这个对象最近有没有被改过”。PG_VIEWS和PG_TRIGGER适合把定义内容补齐,避免只看到对象名,看不到真实影响。
先确认对象状态,再决定要不要进维护窗口
我通常不会一上来就跑改表语句,而是先把对象元数据和最近 DDL 情况看一遍。
SELECT schemaname,
tablename,
tableowner,
hasindexes,
hasrules,
hastriggers,
created,
last_ddl_time
FROM pg_tables
WHERE schemaname = 'acct'
AND tablename = 'txn_order';
如果还想把对象级信息补全,我会再看一次 PG_OBJECT。
SELECT object_oid,
object_type,
creator,
ctime,
mtime,
changecsn
FROM pg_object
WHERE object_oid = 'acct.txn_order'::regclass;
我自己更关注的是 last_ddl_time 和 mtime。因为不少现场问题不是“这张表一直没动”,而是最近有人做过授权、改过索引、调过表定义,只是变更单里没写出来。这个时候如果直接套旧方案,很容易误判窗口大小。
查视图依赖时,我不会只看 PG_VIEWS
很多人排查表结构影响面,只会跑一句:
SELECT schemaname, viewname
FROM pg_views
WHERE definition ILIKE '%txn_order%';
这句有用,但我自己不会只停在这里。
原因是视图在系统里对应的不只是一个名字,还牵涉到重写规则。GBase 8c 的 PG_REWRITE 专门存这类规则信息,真正做依赖定位时,我更习惯把 PG_REWRITE 和 PG_DEPEND 一起看。
SELECT n.nspname AS dep_schema,
c.relname AS dep_view,
r.rulename AS rule_name,
d.deptype
FROM pg_depend d
JOIN pg_rewrite r
ON r.oid = d.objid
JOIN pg_class c
ON c.oid = r.ev_class
JOIN pg_namespace n
ON n.oid = c.relnamespace
WHERE d.classid = 'pg_rewrite'::regclass
AND d.refclassid = 'pg_class'::regclass
AND d.refobjid = 'acct.txn_order'::regclass
ORDER BY 1, 2;
如果这里已经能查到多个依赖视图,我一般不会再考虑“直接删列”或者“直接改类型”这种一步到位的做法,而是优先改成分阶段方案。
触发器和过程是我更担心的隐性风险点
对业务表来说,触发器经常比视图更容易被漏掉。因为视图通常还能在对象清单里看见,触发器往往是表侧逻辑,平时不查就没存在感。
我一般会先看这张表上挂了哪些触发器、触发函数是谁、当前是不是启用状态。
SELECT n.nspname AS schema_name,
c.relname AS table_name,
t.tgname AS trigger_name,
p.proname AS trigger_func,
t.tgenabled,
t.tgisinternal
FROM pg_trigger t
JOIN pg_class c
ON c.oid = t.tgrelid
JOIN pg_namespace n
ON n.oid = c.relnamespace
JOIN pg_proc p
ON p.oid = t.tgfoid
WHERE n.nspname = 'acct'
AND c.relname = 'txn_order'
ORDER BY t.tgname;
如果表结构改动涉及到触发器里使用的列名,我会继续向下看函数或过程定义。
SELECT n.nspname,
p.proname,
p.prokind,
p.prosrc,
p.proargsrc
FROM pg_proc p
JOIN pg_namespace n
ON n.oid = p.pronamespace
WHERE p.prosrc ILIKE '%txn_order%'
OR COALESCE(p.proargsrc, '') ILIKE '%txn_order%'
ORDER BY 1, 2;
这里我会特别小心一个问题:catalog 能查到的是非常重要的一层,但我不会把它当成全部结果。真正落到现场时,过程里拼动态 SQL、调包内私有函数、或者脚本侧直接拼列名,这些都可能绕开“显式依赖”的主链,所以最后我通常还会补一次源码检索和应用侧检索。
我最常拿来判断风险等级的是 deptype
PG_DEPEND 里最值得看的字段之一就是 deptype。我最近整理下来,做结构变更时可以把它先粗分成下面几类。
| deptype | 含义 | 我对它的理解 |
|---|---|---|
n |
普通依赖 | 被引用对象通常要配合 CASCADE 才能删除,最常见 |
a |
自动依赖 | 宿主删掉时,这类对象通常会自动跟着处理 |
i |
内部依赖 | 往往是内部实现的一部分,不能按普通对象随便单删 |
e |
extension 依赖 | 和扩展对象绑定,处理方式要按扩展边界来 |
p |
pin 依赖 | 系统自身依赖,不能碰 |
我个人更倾向于把 n 看成“上线前必须人工确认”的一类,把 i 和 p 看成“不要在生产直接试”的一类。因为这两种一旦误判,问题通常不是一条 SQL 回滚那么简单。
改列类型时,我通常优先评估“重写整表”的代价
这也是我最近特别在意的一点。很多人觉得改类型只是 DDL,应该很快,但 GBase 8c 的手册里其实把风险写得很明确:
- 改字段类型可能重写整个表。
- 用非空默认值加列,也可能重写整个表。
- 大表场景下,这类操作不仅时间长,还可能临时需要更多磁盘空间。
所以我的经验是:
| 场景 | 直接 ALTER COLUMN TYPE |
新列替换方案 |
|---|---|---|
| 小表、低峰时段 | 可以考虑 | 不一定必须 |
| 热表、长事务多 | 风险偏高 | 更稳妥 |
| 有多个视图/过程依赖 | 不建议硬改 | 更容易做灰度切换 |
| 回滚要求严格 | 回滚路径不够直观 | 新旧列并行更好退回 |
我自己更偏向下面这种做法:
- 新增目标类型的新列。
- 分批回填。
- 先改视图、过程、报表口径。
- 观察一段时间。
- 最后再处理旧列。
示意 SQL 可以像这样写:
ALTER TABLE acct.txn_order
ADD COLUMN settle_ts_new timestamp;
UPDATE acct.txn_order
SET settle_ts_new = to_timestamp(settle_ts_old, 'YYYY-MM-DD HH24:MI:SS')
WHERE settle_ts_old IS NOT NULL
AND settle_ts_new IS NULL;
CREATE OR REPLACE VIEW rpt.v_order_day AS
SELECT order_id,
settle_ts_new AS settle_ts,
amount,
status_code
FROM acct.txn_order;
这种方式看起来多走了几步,但从落地角度看,最大的好处是把“高风险瞬时变更”拆成了“可观察、可回退的多步变更”。
我会把预检查结果直接落盘,不靠临场记忆
如果是正式变更,我一般会在 gsql 里把预检查结果直接导出来,避免窗口里来回复制。
gsql -d finance_core -h 192.0.2.31 -p 5432 -U app_dba <<'SQL'
\o ddl_precheck_txn_order_20260402.log
SELECT now();
SELECT schemaname, tablename, hastriggers, created, last_ddl_time
FROM pg_tables
WHERE schemaname = 'acct'
AND tablename = 'txn_order';
SELECT n.nspname, c.relname, r.rulename, d.deptype
FROM pg_depend d
JOIN pg_rewrite r ON r.oid = d.objid
JOIN pg_class c ON c.oid = r.ev_class
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE d.classid = 'pg_rewrite'::regclass
AND d.refclassid = 'pg_class'::regclass
AND d.refobjid = 'acct.txn_order'::regclass
ORDER BY 1,2;
SELECT n.nspname, c.relname, t.tgname, p.proname, t.tgenabled
FROM pg_trigger t
JOIN pg_class c ON c.oid = t.tgrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_proc p ON p.oid = t.tgfoid
WHERE n.nspname = 'acct'
AND c.relname = 'txn_order'
ORDER BY t.tgname;
\o
SQL
我最近养成这个习惯以后,变更复盘会轻松很多。至少出了问题时,能马上回到“变更前到底查到了什么”这个基线,而不是靠口头回忆。
现场里最容易忽略的几个坑
我自己踩过或者见过的坑,主要集中在下面几类。
| 常见坑 | 现场表现 | 我现在的处理习惯 |
|---|---|---|
| 只查视图名,不查定义 | 以为没影响,实际口径已被引用 | PG_VIEWS.definition 必查 |
| 只查表,不查触发器 | 变更后写入链路异常 | 表级触发器固定纳入检查项 |
看到 DROP COLUMN 很快就直接执行 |
列逻辑消失了,但空间没马上回来 | 先分清“逻辑删除列”和“物理空间回收” |
直接 CASCADE |
一次删掉整串对象 | 先把依赖清单落盘,再决定是否级联 |
| 只看数据库对象,不看外部脚本 | 数据库没报错,任务调度开始失败 | 应用 SQL、ETL、报表脚本一起搜 |
这里我再多说一句。DROP COLUMN 在手册里的描述其实很关键:它快,并不代表没有后续影响。很多时候只是让这个列对 SQL 不再可见,空间并不会立刻以我们直觉里的方式回到系统里。所以结构变更和空间治理,最好不要混成一个动作去理解。
我现在更愿意用“先摸清依赖,再设计路径”的方式做 8c 变更
我最近整理这一块资料时,最大的变化不是学会了多少条 DDL,而是把思路改了。
以前更容易把结构变更理解成“执行一条 ALTER TABLE”。现在我更倾向于把它理解成三段:
- 先识别对象关系。
- 再评估这次变更是元数据动作还是重写动作。
- 最后才是选哪种实施路径。
对 GBase 8c 来说,PG_DEPEND、PG_OBJECT、PG_REWRITE、PG_VIEWS、PG_TRIGGER 这些系统对象已经给了我们很好的排查抓手。只要前面这轮检查做扎实,很多原本会在生产窗口里暴露的问题,其实在变更前就能看出七八成。
我自己现在做表结构改动,最不愿意看到的不是“语法报错”,而是“语法没报错,但依赖对象悄悄坏了”。前者还算早发现,后者往往才是最费时间的。
参考资料
[1] GBase 8c 开发者手册 V5_3.0.1
[2] GBase 8c SQL 参考手册 V5_3.0.0
[3] GBase 8c 工具与命令参考手册 V5_5.0.0
热门帖子
- 12025-12-01浏览数:183532
- 22023-05-09浏览数:26250
- 42023-09-25浏览数:19920
- 52020-05-11浏览数:18595