依赖跟踪
数据库对象很少彼此孤立:视图依赖表和列,外键依赖目标键,索引依赖表,函数可能依赖类型,扩展又管理成组对象。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 只是省去手动删除
它可能跨多层删除大量对象。方便不是使用它的充分理由。
重命名不会影响任何东西
内部依赖往往能跟随对象身份,但应用文本、外部系统和接口列名不会自动迁移。
重新创建对象等价于替换
删除重建可能改变对象身份,并丢失权限、所有者、注释和下游依赖。
依赖检查清单
- 是否先使用
RESTRICT观察数据库已知依赖; - 是否检查视图、约束、索引、函数、触发器、序列和扩展;
- 是否递归查看更深层依赖;
- 是否搜索应用代码、动态 SQL、报表、ETL 和 CDC;
- 是否避免未经审查的
CASCADE; - 是否优先采用新增、迁移、最后删除的兼容流程;
- 删除重建后是否恢复权限、所有权和注释;
- 是否在同版本、近似数据环境演练;
- 是否准备回滚或向前修复方案;
- 是否在上线后观察所有已知消费者。
依赖跟踪是数据库提供的第一层保护,不是完整系统架构图。把数据库目录、代码搜索和外部消费者清单结合起来,才能真正判断一次结构变更会影响什么。