Aurora DSQL 补上外键约束:在分布式数据库中守住引用完整性

2026-09-28 17 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

AWS 宣布 Aurora DSQL 支持外键约束,应用现在可以把引用完整性直接交给数据库执行,并使用 CASCADE、SET NULL 等引用动作。这个能力看似是关系型数据库的基础配置,却长期影响着一部分团队是否愿意采用 Aurora DSQL:如果数据库不能阻止孤儿记录,所有写入路径都必须自行维护数据关系。

外键上线之后,应用层校验依然有价值,但它不再是数据一致性的最后一道防线。

外键解决的是“所有写入者都必须正确”的问题

假设订单属于客户,订单明细又属于订单。没有外键时,创建订单的 API 可能会检查客户是否存在,但批处理脚本、数据修复任务、后台消费者或者未来新增的服务未必执行同样的检查。

这类架构实际上隐含了一个很强的前提:每一个写入者都必须永久遵守同一套规则。只要有一条路径遗漏校验,就可能出现以下数据:

  • 指向不存在客户的订单;
  • 订单已经删除,但订单明细仍然保留;
  • 业务对象删除后,关联表仍保存无效标识符;
  • 应用读取数据时不得不增加额外的容错和清理逻辑。

外键把规则放到数据所在的位置。无论写入来自 API、脚本还是运维工具,数据库都会执行同一套约束。

不过,数据库约束并不取代业务校验。应用仍应返回清晰的错误,例如“客户不存在”或者“该对象仍被引用”;外键负责处理竞态条件、遗漏路径和最终防线。

CASCADE 与 SET NULL 表达的是不同生命周期

引用动作不只是删除时的便利选项,它们描述了数据之间的所有权关系。

  • ON DELETE CASCADE:父记录删除时,子记录也应消失。适合生命周期完全从属于父对象的数据,例如订单明细。
  • ON DELETE SET NULL:父记录删除后,子记录仍有独立保留价值,只是解除关联。对应列必须允许 NULL。
  • 阻止删除的默认或限制性行为:父对象仍被引用时拒绝删除,适合账务记录、审计对象等必须显式处理的关系。

不要为了减少清理代码而统一使用级联删除。在关系层级较深、单个父对象关联大量子记录时,一次看似简单的 DELETE 可能影响很大范围。模型设计阶段应明确哪些表是“组成部分”,哪些表只是“有关联”。

可以这样实践:同时验证级联删除和解除关联

下面是一组可复制并按环境改造的 SQL。它只使用常见的关系型 DDL 和 DML,展示客户、订单、订单明细和支持工单之间的关系。运行前请确认当前 Aurora DSQL 版本、客户端连接方式及具体 DDL 支持情况。

CREATE TABLE customers (
    customer_id BIGINT PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    created_at TIMESTAMP NOT NULL,
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id)
        REFERENCES customers (customer_id)
        ON DELETE CASCADE
);

CREATE TABLE order_items (
    item_id BIGINT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    sku TEXT NOT NULL,
    quantity INTEGER NOT NULL,
    CONSTRAINT fk_items_order
        FOREIGN KEY (order_id)
        REFERENCES orders (order_id)
        ON DELETE CASCADE
);

CREATE TABLE support_tickets (
    ticket_id BIGINT PRIMARY KEY,
    customer_id BIGINT NULL,
    subject TEXT NOT NULL,
    CONSTRAINT fk_tickets_customer
        FOREIGN KEY (customer_id)
        REFERENCES customers (customer_id)
        ON DELETE SET NULL
);

INSERT INTO customers (customer_id, name)
VALUES (1001, 'Example Customer');

INSERT INTO orders (order_id, customer_id, created_at)
VALUES (5001, 1001, CURRENT_TIMESTAMP);

INSERT INTO order_items (item_id, order_id, sku, quantity)
VALUES (9001, 5001, 'SKU-RED-01', 2);

INSERT INTO support_tickets (ticket_id, customer_id, subject)
VALUES (7001, 1001, 'Historical delivery question');

DELETE FROM customers
WHERE customer_id = 1001;

-- 订单及订单明细应随客户一起删除。
SELECT * FROM orders WHERE order_id = 5001;
SELECT * FROM order_items WHERE item_id = 9001;

-- 工单仍然存在,但 customer_id 应变成 NULL。
SELECT ticket_id, customer_id, subject
FROM support_tickets
WHERE ticket_id = 7001;

这个例子刻意让两种关系采用不同策略:订单被视为客户生命周期的一部分,而支持工单作为历史服务记录继续保留。实际系统还应结合审计、合规和数据保留政策决定删除语义。

给存量数据加约束前,先找出孤儿记录

外键最容易在新建表时启用。对已有系统而言,真正困难的通常不是写出 FOREIGN KEY,而是确认历史数据已经满足约束。

以订单和客户为例,可以先运行反连接查询:

SELECT
    o.order_id,
    o.customer_id
FROM orders AS o
LEFT JOIN customers AS c
    ON c.customer_id = o.customer_id
WHERE c.customer_id IS NULL;

如果查询返回记录,需要先决定修复策略:

  1. 补建缺失的父记录;
  2. 删除确认无价值的孤儿数据;
  3. 将可选关系改为 NULL;
  4. 把异常记录迁移到隔离表,等待人工审核。

不要直接把不明来源的孤儿记录批量删除。订单、账务、权限和审计数据往往有额外的保留要求。对于现有大表,还应在变更前确认 Aurora DSQL 当前版本添加约束的 DDL 方式、锁定行为和上线限制,再设计分批迁移与回滚方案。

分布式环境下仍需评估代价

外键减少了应用代码中的一致性漏洞,但约束检查不是免费的。每次插入、更新或删除都可能需要确认关联记录,并维护相应的数据关系。在分布式数据库里,这类工作尤其值得通过真实负载进行验证。

上线前建议检查:

  • 高频写入链路增加外键后,延迟和吞吐是否仍符合目标;
  • 级联删除的最大影响行数是否可控;
  • 外键列及相关访问路径是否具备合适的索引设计;
  • 服务是否正确处理约束失败,而不是把它统一转换成 500 错误;
  • 数据导入、回放和恢复流程是否按照正确的父子顺序执行;
  • 是否存在环形依赖或过深的级联链条;
  • 当前 Aurora DSQL 版本对目标引用动作和模式变更方式的支持是否满足需求。

Aurora DSQL 增加外键支持,重要之处不只是多了一项 SQL 功能,而是团队终于可以把跨写入路径的数据关系交给数据库统一保护。采用时最稳妥的方式,是从边界清晰的新表开始,清理存量孤儿数据,对级联范围做压力测试,再逐步把关键关系从应用约定升级为数据库约束。


相关推荐