跳到主要内容

依赖跟踪

数据库对象很少彼此孤立:视图依赖表和列,外键依赖目标键,索引依赖表,函数可能依赖类型,扩展又管理成组对象。PostgreSQL 会在系统目录中记录许多对象依赖,用于阻止破坏性删除或按要求级联处理。

依赖跟踪能保护数据库内部已知关系,但不能识别应用代码、动态 SQL、外部报表和字符串形式对象名中的所有引用。因此结构变更必须同时检查数据库目录和代码仓库。

一个简单示例​

CREATE TABLE products (
product_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_name TEXT NOT NULL,
price NUMERIC(12, 2) NOT NULL
);

CREATE VIEW affordable_products AS
SELECT product_id, product_name
FROM products
WHERE price < 100;

尝试删除被视图使用的列:

ALTER TABLE products DROP COLUMN price;

PostgreSQL 会拒绝操作,并指出视图依赖该列。这就是默认的受限删除保护。

RESTRICT 与 CASCADE​

RESTRICT​

如果存在需要保留的依赖对象,就拒绝删除:

DROP TABLE products RESTRICT;

RESTRICT 通常是默认行为。它迫使维护者先了解影响并显式处理依赖。

CASCADE​

连同依赖对象一起删除:

DROP TABLE products CASCADE;

它可能删除视图、约束和更深层的依赖链。输出中的 NOTICE 不能替代变更前审查,生产脚本也不应为了“避免报错”习惯性添加 CASCADE。

CASCADE 只沿 PostgreSQL 已记录的依赖传播,不会删除应用源代码、缓存、消息消费者或外部 BI 报表中的引用。

常见依赖关系​

视图依赖表和列​

CREATE VIEW product_summary AS
SELECT product_id, product_name
FROM products;

删除或改变相关列时,视图会阻止不兼容操作。重命名列通常能依靠内部对象身份保持关系,但视图对外显示的列名和应用查询仍需验证。

外键依赖唯一键​

CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL
REFERENCES products (product_id)
);

外键依赖被引用表及其唯一性保证。删除目标表、列或约束时,数据库会检查这些关系。

索引依赖表​

CREATE INDEX idx_products_name
ON products (product_name);

删除表时,表自己的索引会作为内部组成部分自动删除,不需要为每个索引单独写 DROP。

标识列依赖序列​

标识列使用的内部序列与列具有紧密依赖。删除标识列或表时,序列会按其依赖类型被自动清理。

扩展拥有成员对象​

扩展中的函数、类型和操作符通常作为扩展成员管理。删除扩展会删除成员对象;不应把扩展管理的对象当成普通手工对象随意修改。

依赖类型的概念​

PostgreSQL 在 pg_depend 等目录中记录不同依赖类型。理解概念比死记内部代码更重要。

依赖概念典型含义
普通依赖被依赖对象不能在 RESTRICT 下删除,可用 CASCADE
自动依赖依赖对象可单独删除,主对象删除时会自动清理它
内部依赖依赖对象属于另一个对象的内部实现,应通过主对象管理
扩展依赖对象是扩展成员,通常随扩展删除
固定依赖系统初始化所需对象,不能按普通方式删除

系统目录还会区分分区主次等更细的依赖关系,具体代码与行为应查阅当前 PostgreSQL 版本文档。

查询依赖​

从错误信息开始​

最安全、最直接的方法常常是在测试环境使用不带 CASCADE 的 DDL,让 PostgreSQL 列出阻止操作的已知依赖。

使用 psql​

psql 的 \d、\d+、\dv+、\df+ 等命令可以查看对象定义与部分关联信息。

\d+ products
\dv+ affordable_products

查询系统目录​

下面展示依赖目录的基本形态:

SELECT
pg_describe_object(d.classid, d.objid, d.objsubid)
AS dependent_object,
pg_describe_object(d.refclassid, d.refobjid, d.refobjsubid)
AS referenced_object,
d.deptype
FROM pg_depend AS d
WHERE d.refclassid = 'pg_catalog.pg_class'::regclass
AND d.refobjid = 'products'::regclass
ORDER BY dependent_object;

这不是完整的通用依赖查询器:

  • 数据库共享对象可能记录在其他目录;
  • 重写规则会让视图依赖表现得较间接;
  • 依赖方向需要根据目标问题调整;
  • 对象 OID 与目录类型需要正确解释;
  • 应过滤系统对象并处理递归链。

生产级依赖报告应使用经过验证的查询或可信管理工具,不要仅凭一条示例 SQL 做删除决策。

数据库无法跟踪的依赖​

以下引用通常不会被完整记录:

  • 应用源代码中的 SQL 字符串;
  • ORM 模型和迁移外的映射;
  • 存储在表中的动态查询文本;
  • PL/pgSQL 中运行时拼接的动态 SQL;
  • 外部 BI、ETL、脚本和定时任务;
  • 另一个数据库中的 FDW、视图或同步配置;
  • 日志解析、CDC 消费者和数据契约;
  • 通过文本配置引用的表名、列名或函数名。

因此“数据库允许删除”只表示它没有发现阻止操作的已记录依赖,不代表整个系统不会受影响。

函数依赖的细节​

函数参数类型和返回类型通常会形成明确依赖。但以字符串保存的过程语言函数体,内部引用可能直到执行或首次规划时才解析,数据库未必在创建时记录所有对象级依赖。

在支持的场景中,使用 SQL 标准形式的函数体能让解析更早发生,从而更早发现错误和记录依赖。例如:

CREATE FUNCTION product_name(BIGINT)
RETURNS TEXT
LANGUAGE SQL
STABLE
RETURN (
SELECT p.product_name
FROM products AS p
WHERE p.product_id = $1
);

动态 SQL 仍然无法完整静态跟踪。重构前应搜索函数定义文本并执行回归测试。

依赖与重命名​

PostgreSQL 内部通常通过对象标识而不是名称字符串跟踪依赖。因此:

ALTER TABLE products
RENAME COLUMN product_name TO display_name;

已记录的视图等依赖往往能继续指向同一列。但以下内容不会自动修复:

  • 应用 SQL;
  • 动态 SQL 字符串;
  • 视图向调用方暴露的既有列名契约;
  • 外部工具缓存的元数据;
  • 文档、监控和告警规则。

重命名仍应使用兼容发布策略,而不是因为数据库没有报错就直接上线。

依赖与 CREATE OR REPLACE​

某些对象支持 CREATE OR REPLACE,可以在保留对象身份和依赖的情况下修改定义:

CREATE OR REPLACE VIEW active_products AS
SELECT product_id, product_name, price
FROM products
WHERE discontinued_at IS NULL;

但替换通常要求输出列结构保持兼容,不能完成任意变更。函数替换也不能随意改变名称或参数类型,因为那会成为另一个重载对象。

先删除再创建会丢失对象身份,并可能丢失权限、注释、所有权或下游依赖。迁移脚本应明确恢复这些元数据。

安全的结构变更流程​

1. 收集数据库依赖​

使用 RESTRICT、系统目录、psql 和管理工具列出表、列、视图、约束、函数、触发器与扩展关系。

2. 搜索仓库和外部消费者​

搜索对象名、旧列名和 API 字段,同时检查 BI、ETL、CDC、报表、定时任务和其他服务。

3. 采用兼容性变更​

优先新增对象或列,先让新旧消费者并存,再迁移读取与写入。删除放到后续发布。

4. 在相同版本测试恢复​

从接近生产的备份恢复测试环境,执行迁移、回滚和关键查询,确认实际依赖链。

5. 记录和观察​

上线后观察错误率、慢查询、复制、任务失败和外部报表。结构变更完成不等于消费者迁移完成。

删除对象的原则​

推荐:

DROP VIEW affordable_products;
DROP TABLE products;

也就是按理解后的依赖顺序显式删除。只有当确实希望删除整条依赖链,并已审查影响时才使用:

DROP TABLE products CASCADE;

把数据库提示的级联对象列表保存到变更记录中,便于审查和恢复。

常见误区​

PostgreSQL 会跟踪所有依赖​

它只能跟踪数据库内部能够解析和记录的关系,无法理解所有动态 SQL 与外部系统。

CASCADE 只是省去手动删除​

它可能跨多层删除大量对象。方便不是使用它的充分理由。

重命名不会影响任何东西​

内部依赖往往能跟随对象身份,但应用文本、外部系统和接口列名不会自动迁移。

重新创建对象等价于替换​

删除重建可能改变对象身份,并丢失权限、所有者、注释和下游依赖。

依赖检查清单​

  1. 是否先使用 RESTRICT 观察数据库已知依赖;
  2. 是否检查视图、约束、索引、函数、触发器、序列和扩展;
  3. 是否递归查看更深层依赖;
  4. 是否搜索应用代码、动态 SQL、报表、ETL 和 CDC;
  5. 是否避免未经审查的 CASCADE;
  6. 是否优先采用新增、迁移、最后删除的兼容流程;
  7. 删除重建后是否恢复权限、所有权和注释;
  8. 是否在同版本、近似数据环境演练;
  9. 是否准备回滚或向前修复方案;
  10. 是否在上线后观察所有已知消费者。

依赖跟踪是数据库提供的第一层保护,不是完整系统架构图。把数据库目录、代码搜索和外部消费者清单结合起来,才能真正判断一次结构变更会影响什么。