GBase 8c
运维管理
文章
精选

GBase 8c 表结构变更前的对象依赖排查

发表于2026-04-02 09:56:33218次浏览2个评论

GBase 8c 表结构变更前的对象依赖排查

我最近看 GBase 8c 资料时,一个感受越来越明显:很多表结构变更失败,并不是 DDL 语法本身有问题,而是变更前没有把对象依赖和影响面查清楚。

现场里很常见的一类情况是:测试库里 ALTER TABLE 很顺,到了生产库却突然报依赖错误,或者虽然语句执行成功了,但后面的视图、触发器、过程、报表任务开始连锁异常。再往后查,往往不是数据库“脾气古怪”,而是对象之间本来就有引用关系,只是平时没把这张依赖网摊开看。

我自己理解下来,GBase 8c 的表结构变更,真正难的不是把 DDL 写出来,而是回答下面这几个问题:

  • 这个表现在被哪些对象引用了。
  • 这些引用是“能独立删”的,还是“跟着宿主一起删”的。
  • 这次变更是纯元数据动作,还是会引发表重写、空间占用和更长窗口期。
  • catalog 里能查到的依赖,和业务代码里手写的动态 SQL,分别要怎么确认。

所以我现在做 8c 结构变更,基本都会先做一轮“对象依赖体检”,再决定到底是直接改、分阶段改,还是新旧对象并行切换。

先把风险分层,不要一上来就执行 DDL

我最近整理下来觉得,表结构变更至少可以分成下面几类,处理思路完全不一样。

变更动作 现场最容易踩的点 我更倾向的处理方式
新增列 看起来简单,但默认值、非空约束可能触发额外扫描或重写 先加可空列,再分批回填,再补约束
删除列 依赖视图、规则、过程;删得快但空间不会立刻回来 先查依赖,再确认是否真要马上删
修改列类型 可能重写整表,窗口期和磁盘占用都要重估 热表优先走“新列替换”
重命名表/列 业务 SQL、存储过程、外部任务脚本容易漏改 先扫数据库对象,再扫应用脚本
删除表 影响范围最大,CASCADE 一旦用错很难止损 先把依赖对象列清楚,尽量不用盲删

我实际排查时一般先看两件事:

  1. 这次变更会不会触发表重写。
  2. 这张表上游下游挂了多少对象。

原因很简单。前者决定变更窗口,后者决定回滚难度。

我会先看的几个系统对象

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 新列替换方案
小表、低峰时段 可以考虑 不一定必须
热表、长事务多 风险偏高 更稳妥
有多个视图/过程依赖 不建议硬改 更容易做灰度切换
回滚要求严格 回滚路径不够直观 新旧列并行更好退回

我自己更偏向下面这种做法:

  1. 新增目标类型的新列。
  2. 分批回填。
  3. 先改视图、过程、报表口径。
  4. 观察一段时间。
  5. 最后再处理旧列。

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

评论

登录后才可以发表评论
用户头像
柒柒天晴发表于 5个月前
111
流泪猫猫头发表于 3个月前
很详细实用的文章