关系模型
问题
什么是关系模型?关系型数据库中的表、行、列、主键、外键分别是什么?
答案
关系模型(Relational Model) 是由 IBM 研究员 Edgar F. Codd 在 1970 年提出的数据模型,是目前最广泛使用的数据库理论基础。关系型数据库(如 MySQL、PostgreSQL、Oracle)都基于关系模型构建。
简单来说,关系模型用二维表格来组织数据——你可以把它理解为一个结构化的 Excel 表格。
核心概念
关系(Relation)= 表(Table)
一个关系就是一张表。例如,存储用户信息的表 users:
| id | name | age | |
|---|---|---|---|
| 1 | 张三 | zhang@example.com | 25 |
| 2 | 李四 | li@example.com | 30 |
| 3 | 王五 | wang@example.com | 28 |
元组(Tuple)= 行(Row)= 记录(Record)
表中的每一行就是一个元组,代表一条具体的数据记录。例如 (1, '张三', 'zhang@example.com', 25) 是一个元组。
属性(Attribute)= 列(Column)= 字段(Field)
表中的每一列就是一个属性,代表数据的一个特征维度。例如 name、email、age 都是属性。
域(Domain)
每个属性的取值范围叫做域。例如:
age的域可以是0~150 的整数email的域是符合邮箱格式的字符串
键(Key)
键是关系模型中最重要的概念之一,用来唯一标识记录和建立表之间的关联。
主键(Primary Key)
主键是表中能唯一标识每一行记录的列(或列的组合)。
规则:
- 每张表只能有一个主键
- 主键的值不能为空(NOT NULL)
- 主键的值不能重复(UNIQUE)
-- 创建表时指定主键
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT, -- id 是主键,自增
name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE
);
- 推荐:使用自增整数
id或 UUID 作为主键(称为"代理键") - 不推荐:使用业务字段(如手机号、身份证号)作为主键——业务规则可能变化
外键(Foreign Key)
外键是一张表中引用另一张表主键的列,用来建立表与表之间的关联。
-- orders 表的 user_id 是外键,引用 users 表的 id
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product VARCHAR(100),
amount DECIMAL(10, 2),
FOREIGN KEY (user_id) REFERENCES users(id)
);
对应关系如图:
外键级联操作
当主表的数据被删除或更新时,外键约束可以自动处理从表的关联数据:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
product VARCHAR(100),
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE -- 删除用户时,自动删除其所有订单
ON UPDATE CASCADE -- 更新用户 id 时,自动更新订单中的 user_id
);
| 级联操作 | 说明 | 适用场景 |
|---|---|---|
CASCADE | 主表删除/更新时,从表跟着删除/更新 | 用户删除 → 删除所有订单 |
SET NULL | 主表删除/更新时,从表外键设为 NULL | 员工离职 → 其负责项目的 manager_id 设空 |
SET DEFAULT | 主表删除/更新时,从表外键设为默认值 | 较少使用 |
RESTRICT | 主表有关联数据时禁止操作(默认行为) | 防止误删有订单的用户 |
NO ACTION | 与 RESTRICT 类似(在 MySQL 中等同) | 标准 SQL 写法 |
-- 实际例子:员工表和部门表
CREATE TABLE departments (
id INT PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
dept_id INT,
-- 删除部门时,员工的 dept_id 设为 NULL(而不是删除员工)
FOREIGN KEY (dept_id) REFERENCES departments(id) ON DELETE SET NULL
);
ON DELETE CASCADE 虽然方便,但可能导致意外的大规模数据删除。例如删除一个分类,可能级联删除该分类下上万条商品记录。生产环境中建议使用软删除(逻辑删除)代替级联删除。
其他键类型
| 键类型 | 说明 | 示例 |
|---|---|---|
| 候选键(Candidate Key) | 能唯一标识记录的最小属性集,表中可以有多个 | id、email(假设 email 也唯一) |
| 超键(Super Key) | 包含候选键的任意属性集合 | {id}、{id, name}、{email, age} |
| 主键(Primary Key) | 从候选键中选出一个作为主键 | 选 id 作为主键 |
| 外键(Foreign Key) | 引用其他表主键的列 | orders.user_id → users.id |
| 复合键(Composite Key) | 由多个列组成的键 | (student_id, course_id) 作为选课表的主键 |
关系之间的联系
表与表之间有三种基本关系:
一对一(1:1)
一个用户对应一条详细信息。通常可以合并为一张表,但出于性能考虑可能拆分。
-- 用户基本信息(高频访问)
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 用户详细信息(低频访问,拆分存储)
CREATE TABLE user_profiles (
user_id INT PRIMARY KEY, -- 同时也是外键
address VARCHAR(200),
bio TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
一对多(1:N)
最常见的关系——一个用户可以有多个订单。
-- 一个用户 → 多个订单
-- 在"多"的一方加外键
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT, -- 外键指向 users
amount DECIMAL(10,2),
FOREIGN KEY (user_id) REFERENCES users(id)
);
多对多(M:N)
一个学生可以选多门课,一门课可以有多个学生。需要通过中间表(关联表) 来实现:
-- 学生表
CREATE TABLE students (
id INT PRIMARY KEY,
name VARCHAR(50)
);
-- 课程表
CREATE TABLE courses (
id INT PRIMARY KEY,
title VARCHAR(100)
);
-- 中间表:选课记录
CREATE TABLE enrollments (
student_id INT,
course_id INT,
enrolled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id), -- 复合主键
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
关系代数(了解)
关系代数是关系模型的理论基础,定义了对关系(表)的基本操作。SQL 语句本质上就是关系代数的实现:
| 关系代数操作 | 对应 SQL | 说明 |
|---|---|---|
| 选择(σ) | WHERE | 过滤满足条件的行 |
| 投影(π) | SELECT 列名 | 选择需要的列 |
| 并(∪) | UNION | 合并两个结果集 |
| 差(−) | EXCEPT | 在 A 中但不在 B 中 |
| 笛卡尔积(×) | CROSS JOIN | 两表所有行的组合 |
| 连接(⋈) | JOIN | 按条件合并两表 |
| 除(÷) | 需要组合实现 | 满足所有关联条件的行 |
| 重命名(ρ) | AS 别名 | 给表或列起别名 |
关系代数到 SQL 的映射示例
-- 关系代数:π(name, email) σ(age > 25)(users)
-- 含义:从 users 表中选择 age > 25 的记录,只取 name 和 email 列
SELECT name, email -- 投影(π)
FROM users
WHERE age > 25; -- 选择(σ)
-- 关系代数:users ⋈(users.id = orders.user_id) orders
-- 含义:将 users 和 orders 按 id 连接
SELECT *
FROM users
JOIN orders ON users.id = orders.user_id; -- 连接(⋈)
数据库 Schema
Schema(模式) 是数据库的逻辑结构定义,它描述了数据库中有哪些表、每张表有哪些列、列的类型是什么等。
三层模式结构
| 层次 | 说明 | 对应 |
|---|---|---|
| 外模式(External Schema) | 不同用户看到的数据视图 | SQL VIEW |
| 概念模式(Conceptual Schema) | 数据库的逻辑结构(表、列、约束) | CREATE TABLE |
| 内模式(Internal Schema) | 数据在磁盘上的物理存储方式 | 存储引擎(InnoDB) |
这种三层架构的好处是数据独立性:修改物理存储方式不影响逻辑结构,修改逻辑结构不影响用户视图。
Schema 在不同数据库中的含义
-- MySQL 中 schema ≈ database
CREATE DATABASE myapp;
USE myapp;
-- PostgreSQL 中 schema 是 database 下的命名空间
CREATE SCHEMA sales;
CREATE TABLE sales.orders (...); -- 在 sales 模式下创建表
视图(View)
视图是基于 SQL 查询结果的虚拟表。它不存储数据,每次查询时动态生成。
-- 创建视图:活跃用户(最近30天有登录的用户)
CREATE VIEW active_users AS
SELECT u.id, u.name, u.email, MAX(l.login_time) AS last_login
FROM users u
JOIN login_logs l ON u.id = l.user_id
WHERE l.login_time > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY u.id, u.name, u.email;
-- 使用视图,就像查询普通表一样
SELECT * FROM active_users WHERE name LIKE '张%';
视图的用途:
- 简化复杂查询:把常用的复杂 JOIN 封装为视图
- 权限控制:只让用户看到部分列(如隐藏薪资字段)
- 数据抽象:底层表结构变化时,通过修改视图保持接口不变
NULL 在关系模型中的处理
NULL 表示"未知"或"不适用",它在关系模型中有特殊的行为,是面试和实际开发中的常见坑点。
NULL 的三值逻辑
普通布尔逻辑只有 TRUE 和 FALSE,但 NULL 引入了第三种值——UNKNOWN:
| 表达式 | 结果 |
|---|---|
NULL = NULL | UNKNOWN(不是 TRUE!) |
NULL != NULL | UNKNOWN |
NULL > 5 | UNKNOWN |
NULL AND TRUE | UNKNOWN |
NULL OR TRUE | TRUE |
NULL AND FALSE | FALSE |
NOT NULL | UNKNOWN |
-- ❌ 错误写法:WHERE 不会返回 UNKNOWN 的行
SELECT * FROM users WHERE age = NULL; -- 永远返回空结果!
-- ✅ 正确写法:使用 IS NULL / IS NOT NULL
SELECT * FROM users WHERE age IS NULL;
SELECT * FROM users WHERE age IS NOT NULL;
NULL 对聚合函数的影响
-- 数据:scores = [90, NULL, 80, NULL, 70]
SELECT COUNT(*) FROM t; -- 5(计算所有行,包括 NULL)
SELECT COUNT(score) FROM t; -- 3(只计算非 NULL 值)
SELECT SUM(score) FROM t; -- 240(忽略 NULL)
SELECT AVG(score) FROM t; -- 80(= 240 / 3,不是 240 / 5)
NOT IN遇到 NULL 会返回空结果:WHERE id NOT IN (1, 2, NULL)→ 空!DISTINCT会把多个 NULL 视为相同值ORDER BY中 NULL 的排序位置各数据库不同
实战:电商数据库设计示例
把上面学到的概念串起来,设计一个简单的电商数据库:
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
phone VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 商品分类表(自引用——树形结构)
CREATE TABLE categories (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
parent_id INT, -- 自引用外键,指向父分类
FOREIGN KEY (parent_id) REFERENCES categories(id) ON DELETE SET NULL
);
-- 商品表
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL CHECK (price >= 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
category_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
);
-- 订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
total_amount DECIMAL(12, 2) NOT NULL,
status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 订单明细表(多对多:订单 ↔ 商品)
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10, 2) NOT NULL, -- 下单时的价格快照
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id)
);
常见面试问题
Q1: 什么是关系模型?它的核心特点是什么?
答案:
关系模型是用二维表(关系) 来组织和管理数据的模型。核心特点:
- 数据以表格形式存储:每张表有固定的列(属性)和不定数量的行(记录)
- 行无序:行的存储顺序不影响查询结果
- 列有类型:每列有固定的数据类型(如 INT、VARCHAR)
- 通过键来关联:表与表之间通过主键-外键建立联系
- 操作基于关系代数:提供 SELECT、JOIN 等操作
Q2: 主键和唯一键有什么区别?
答案:
| 对比项 | 主键(PRIMARY KEY) | 唯一键(UNIQUE) |
|---|---|---|
| 数量 | 每表只能有 1 个 | 可以有多个 |
| NULL | 不允许 NULL | 允许 NULL(通常只允许一个 NULL) |
| 索引 | 自动创建聚簇索引 | 自动创建非聚簇索引 |
| 用途 | 唯一标识行的主要方式 | 保证列值不重复 |
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键:不能为空,不能重复
email VARCHAR(100) UNIQUE, -- 唯一键:不能重复,可以为空
phone VARCHAR(20) UNIQUE -- 另一个唯一键
);
Q3: 什么是外键?使用外键有什么优缺点?
答案:
外键是一个表中的列,它引用另一个表的主键,用来维护表之间的参照完整性。
优点:
- 自动保证数据一致性(不能插入不存在的引用值)
- 支持级联操作(删除用户时自动删除其订单)
- 代码层面更清晰的关系表达
缺点:
- 影响写入性能:每次 INSERT/UPDATE 都需要验证外键约束
- 增加锁竞争:外键检查需要对被引用的表加锁
- 不利于分库分表:跨库的外键约束无法实现
很多互联网公司在生产环境中不使用数据库外键约束,而是在应用层(代码层面)来保证数据一致性。原因是大规模系统中外键的性能和运维成本过高。但在面试中你要清楚外键的概念和作用。
Q4: 什么是复合主键?什么时候使用?
答案:
复合主键是由多个列组合而成的主键。当单个列无法唯一标识一行记录时使用。
典型场景是多对多关系的中间表:
-- 选课表:一个学生 + 一门课 = 唯一的选课记录
CREATE TABLE enrollments (
student_id INT,
course_id INT,
grade DECIMAL(3,1),
PRIMARY KEY (student_id, course_id) -- 复合主键
);
student_id 单独不唯一(一个学生选多门课),course_id 单独也不唯一(一门课有多个学生),但组合起来就是唯一的。
Q5: 一对一、一对多、多对多关系分别怎么实现?
答案:
| 关系类型 | 实现方式 | 示例 |
|---|---|---|
| 一对一 | 在任一方加外键(通常在从表上加),或合并为一张表 | 用户 ↔ 用户详情 |
| 一对多 | 在"多"的一方添加外键,指向"一"的主键 | 用户 → 订单 |
| 多对多 | 创建中间表(关联表),两个外键分别指向两张主表 | 学生 ↔ 课程 |
Q6: 自然键和代理键有什么区别?推荐用哪种?
答案:
- 自然键(Natural Key):使用有业务含义的字段作为主键,如身份证号、手机号
- 代理键(Surrogate Key):使用无业务意义的自增 ID 或 UUID 作为主键
| 对比 | 自然键 | 代理键 |
|---|---|---|
| 含义 | 有业务意义 | 无业务意义 |
| 稳定性 | 可能因业务规则变化而修改 | 稳定不变 |
| 存储 | 可能较长(如身份证 18 位) | 通常较短(INT 4 字节) |
| JOIN 性能 | 可能更慢(字符串比较) | 更快(整数比较) |
推荐使用代理键:自增 INT(单机)或 BIGINT(高并发)。UUID 适合分布式场景,但存储占用更大且对索引不友好。
Q7: NULL 和空字符串有什么区别?
答案:
| 对比 | NULL | 空字符串 '' |
|---|---|---|
| 含义 | 未知、不存在 | 已知,但值为空 |
| 存储 | 不占用实际数据空间(只有标志位) | 占用 0~1 字节 |
| 比较 | NULL = NULL → UNKNOWN | '' = '' → TRUE |
| 判断 | IS NULL / IS NOT NULL | = '' / != '' |
| 聚合 | COUNT(col) 不计入 | COUNT(col) 计入 |
-- 实际开发建议:尽量避免 NULL,给字段设默认值
ALTER TABLE users MODIFY phone VARCHAR(20) NOT NULL DEFAULT '';
Q8: 什么是数据库视图?视图和表有什么区别?
答案:
视图是基于 SQL 查询定义的虚拟表,不实际存储数据。
| 对比 | 表(Table) | 视图(View) |
|---|---|---|
| 数据存储 | 真实存储在磁盘上 | 不存储数据,每次查询时动态执行 |
| 增删改 | 完全支持 | 简单视图可以,复杂视图不行 |
| 性能 | 直接读取 | 每次需要执行底层 SQL |
| 索引 | 可以创建索引 | 一般不能(物化视图除外) |
| 用途 | 存储业务数据 | 简化查询、权限控制、数据抽象 |
Q9: 什么是自引用外键?给一个例子
答案:
自引用外键是指表中的某一列引用同一张表的主键,用来表示树形/层级结构:
-- 部门表:每个部门有一个上级部门
CREATE TABLE departments (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
parent_id INT, -- 自引用:指向同一张表的 id
FOREIGN KEY (parent_id) REFERENCES departments(id)
);
INSERT INTO departments VALUES (1, '总公司', NULL); -- 根节点
INSERT INTO departments VALUES (2, '技术部', 1); -- 一级部门
INSERT INTO departments VALUES (3, '前端组', 2); -- 二级部门
INSERT INTO departments VALUES (4, '后端组', 2); -- 二级部门
常见场景:部门层级、商品分类、评论回复、地区树(省-市-区)等。
Q10: 级联删除(ON DELETE CASCADE)有什么风险?
答案:
主要风险:
- 意外的大规模删除:删除一条父记录可能级联删除数十万条子记录
- 难以追踪:应用层日志可能无法感知数据库级联删除的操作
- 性能问题:级联删除大量数据时会产生大量 redo log,可能阻塞其他操作
生产环境推荐做法:
- 使用软删除(逻辑删除):添加
deleted_at或is_deleted字段 - 需要物理删除时,在应用层显式删除,可以加事务、写日志
- 用
RESTRICT代替CASCADE,防止误操作
Q11: 为什么很多互联网公司不用外键约束?
答案:
- 性能影响:每次 INSERT/UPDATE/DELETE 都要检查外键约束,高并发下是瓶颈
- 锁竞争:外键检查需要对被引用的表加共享锁,影响写入并发
- 分库分表困难:跨数据库实例的外键约束无法实现
- 运维不便:DDL 操作(如加字段)需要考虑外键依赖关系
替代方案:在应用层通过代码逻辑保证数据一致性,并用定时任务或数据校验脚本发现不一致数据。
Q12: 关系模型和文档模型有什么本质区别?
答案:
| 对比 | 关系模型 | 文档模型(如 MongoDB) |
|---|---|---|
| 数据结构 | 二维表格(行+列) | JSON/BSON 文档(支持嵌套) |
| Schema | 固定 Schema(写入前定义) | 灵活 Schema(写入时决定) |
| 关联 | 通过 JOIN 连接多表 | 通过嵌套或引用(无 JOIN) |
| 事务 | 完善的 ACID 事务 | 单文档原子性(多文档事务需 4.0+) |
| 扩展 | 垂直扩展为主 | 水平扩展(分片)更方便 |
| 适合场景 | 结构化数据、强一致性 | 半结构化数据、快速迭代 |
-- 关系模型:用户和地址分开存
-- users: {id, name, email}
-- addresses: {id, user_id, city, street}
-- 文档模型:嵌套在一起
-- { name: "张三", email: "...", addresses: [{city: "北京", street: "..."}] }