PostgreSQL之外键约束

外键

又称外键约束,Foreign key constraint。

外键是一个表中,用于标识另一张表中行的一个字段或多个字段。包含外键的表称为引用表,外键引用表称为被引用表。所谓外键约束是指引用字段必须在被引用字段中出现。被引用字段需要是唯一约束或主键。

外键约束维护引用表和被引用表之间的参照完整性(referential integrity)。

外键约束可以在创建表时定义,也可以在表创建后通过alter table语句定义。

定义外键约束的完整语法

<span style="color:#000000"><span style="background-color:#ffffff"><code>[ CONSTRAINT constraint_name ] FOREIGN KEY ( column_name [, … ] ) REFERENCES reftable [ ( refcolumn [, … ] ) ] [ MATCH matchtype ] [ ON DELETE action ] [ ON UPDATE action ]
</code></span></span>

其中:
constraint_name : 外键约束名称
( column_name [, … ] ) : 引用表中的引用字段
reftable : 被引用表
( refcolumn [, … ] ) : 被引用表中的被引用字段,和( column_name [, … ] )对应。
MATCH [SIMPLE|FULL] : 外键匹配模式,如果引用字段全部不是NULL,则强匹配,否则根据匹配模式进行弱匹配。

  • SIMPLE,默认值,只要引用字段中任一字段为NULL,则不要求与被引用字段强匹配;

  • FULL,只有引用字段全部为NULL,才不要求与被引用字段强匹配。

ON DELETE [CASCADE | NO ACTION] : 默认NO ACTION。

  • CASCADE,删除被引用表数据时级联删除引用表数据

  • NO ACTION,删除被引用表数据时必须先删除引用表数据,否则,如果引用表如果存在数据,直接删除被引用表数据返回失败。

ON UPDATE [CASCADE | NO ACTION] : 默认NO ACTION

  • CASCADE,更新被引用表时级联更新引用表数据

  • NO ACTION,更新被引用表时必须先删除引用表数据,否则,如果引用表存在数据,直接更新被引用表数据返回失败。

创建表时定义外键约束

创建一张t_currency表

<span style="color:#000000"><span style="background-color:#ffffff"><code>CREATE TABLE t_currency
(
    id      int,
    shortcut    char (3),
    PRIMARY KEY (id)
);
</code></span></span>

创建一张t_product表, 其中包含外键约束currency_id引用t_currency的id字段。pg中定义外键约束需要用到REFERENCES关键字。

<span style="color:#000000"><span style="background-color:#ffffff"><code>CREATE TABLE t_product
(
    id      int,
    name        text,
    currency_id int REFERENCES t_currency (id),
    PRIMARY KEY (id)
);

</code></span></span>

上面提到被引用字段需要时被引用表的主键和唯一约束。如果我们引用了非主键和唯一约束会发生什么?来看一下。 创建一张表t_product1,定义外键约束引用t_currency的shortcut,shortcut既不是主键,也不是唯一约束。

<span style="color:#000000"><span style="background-color:#ffffff"><code>postgres=<span style="color:#5c6370"><em># CREATE TABLE t_product1</em></span>
(
    id      int,
    name        text,
    currency_id int REFERENCES t_currency (shortcut),
    PRIMARY KEY (id)
);
ERROR:  there is no unique constraint matching given keys <span style="color:#c678dd">for</span> referenced table <span style="color:#98c379">"t_currency"</span>
</code></span></span>

可以看到创建失败,提示“there is no unique constraint matching given keys for referenced table "t_currency"”

创建完外键约束之后,t_product和t_currency之间的参照完整性就建立了,也就说我不能在t_product中插入一条curruncy_id非空但没有出现在t_currency的记录。

尝试在t_product表中插入一条记录,其中currency_id等于1,此时t_currency表中并没有id等于1的记录。

<span style="color:#000000"><span style="background-color:#ffffff"><code>postgres=<span style="color:#5c6370"><em># INSERT INTO t_product VALUES (1, 'PostgreSQL consulting', 1);</em></span>
ERROR:  insert or update on table <span style="color:#98c379">"t_product"</span> violates foreign key constraint <span style="color:#98c379">"t_product_currency_id_fkey"</span>
DETAIL:  Key (currency_id)=(1) is not present <span style="color:#c678dd">in</span> table <span style="color:#98c379">"t_currency"</span>.
</code></span></span>

可以看到执行报错:

ERROR: insert or update on table "t_product" violates foreign key constraint "t_product_currency_id_fkey" DETAIL: Key (currency_id)=(1) is not present in table "t_currency".

这个点在实践场景是非常需要注意的,因为引用表和被引用表之间的参照完整性的存在,就已经确立了表记录的插入顺序。如果没有外键,则可以以任意顺序插入任何表,但有了外键,就需要保证正确的插入顺序

定义多个字段组成的外键

创建一张表t_unique, 它包含唯一约束uk_tbl_unique_a_b(a,b)

<span style="color:#000000"><span style="background-color:#ffffff"><code>create table t_unique(
a int not null,
b int,
c varchar(10) not null default <span style="color:#98c379">'catch u'</span>,
constraint uk_tbl_unique_a_b unique(a,b)
);
</code></span></span>

创建一张表t_child,定义外键约束引用t_unique的a,b字段。

<span style="color:#000000"><span style="background-color:#ffffff"><code>CREATE TABLE t_child(
  c1 <span style="color:#e6c07b">integer</span> PRIMARY KEY,
  c2 <span style="color:#e6c07b">integer</span>,
  c3 <span style="color:#e6c07b">integer</span>,
  FOREIGN KEY (c2, c3) REFERENCES t_unique (a, b)
);
</code></span></span>

给已存在的表定义外键

使用 ALTER TABLE 给一个已存在的表定义外键。

示例:

<span style="color:#000000"><span style="background-color:#ffffff"><code>ALTER TABLE t_child 
ADD CONSTRAINT fk_c1 FOREIGN KEY (c1) REFERENCES t_parent (p1);
</code></span></span>

删除外键

示例:

<span style="color:#000000"><span style="background-color:#ffffff"><code>alter table t_child drop constraint fk_c1;
</code></span></span>

禁用外键

有时候我们想让外键暂时失效,而不是删除它,可以怎么做?

<span style="color:#000000"><span style="background-color:#ffffff"><code>alter table t_product <span style="color:#e6c07b">disable</span> trigger all;
</code></span></span>

disable trigger all 会禁用表上的所有外键,同时也禁用负责验证约束的内部触发器。

<span style="color:#000000"><span style="background-color:#ffffff"><code>postgres=<span style="color:#5c6370"><em>#  INSERT INTO t_product VALUES (1, 'PostgreSQL consulting', 1);</em></span>
ERROR:  insert or update on table <span style="color:#98c379">"t_product"</span> violates foreign key constraint <span style="color:#98c379">"t_product_currency_id_fkey"</span>
DETAIL:  Key (currency_id)=(1) is not present <span style="color:#c678dd">in</span> table <span style="color:#98c379">"t_currency"</span>.
postgres=<span style="color:#5c6370"><em># alter table t_product disable trigger all;</em></span>
ALTER TABLE
postgres=<span style="color:#5c6370"><em>#  INSERT INTO t_product VALUES (1, 'PostgreSQL consulting', 1);</em></span>
INSERT 0 1
</code></span></span>

这一点在做数据迁移时也很重要。数据迁移的时,遇到有外键约束的表,如果不注意表数据的导入顺序将会导致数据加载失败。

怎么重新启用外键盘约束?

<span style="color:#000000"><span style="background-color:#ffffff"><code>alter table t_product <span style="color:#e6c07b">enable</span> trigger all;
</code></span></span>

看下有没有生效:

<span style="color:#000000"><span style="background-color:#ffffff"><code>postgres=<span style="color:#5c6370"><em># alter table t_product enable trigger all;</em></span>
ALTER TABLE
postgres=<span style="color:#5c6370"><em>#  INSERT INTO t_product VALUES (2, 'PostgreSQL consulting1', 2);</em></span>
ERROR:  insert or update on table <span style="color:#98c379">"t_product"</span> violates foreign key constraint <span style="color:#98c379">"t_product_currency_id_fkey"</span>
DETAIL:  Key (currency_id)=(2) is not present <span style="color:#c678dd">in</span> table <span style="color:#98c379">"t_currency"</span>.
</code></span></span>

启用之后,可以发现,外键约束已经启用,它会对于后续新插入或者更新的数据会进行检查。

这里有个问题,怎么去验证老的数据呢?

方法:修改pg_constraint表,将convalidated置为false,然后使用 Alter table validate constraint语句。
示例:

<span style="color:#000000"><span style="background-color:#ffffff"><code>postgres=<span style="color:#5c6370"><em># select convalidated from pg_constraint where conname = 't_product_currency_id_fkey';</em></span>
 convalidated
--------------
 t
(1 row)

postgres=<span style="color:#5c6370"><em># update pg_constraint set convalidated = false where conname = 't_product_currency_id_fkey';</em></span>
UPDATE 1
postgres=<span style="color:#5c6370"><em># select convalidated from pg_constraint where conname = 't_product_currency_id_fkey';</em></span>
 convalidated
--------------
 f
(1 row)

postgres=<span style="color:#5c6370"><em># Alter table t_product validate constraint t_product_currency_id_fkey;</em></span>
ERROR:  insert or update on table <span style="color:#98c379">"t_product"</span> violates foreign key constraint <span style="color:#98c379">"t_product_currency_id_fkey"</span>
DETAIL:  Key (currency_id)=(1) is not present <span style="color:#c678dd">in</span> table <span style="color:#98c379">"t_currency"</span>.
</code></span></span>

我们现在知道了对于存在外键约束的表,表数据的插入顺序很重要,对于这一点,cybertec提供了一个魔法SQL,用于查询我们应该插入数据的顺序。

<span style="color:#000000"><span style="background-color:#ffffff"><code>WITH RECURSIVE fkeys AS (
   /* <span style="color:#e6c07b">source</span> and target tables <span style="color:#c678dd">for</span> all foreign keys */
   SELECT conrelid AS <span style="color:#e6c07b">source</span>,
          confrelid AS target
   FROM pg_constraint
   WHERE contype = <span style="color:#98c379">'f'</span>
),
tables AS (
      (   /* all tables ... */
          SELECT oid AS table_name,
                 1 AS level,
                 ARRAY[oid] AS trail,
                 FALSE AS circular
          FROM pg_class
          WHERE relkind = <span style="color:#98c379">'r'</span>
            AND NOT relnamespace::regnamespace::text LIKE ANY
                    (ARRAY[<span style="color:#98c379">'pg_catalog'</span>, <span style="color:#98c379">'information_schema'</span>, <span style="color:#98c379">'pg_temp_%'</span>])
       EXCEPT
          /* ... except the ones that have a foreign key */
          SELECT <span style="color:#e6c07b">source</span>,
                 1,
                 ARRAY[ <span style="color:#e6c07b">source</span> ],
                 FALSE
          FROM fkeys
      )
   UNION ALL
      /* all tables with a foreign key pointing a table <span style="color:#c678dd">in</span> the working <span style="color:#e6c07b">set</span> */
      SELECT fkeys.source,
             tables.level + 1,
             tables.trail || fkeys.source,
             tables.trail @> ARRAY[fkeys.source]
      FROM fkeys
         JOIN tables ON tables.table_name = fkeys.target
      /*
       * Stop when a table appears <span style="color:#c678dd">in</span> the trail the third time.
       * This way, we get the table once with <span style="color:#98c379">"circular = TRUE"</span>.
       */
      WHERE cardinality(array_positions(tables.trail, fkeys.source)) < 2
),
ordered_tables AS (
   /* get the highest level per table */
   SELECT DISTINCT ON (table_name)
          table_name,
          level,
          circular
   FROM tables
   ORDER BY table_name, level DESC
)
SELECT table_name::regclass,
       level
FROM ordered_tables
WHERE NOT circular
ORDER BY level, table_name;
</code></span></span>

输出结果示例:

<span style="color:#000000"><span style="background-color:#ffffff"><code> table_name | level
------------+-------
 t_currency |     1
 t_product  |     2
(2 rows)
</code></span></span>

该篇已首发到公众号PostgreSQL运维技术,欢迎来踩~

悄悄放一张:

PostgreSQL运维技术 

 

参考文档

https://www.cybertec-postgresql.com/en/postgresql-foreign-keys-and-insertion-order-in-sql/ https://www.infoq.cn/article/kahutf7zlid0biyhadiq

<think>嗯,用户这次提出的问题很专业啊,他想了解数据库设计除了范式之外的核心思想和最佳实践。看来用户对数据库设计已经有了一定基础,不满足于教科书式的范式理论,想要更落地的实战经验。从用户提供的引用材料来看,他应该已经掌握了1NF到5NF的基本概念,特别是反复提到了多值依赖和反范式设计。不过用户特意强调“除了范式”,说明他可能在实际工作中遇到了范式无法解决的痛点,比如性能问题或者业务模型适配问题。用户身份推测可能是中级开发人员或DBA,正在参与实际数据库设计项目。他的深层需求可能是想平衡理论规范和工程实践,尤其是在高并发或复杂业务场景下的设计技巧。上次回答中我提到过反范式设计和一致性工程,他这次显然想深入这个方向。需要特别注意用户提到的“核心思想和主要方面”这两个关词。核心思想应该聚焦数据库的本质目标:数据安全和高效访问;主要方面则需要覆盖全生命周期,从抽象建模到具体优化策略。关于“除了范式之外的最佳实践”,其实业内有很多经验性规则。比如在物联网场景常用“数据分桶”策略,电商系统的“冗余计数”方案,但这些可能过于具体。更适合用户的是跨行业通用的五大支柱:数据完整性、性能、安全、可扩展和维护成本。每个支柱下都可以展开典型方法,比如读写分离具体怎么降低主库压力。考虑到用户上次关注反范式,这次可以强化使用场景的对比分析。特别注意标注反范式的风险控制手段,比如触发器补偿机制,这样用户不会误以为反范式就是随便破坏规则。技术上还需要澄清一个概念误区:很多人认为非关系型数据库就不需要设计原则。其实文档型数据库同样有“索引优化”“数据分片”等核心思想,这点可以在“技术选型”部分点明。不过用户没明确问NoSQL,简单提及即可。最后示例部分非常关。用户上次对学院案例有互动,这次延续教育系统场景能降低理解成本。选课系统的例子能同时展示范式理论的局限(效率问题)和反范式带来的提升,顺带引出查询优化的可视化解释计划。</think> 好的,数据库设计远不止范式理论,范式只是其“规范化”的一个核心方面。完整的数据库设计需要综合考虑多个维度和核心思想。以下是数据库设计的核心思想、主要方面以及超越范式的最佳实践: **核心思想:** 1. **准确性与完整性:** 核心目标是**准确、完整**地反映现实业务中的数据及其关系(即数据模型)。数据必须真实反映业务实体、状态和规则。 2. **易用性与效率:** 设计必须便于用户(应用程序和最终用户)**高效**地存储、检索、操作和管理数据。查询性能、更新速度是关考量。 3. **数据一致性:** 确保数据在数据库中的**一致**状态。避免冗余导致的不一致(范式着力于此),并维持实体完整性、参照完整性等约束。 4. **可维护性与可扩展性:** 设计应易于理解、修改和适应未来的业务变化或数据量增长(**可伸缩性**)。结构清晰、文档齐全至关重要。 5. **安全性:** 保护数据免受未授权访问、修改或破坏,确保数据的保密性、完整性和可用性。 **数据库设计的主要方面:** 1. **需求分析:** * 深入了解业务领域、流程、规则和用户需求。 * 识别核心**实体(Entity)**(如:用户、订单、产品)及其关**属性(Attribute)**。 * 确定实体之间的**关系(Relationship)**(一对一、一对多、多对多)以及**基数约束**。 * 收集业务规则(如:订单状态转换规则、库存不能为负等)。 * *核心思想:准确性、完整性、理解业务。* 2. **概念数据模型(CDM):** * 使用ER图(实体-关系图)等工具可视化表示实体、属性和关系。 * 专注于业务需求,**独立于具体数据库技术**。 * *核心思想:抽象化业务需求、建立领域共识。* 3. **逻辑数据模型(LDM):** * 将概念模型转化为适用于关系型数据库的结构(表)。 * **应用范式(Normalization)**:目标是减少冗余、消除异常(插入、更新、删除异常)。这是保证数据一致性和完整性基础的重要过程[^2][^3]。 * 定义: * 表(Table)对应实体或关系。 * 列(Column/Field)对应属性。 * 主(Primary Key, PK)唯一标识行。 * 外Foreign Key, FK)表示表间关系。 * 定义数据类型、是否允许空值等约束。 * *核心思想:数据一致性、结构清晰、减少冗余、理论基础(范式)。* 4. **物理数据模型(PDM):** * 针对具体的数据库管理系统(DBMS,如 MySQL, Oracle, PostgreSQL)实现逻辑模型。 * 进行**性能优化**考虑: * **反范式化(Denormalization)**:*有目的地引入数据冗余*以提升查询效率(如:在订单表中冗余客户姓名,避免每次都要联查客户表)[^1]。这是范式之外最关的*实践权衡*。 * **索引(Indexing)**:创建合适的索引(聚簇索引、非聚簇索引、覆盖索引、全文索引等)加速搜索。索引设计是一门艺术。 * **分区(Partitioning)**:将大表按规则(范围、列表、哈希)拆分成物理文件,提升查询和管理效率。 * 数据类型选择(考虑存储空间和精度)。 * 存储引擎选择(如 InnoDB vs MyISAM in MySQL)。 * 定义表空间、文件组等物理存储结构。 * *核心思想:性能、效率、技术适配、物理存储、工程权衡(反范式化!)。* 5. **完整性约束定义:** * **实体完整性:** 主值唯一且非空。 * **参照完整性:** 外值必须存在于父表的主中,或为空(根据业务规则)。 * **域完整性:** 列的数据类型、格式、范围(CHECK约束)或是否允许空值(NOT NULL)。 * **业务规则约束:** 使用存储过程、触发器或应用逻辑实现更复杂的业务规则。 * *核心思想:数据一致性、准确性、业务规则强制。* 6. **安全设计:** * 用户角色与权限管理(如:GRANT, REVOKE,基于角色的访问控制 RBAC)。 * 数据加密(传输加密 SSL/TLS,静态加密 TDE)。 * 审计(Audit Trail):记录关操作。 * *核心思想:数据安全、访问控制、合规性。* **超越范式的最佳实践:** 1. **适度反范式化(关!):** 这是平衡范式与性能的核心手段。 * **常见场景:** 频繁执行的复杂查询(如包含多个JOIN的报告)、读密集型应用、需要极低查询延迟的场景。 * **常见手法:** 预先计算聚合值(计数、总和、平均值存入表)、冗余常用查询需要的列、创建包含多表数据的物化视图。 * **重要原则:** **理解并明确引入冗余的目的和代价**。冗余带来性能提升的同时,增加了更新复杂度和存储成本,必须通过应用逻辑或触发器维护冗余数据的一致性。 2. **以查询为中心的设计:** 分析系统核心业务流程和高频查询语句,优化相关表的关联方式和索引策略。 3. **合理选择主:** * 优先使用**代理(Surrogate Key)** (如:自增整数 ID,UUID),而非自然(如身份证号、产品编码)。代理短小、稳定、无业务含义,性能更好,不易因业务变化而改变。 * 谨慎使用复合主,会增加外的复杂度。 4. **一致性工程:** * **原子性(Atomicity):** 使用事务(Transaction)确保操作要么全成功要么全失败。 * **隔离性(Isolation):** 理解不同事务隔离级别(Read Uncommitted, Read Committed, Repeatable Read, Serializable)对性能和一致性的影响。 * 最终一致性策略:在分布式数据库或强调高可用性的场景,可能需要接受最终一致性。 5. **文档化:** * 详细记录数据字典(表、列、含义、类型、约束)。 * 记录ER图、重要的设计决策(如反范式选择原因)、业务规则、核心查询。 * 记录权限设置和变更历史。 * *核心思想:可维护性、团队协作。* 6. **考虑可扩展性:** * 垂直扩展(升级服务器) vs 水平扩展(分库分表 Sharding):设计时就需有所考虑。 * 分片策略(如何拆分数据)对应用透明性设计的影响很大。 7. **技术选型:** * 关系型数据库(RDBMS) vs 非关系型数据库(NoSQL)?根据数据结构(结构化/半结构化/非结构化)、查询模式(点查/复杂联查/聚合)、一致性要求、扩展性需求来选择。**没有银弹**。核心思想:**合适胜于单一理论**。范式主要针对 RDBMS。 **总结核心思想:** 数据库设计是在 **准确完整建模业务数据、确保强一致性** (范式的重要贡献)与 **追求极致性能和可扩展性** (反范式化、索引、分区等工程实践的舞台)之间**进行精心权衡的艺术**。一个“好”的设计必须在这些冲突目标中找到一个平衡点,并充分考虑安全性、可维护性和未来的演化成本。范式是基础规范,而高效的设计需要在理解范式原理的基础上,根据实际场景,明智地运用和**突破**这些规范来实现综合目标[^1][^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值