数据库 Schema、FK 约束与 JOIN 查询

理解数据库结构定义、数据完整性约束、表间查询关系

一、Schema(模式)= 数据库的结构定义

Schema 描述数据长什么样,不涉及数据本身。它包含以下所有元素:

组成部分 作用 示例
表 (Table) 定义有哪些表、每个表的列和列类型 CREATE TABLE guests (id VARCHAR, name TEXT, ...)
约束 (Constraint) 对数据的规则限制 PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK
索引 (Index) 加速查询的数据结构 CREATE INDEX idx_appointments_date ON appointments(date)
触发器 (Trigger) 数据变动时自动执行的函数 AFTER INSERT ON schedules_daily EXECUTE fn_generate_times()
视图 (View) 虚拟表,基于查询定义 CREATE VIEW v_active_employees AS SELECT ...

约束的 5 种类型

约束 作用 示例
PRIMARY KEY 唯一标识一行,不能重复、不能为空 id VARCHAR(255) PRIMARY KEY
FOREIGN KEY 引用另一张表的主键,保证引用有效 guest_id REFERENCES guests(id)
UNIQUE 列值不能重复 email VARCHAR(255) UNIQUE
NOT NULL 列值不能为空 name TEXT NOT NULL
CHECK 自定义条件 CHECK (price > 0)
PostgreSQL 的另一层含义:在 PostgreSQL 里,Schema 还有命名空间的意思。Celoria 的多租户架构就是利用了这一点:public schema、tenant_spa001 schema,每个 schema 里有一套独立的表。但本文讨论的是广义的"数据库结构定义"。

二、FK 约束 vs JOIN 查询

这是两个完全不同的概念,但非常容易混淆。

FK 约束 = 写入时的规则

FK 只在你 INSERT / UPDATE / DELETE 的时候起作用——它阻止你写入非法数据

-- 定义:appointments 的 guest_id 必须指向 guests 表里存在的 id
ALTER TABLE appointments
ADD CONSTRAINT fk_appointments_guest
FOREIGN KEY (guest_id) REFERENCES guests(id);

加了这个约束后:

JOIN = 读取时的操作

JOIN 是查询时你主动告诉数据库怎么把两张表关联起来读取。

-- 查询:把 appointments 和 guests 关联起来读取
SELECT a.id, g.name
FROM appointments a
JOIN guests g ON a.guest_id = g.id;

JOIN 不需要 FK 约束存在——只要两张表有可以匹配的列,你就能 JOIN。

核心区别对比

FK 约束 JOIN 查询
作用时机 写入数据时(INSERT / UPDATE / DELETE) 读取数据时(SELECT)
目的 保证数据完整性(拒绝非法数据) 关联查询多张表的数据
是否必须 可选(没有也能跑) 需要关联数据时才写
谁触发 数据库自动检查 开发者手动写 SQL
性能影响 写入变慢(每次 INSERT 需校验) 读取时按需消耗
一句话总结:建立表与表之间查询关系的是 JOIN,不是 FK。FK 只是保证你 JOIN 的时候不会碰到孤儿数据(比如 appointment 引用了一个不存在的 guest_id)。

三、Celoria 项目的实际情况

比喻:你写了一份建筑图纸,上面标注了"这面墙要装一道防火门"。但施工的时候这道门没装。墙在、门洞在,但防火门不在。

"Schema 声明 FK" vs "DB 强制 FK"

Schema 声明 FK DB 强制 FK
含义 SQL 文件里写了 REFERENCES 语句 information_schema 里能查到的约束
位置 database/schema/tables/*.sql 文件中 PostgreSQL 数据库内部
是否强制执行 不强制(只是代码文件) 强制(数据库拒绝违规写入)
Celoria 示例 appointments, employees, guests 等核心表 day_end_closeouts, campaign_*, tip_distributions 等后期模块

具体来说:

database/schema/tables/05_appointments.sql 文件里写了:

guest_id VARCHAR(255) REFERENCES guests(id) ON DELETE CASCADE

但数据库里 appointments 表实际上没有这个 FK 约束

这意味着你可以执行:

INSERT INTO appointments (guest_id) VALUES ('random_garbage');
-- 不会报错!因为没有 FK 约束来阻止

为什么会这样?

早期核心业务表建表时可能没有跑那些 SQL 文件,或者建表脚本没包含 FK 部分。后来通过迁移脚本(migration)添加的模块(财务、营销等)正确部署了 FK 约束。

统计数据(2026-02-08 实测)

数据源 FK 边数 说明
数据库实际 FK 64 主要在财务/运营/营销模块
Schema 文件声明的 FK 36 主要在核心业务表(未部署到 DB)
依赖图中标记的 FK 25 大部分是 Schema 声明的,非 DB 强制的
风险:核心业务表没有 DB 级 FK 约束,引用完整性完全靠应用层代码保证。如果代码有 bug 写入了错误的 guest_id,数据库不会报错,产生的孤儿记录会导致 JOIN 查询时数据"消失"。

四、完整的关系机制总览

机制 作用 层级 是否自动
FK 约束 写入时校验引用有效性 数据库层 自动(数据库强制)
JOIN 查询 读取时关联多张表 SQL 查询层 手动(开发者写 SQL)
触发器 数据变动时自动执行逻辑 数据库层 自动(绑定事件触发)
索引 加速查询(尤其是 JOIN 的匹配列) 数据库层 自动(查询优化器选择)
应用层校验 代码中检查数据合法性 应用层 手动(开发者写代码)
ORM 关联 框架自动生成 JOIN 查询 应用层 半自动(配置后框架生成)
理想状态:FK 约束(数据库层保证完整性)+ JOIN 查询(应用层关联数据)+ 索引(加速 JOIN 性能)三者配合使用。Celoria 的核心表目前缺少第一层(FK 约束),依靠应用层代码同时承担完整性校验和数据查询两个职责。