跳至主要内容

SQL 外键:让您的数据库关系始终井然有序

了解外键如何将表关联在一起,阻止无效数据混入,并帮助您的数据库保持一致性。
· 13分钟 阅读

使用 AI 探索

在关系型数据库中,数据通常分布在多个表中,每个表存储更大系统中的某个特定方面。这样的分离是良好的设计选择,但也带来了一个挑战:数据可能很快变得难以理解。

为了解决这一挑战,关系型数据库依赖于强制执行表间关系的规则。维护这种结构与可靠性最重要的机制之一就是外键,它用于确保相关数据保持对齐。

在本教程中,我将解释外键的工作原理。如果您刚开始做数据库工程师,建议学习我们的SQL 关系型数据库入门和数据库设计课程,了解在定义数据库架构时如何创建关系。

主键 vs. 外键

我们在另一篇文章中介绍过的主键以及外键,都有助于维护关系型数据库的结构与完整性。让我分别说明:

什么是外键?

外键是某个表中的一个或一组字段,用于引用另一个表中的主键。该外键在表之间创建链接,确保引用表中的数据与被引用表中的有效条目匹配。

假设您有一个包含 users 表和 orders 表的数据库。orders 表可能包含一个 user_id 列,它必须与 users 表中现有的 user_id 匹配。外键约束确保您无法创建关联到不存在用户的订单。

理解外键还需要理解主键,因为外键依赖主键来建立并维护这些关系。

什么是主键?

主键是一个或一组列,用于唯一标识表中的每条记录。两行不能共享同一个主键值,且主键值永远不能为 NULL。因此,一个表必须有一个主键,它是唯一标识符,可被其他表中的所有外键引用。

例如,下面的查询创建了 users 表,并将 user_id 列设为主键:

-- Create users table with user_id as primary key
CREATE TABLE users (
    user_id INT PRIMARY KEY, 
    username VARCHAR(100),
    email VARCHAR(255)
);

主键与外键的主要区别

我在下表中总结了主键与外键之间的差异:

方面

主键

外键

目的

在表内唯一标识每条记录

通过引用另一个表的主键来创建关系

唯一性

必须唯一

可以包含重复值

NULL 值

不能为 NULL

可以为 NULL(除非受限)

位置

定义在同一张表中

引用另一个表的主键

由以上说明可见,主键与外键对于创建结构化且可靠的数据库至关重要。它们协同工作,确保数据一致、关系得以维护,并保护数据库的完整性。

在 SQL 中构建与管理外键

现在您已经了解了什么是外键以及它们为何重要,接下来我们来学习在设计数据库时如何定义、管理并控制其行为。

使用 SQL 定义外键

您可以通过两种方式在 SQL 中设置外键:在创建表时定义,或在表已存在时后续添加。

下面的示例展示了在 SQL Server 中创建表时定义外键的第一种方式。

-- Create parent table: users
CREATE TABLE users (
    user_id INT IDENTITY(1,1) PRIMARY KEY,
    username NVARCHAR(100),
    email NVARCHAR(255)
);

-- Create child table: orders
CREATE TABLE orders (
    order_id INT IDENTITY(1,1) PRIMARY KEY,
    user_id INT,
    order_date DATE,
    CONSTRAINT fk_orders_users FOREIGN KEY (user_id)
        REFERENCES users(user_id)
);

在 PostgreSQL 中语法相同,不过主键的自增使用 SERIAL。

-- Create parent table: users
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,   -- SERIAL auto-increments
    username VARCHAR(100),
    email VARCHAR(255)
);

-- Create child table: orders
CREATE TABLE orders (
    order_id SERIAL PRIMARY KEY,
    user_id INT,
    order_date DATE,
    CONSTRAINT fk_orders_users FOREIGN KEY (user_id)
        REFERENCES users(user_id)
);

在 MySQL 中设置外键时,必须使用 InnoDB,如下所示。

-- Create parent table: users
CREATE TABLE users (
    user_id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(100),
    email VARCHAR(255)
) ENGINE=InnoDB;   -- Must use InnoDB for FK support

-- Create child table: orders
CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    order_date DATE,
    CONSTRAINT fk_orders_users FOREIGN KEY (user_id)
        REFERENCES users(user_id)
) ENGINE=InnoDB;

如果您使用的是 Oracle 数据库,您可以定义外键,但请确保两个表中的数据类型完全匹配。

-- Create parent table: users
CREATE TABLE users (
    user_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    username VARCHAR2(100),
    email VARCHAR2(255)
);

-- Create child table: orders
CREATE TABLE orders (
    order_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    user_id NUMBER,
    order_date DATE,
    CONSTRAINT fk_orders_users FOREIGN KEY (user_id)
        REFERENCES users(user_id)
);

要为已有表添加外键,请使用以下查询:

-- Assuming orders table already exists
ALTER TABLE orders
ADD CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
REFERENCES users(user_id);

建议学习我们的SQL Server 入门和创建 PostgreSQL 数据库课程,了解在创建数据库时不同 SQL 方言之间的差异。

管理约束与命名约定

外键是一种约束,是由数据库管理系统(DBMS)在插入、更新和删除等操作时用于校验数据的规则。当外键约束处于激活状态时,数据库会确保输入到外键列中的任意值都必须已经存在于被引用的主键列中。

一种常见做法是按照 fk_<child>_<parent> 的模式命名约束。在我们的示例中,外键就是 fk_orders_users。以这种方式命名有助于提高架构的可读性,便于调试约束违规问题,并且与迁移工具配合良好。以下操作都会触发约束校验:

  • INSERT:如果引用的父值不存在,则拒绝插入该行

  • UPDATE:阻止会破坏关系的修改。

  • DELETE:当存在依赖的子行时阻止删除操作。

引用完整性与级联操作

引用完整性是外键约束所强制执行的核心原则。外键通过防止孤立记录(当子表中的行指向父表中不存在的行时产生)来帮助执行这一原则。

SQL 还允许您设置级联操作。这些是自动指令,告诉数据库管理系统(DBMS)在被引用记录发生变化时应如何处理。例如,您可以使用这些级联操作来实现如下行为:

操作

行为

ON DELETE CASCADE

当父行被删除时删除子行

ON UPDATE CASCADE

当父键变化时更新子键

SET NULL

子表外键设为 NULL

SET DEFAULT

子表获得默认值

NO ACTION / RESTRICT

阻止破坏关系的更改

例如,下面的查询添加了级联操作:当删除用户时一并删除其订单;当 user_id 变化时自动更新。

-- Adds cascading rules to enforce hierarchical cleanup
ALTER TABLE orders
ADD CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
REFERENCES users(user_id)
ON DELETE CASCADE     -- deleting a user deletes their orders
ON UPDATE CASCADE;    -- if user_id changes, update automatically

设计表之间的关系

外键是数据建模中的基石,用于定义数据库中表与表之间的关系。本节将介绍外键在关联表时如何通过不同方式确保数据一致性。

一对多与多对多关系

在一对多(1:N)关系中,父表中的一行可以关联到子表中的多条记录。该关系通过将父表的主键(如 users.user_id)作为外键放在子表中(如 orders.customer_id)来强制执行。

例如,一个用户可以有多个订单,或一个部门可以有多名员工。

一对多关系示例。

一对多关系示例。图片由作者提供。

另一方面,多对多(M:N)关系指一个表中的多条记录可以与另一个表中的多条记录相关联。由于在规范化数据库中无法直接实现多对多关系,我们使用包含两个外键的联结表(桥表)。

多对多关系示例。

多对多关系示例。图片由作者提供。

复合外键与自引用外键

复合键由两个或多个列组成,它们共同唯一标识一条记录,且必须一起被引用。当自然键跨越多个字段,或您希望防止重复关系时会使用这种模式。例如,某个课程开课可由(course_id,semester)标识,或者某个联结表以 student_id 与 course_id 作为复合主键。

-- Defines courses table
CREATE TABLE course_offerings (
    course_id INT,
    semester VARCHAR(10),
    
    -- Composite primary key ensures a course can only be offered once per semester
    PRIMARY KEY (course_id, semester) 
);

-- Create enrollment table
CREATE TABLE enrollment (
    student_id INT,
    course_id INT,
    semester VARCHAR(10),

    -- This foreign key links enrollment records to a valid course offering
    FOREIGN KEY (course_id, semester)
        REFERENCES course_offerings(course_id, semester)
);

自引用外键(或递归外键)是指在同一张表内引用该表主键的外键。这种模式在层级数据中很常见,比如“员工 → 经理”。它允许在不创建额外表的情况下表示递归关系。

在下面的查询中,manager_id 引用的是同一张表中的 employee_id。

CREATE TABLE employees (
    employee_id INT PRIMARY KEY,
    name VARCHAR(100),

    -- manager_id references employee_id in the same table
    manager_id INT,
    FOREIGN KEY (manager_id) REFERENCES employees(employee_id)
);

处理数据并确保完整性

我们已经知道外键用于确保数据库中的数据完整性。本节将介绍在使用外键进行 SQL 操作时的实践要点、常见挑战与约束管理方法。

INSERT、UPDATE 与 DELETE 操作

下面看看这些操作如何与外键交互。

带外键的插入操作

要插入子记录,引用的父值必须存在。否则插入将失败。例如,若 users 表包含 user_id 为 1 的记录,下面的查询将把记录插入到 orders 表中。

-- users table contains user_id = 1
INSERT INTO orders (order_id, user_id, order_date)
VALUES (100, 1, '2024-02-01');

如果您尝试为父表中不存在的用户插入记录,将会收到外键约束违规的错误。

-- user_id = 999 does NOT exist in users table
INSERT INTO orders (order_id, user_id, order_date)
VALUES (101, 999, '2024-02-01');
ERROR: insert or update on table "orders" violates foreign key constraint "fk_orders_users"

更新操作与级联行为

更新可能影响外键或主键表。例如,如果您更新 orders 表中的外键,只有当父表中存在该主键时才会成功。比如,下面的更新只有在 users.user_id = 5 存在时才会成功。

-- updating foreign key value
UPDATE orders
SET user_id = 5
WHERE order_id = 100;

从上例可见,如果没有 ON UPDATE CASCADE,您无法更新主键,因为子行仍引用 1。为避免此问题,我们先删除现有外键约束,然后以启用 ON UPDATE CASCADE 的方式重新创建。这样,相关的 orders.user_id 值会自动更新。

-- Drop existing foreign key constraint
ALTER TABLE orders
DROP CONSTRAINT fk_orders_users;

-- Recreate FK with ON UPDATE CASCADE enabled
ALTER TABLE orders
ADD CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
    REFERENCES users(user_id)
    ON UPDATE CASCADE;   -- Automatically update child rows when parent key changes
UPDATE users
SET user_id = 500
WHERE user_id = 1;

删除操作与级联

如果在没有任何级联规则的情况下尝试删除父行,删除将失败,并返回错误。添加 ON DELETE CASCADE 子句后,它会指示数据库在删除父记录时自动删除任何相关的子行,以保持一致性。

在下面的示例中,我们在 orders.user_id 上创建引用 users.user_id 的外键。有了 ON DELETE CASCADE,当从 users 表删除某个用户时,所有相关订单都会自动被删除。

-- Add a new foreign key constraint to the orders table
ALTER TABLE orders
ADD CONSTRAINT fk_orders_users
FOREIGN KEY (user_id)
    REFERENCES users(user_id)
    ON DELETE CASCADE;             -- Delete orders automatically when user is deleted

有时您可能希望数据库自动处理删除。创建表时可通过 ON DELETE SET NULL 来强制执行。如果父记录发生变化或被删除,子表中的外键列将被设为 NULL。

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    user_id INT,               -- FK referencing users.user_id
    order_date DATE,
    FOREIGN KEY (user_id)
        REFERENCES users(user_id)
        ON DELETE SET NULL     -- If a user is deleted, set user_id in orders to NULL
);

处理常见挑战

在设计复杂数据库时,您很可能会遇到与外键相关的挑战。以下是我遇到的常见问题及各自的实用解决方案。

  • 数据类型不匹配:如果外键与被引用主键列的数据类型不兼容,将无法创建约束。为避免这种情况,请始终在相关表之间统一类型。

  • 循环依赖:当两个表相互引用时可能导致插入死锁。为解决此问题,您应重新评估架构设计,或先不加外键创建表、载入基础数据,再使用 ALTER TABLE 添加约束。

  • 悬挂外键:当子表的外键指向一个不存在的父记录时会出现该问题。可通过在删除与更新操作中使用级联动作来校验数据并修复。

管理修改与删除约束

在重新设计数据库架构时,您有时可能需要修改外键约束。以下是在不同 SQL 方言中删除外键的方法(语法略有差异)。

在 PostgreSQL、SQL Server 和 Oracle 数据库中,我们使用 DROP CONSTRAINT 语句。

-- Drop foreign key
ALTER TABLE orders
DROP CONSTRAINT fk_orders_users;

而在 MySQL 中,我们使用 DROP FOREIGN KEY 语句来移除外键。

-- Drop foreign key
ALTER TABLE orders
DROP FOREIGN KEY fk_orders_users;

请注意,移除外键约束会取消数据库对引用完整性的强制。移除之后,您可以插入或更新违反关系的数据,从而可能导致数据不一致。

性能与优化

通过上述示例,我们已经看到外键如何通过维护数据完整性来确保有效的数据设计。不过,您也应了解如何优化外键以提升数据库性能。以下是我在处理外键时认为有用的一些建议:

索引与查询性能

当您为外键建立索引时,数据库可以在插入、更新或删除操作期间更快地通过定位父表中的对应键来验证变更。同样,索引也能加速连接子表与父表的联接操作,使数据库引擎能够基于外键值匹配行。

作为最佳实践,建议在 DBMS 未自动处理的情况下,显式为外键列建立索引。

查询优化技巧

一些 SQL 引擎会利用外键的存在来应用联接消除(join elimination)技术,即识别并移除查询执行计划中不必要的联接。例如,如果查询只访问子表中的列,但为了筛选而包含了在外键约束下可被保证的父表联接,那么该联接操作可以被优化掉。

您随时可以查看执行计划,了解查询优化器如何处理外键与联接。这些优化及其效率在各 SQL 引擎(如PostgreSQL、MySQL 与 SQL Server)之间有所不同。理解您所用数据库的优化器行为,有助于编写充分利用外键约束、性能更优的查询。

例如,您可以使用以下查询在 PostgreSQL 中查看执行计划:

-- View execution plan
EXPLAIN ANALYZE
SELECT o.order_id, u.email
FROM orders o
JOIN users u ON o.user_id = u.user_id;

下表总结了在不同数据库中查看执行计划的方法:

数据库

命令

PostgreSQL

EXPLAIN / EXPLAIN ANALYZE

MySQL

EXPLAIN

SQL Server

估计/实际执行计划

Oracle

EXPLAIN PLAN FOR

我推荐您学习我们的SQL 中的数据联接课程,了解 SQL 中不同类型的联接以及如何处理数据库中的不同相关表。

高级用例与最佳实践

除了基础实现之外,您还可以利用外键构建稳健、可扩展且易维护的数据库架构。本节将介绍在使用外键时需要考虑的特殊场景与高层次设计原则。

跨数据库或跨架构的关系

尽管外键旨在在单个数据库或架构内强制执行完整性,但更复杂的企业环境往往需要跨越这些边界的关系。下表总结了不同 SQL 方言的跨架构支持与跨数据库支持情况。

数据库

跨架构支持

跨数据库支持

限制

PostgreSQL

支持

不支持

受限于单一数据库集群。

SQL Server

支持

支持

跨数据库完整性由应用逻辑或触发器管理,而非原生外键。

Oracle

支持

支持

数据库链接用于数据访问;通常不会在链接之间强制原生外键约束。

MySQL

不支持

不支持

严格限制在同一数据库与同一存储引擎(如 InnoDB)内的表。

SQLite

不支持

不支持

受限于特定的文件/数据库连接。

维护与审计

随着您构建外键,您会注意到它们会随着业务逻辑的变化而变化。请考虑以下方式对外键进行审计与维护,确保其保持更新:

  • 审查约束:定期审计外键约束,确保其与不断演进的业务规则与架构变更保持一致。
  • 定期索引审计:始终核查所有外键列上是否存在索引,以维持最佳的联接与校验性能,尤其是在大规模数据导入或系统升级之后。
  • 文档化与可视化:使用实体-关系(ER)图或架构可视化工具记录关系。这些可视化工具可确保所有开发者与分析师理解关系以及外键所强制执行的完整性规则。

健壮数据库设计的最佳实践

要充分发挥外键的作用,请遵循以下指南:

  • 使键与真实业务逻辑对齐:确保主键与外键真正代表了实际实体及其关系,而非依赖人为或不稳定的标识符。

  • 使用清晰、一致的命名:选择描述性约束名,如 fk_orders_user_id,以便更容易维护、调试并与他人协作。

  • 在严格与灵活之间取得平衡:约束应保护数据质量,但您的设计仍应允许在应用层处理实际的边界情况或校验。

  • 避免循环关系:设计架构时应避免外键依赖彼此回环,因为这会使插入、更新与删除变得不必要地复杂。

  • 优化性能:为外键正确建立索引,并谨慎应用级联操作,这样既能维护完整性又不至于拖慢系统。

结语

与任何技能一样,掌握外键来自实践。我鼓励您尝试进行架构建模、测试级联操作,并理解约束在真实场景中的表现。随着时间推移,您将学会在严格规则与应用所需的灵活性之间取得平衡,从而在需求演进的过程中构建既稳健又可适应的数据库。

现在您已经了解了如何在数据库设计中使用外键,建议您尝试我们的SQL 助理数据工程师职业路径,学习数据工程与数据仓库的基础知识。最后,如果您希望在大数据场景下提升数据库管理技能,推荐学习我们的Snowflake 数据建模入门课程,进一步了解维度建模。

Allan Ouko

作者

Allan Ouko

数据科学技术写作者,具备数据分析、商业智能和数据科学的一线实践经验。我撰写以行业为导向的实用内容,涵盖 SQL、Python、Power BI、Databricks 和数据工程,并以真实的分析工作为基础。我的写作兼顾技术深度与业务影响,帮助专业人士将数据转化为自信的决策。

FAQs

主键与外键有什么区别?

主键在其自身表中唯一标识记录,而外键通过引用另一个表的主键来建立关系。

一个表可以有多个外键吗?

可以,一个表可以有多个外键,每个外键可链接到不同的父表。

外键列在表中必须唯一吗?

不需要唯一。外键列可以包含重复值,但必须引用父表中的有效行。

删除被外键引用的记录会发生什么?

取决于约束,数据库可能会限制删除、级联删除相关行,或将外键值设为 NULL。

外键在关系型数据库中是强制的吗?

虽然不是强制性的,但外键对于强制实施关系完整性与可靠的数据库设计至关重要。

主题