Skip to content

第 10 章:数据库设计与范式

良好的数据库设计是构建高性能、可维护应用的基础。在本章中,我们将学习数据库设计的基本原则、实体关系模型以及规范化理论,帮助你设计出结构合理、性能优良的数据库。

本章内容概要

  • 数据库设计的基本原则
  • 实体关系模型(ER 模型)
  • 数据库设计步骤
  • 第一范式(1NF)
  • 第二范式(2NF)
  • 第三范式(3NF)
  • 反范式化设计
  • 实战练习:电商系统数据库设计

数据库设计的基本原则

优秀的数据库设计应该遵循以下基本原则:

1. 完整性原则

确保数据的准确性和一致性:

  • 实体完整性 - 主键约束
  • 参照完整性 - 外键约束
  • 域完整性 - 数据类型和检查约束

2. 一致性原则

数据库设计应该与业务逻辑保持一致:

  • 表结构反映现实世界的实体和关系
  • 字段命名清晰、有意义
  • 数据类型选择合适

3. 可扩展性原则

设计应该考虑未来的扩展需求:

  • 预留扩展字段
  • 合理的表结构设计
  • 避免过度复杂的设计

4. 性能原则

在满足功能需求的前提下优化性能:

  • 合理使用索引
  • 避免冗余数据
  • 适当的数据分区

实体关系模型(ER 模型)

实体关系模型是数据库设计的重要工具,用于描述现实世界中实体及其相互关系。

基本概念

实体(Entity)

现实世界中可以区分的对象或概念:

  • 学生、课程、教师
  • 订单、商品、客户
  • 部门、员工、项目

属性(Attribute)

实体的特征或性质:

  • 学生:学号、姓名、年龄、专业
  • 订单:订单号、下单时间、总金额
  • 商品:商品编号、名称、价格、库存

关系(Relationship)

实体之间的联系:

  • 一个学生可以选修多门课程(一对多)
  • 一个订单包含多个商品(一对多)
  • 一个学生可以有多个联系电话(一对多)

ER 图表示法

实体表示

[学生]
+--------+
| 学号   |
| 姓名   |
| 年龄   |
| 专业   |
+--------+

关系表示

[学生] ----<选修>---- [课程]

基数表示

  • 一对一:1:1
  • 一对多:1:N
  • 多对多:M:N

数据库设计步骤

1. 需求分析

明确系统需要存储和管理的数据:

  • 业务流程分析
  • 数据流图绘制
  • 数据字典建立

2. 概念设计

使用 ER 模型描述数据结构:

  • 确定实体和属性
  • 确定实体间的关系
  • 绘制 ER 图

3. 逻辑设计

将概念模型转换为数据库模式:

  • ER 图转换为关系模式
  • 确定主键和外键
  • 定义完整性约束

4. 物理设计

确定数据的物理存储结构:

  • 选择存储引擎
  • 设计索引策略
  • 考虑分区和分表

5. 实施与维护

创建数据库并持续优化:

  • 创建表结构
  • 导入初始数据
  • 性能调优和维护

范式理论

范式是数据库设计的规范化标准,用于消除数据冗余和异常。

第一范式(1NF)

第一范式要求表中的每个字段都是不可分割的原子值。

不符合 1NF 的设计

sql
-- 错误示例:联系方式字段包含多个值
CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    contact VARCHAR(100)  -- 包含"电话:138****,邮箱:****@***.com"
);

-- 查询困难,无法单独查询电话或邮箱

符合 1NF 的设计

sql
-- 正确示例:每个字段都是原子值
CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    phone VARCHAR(20),
    email VARCHAR(100)
);

-- 每个字段都可以独立查询和更新

第二范式(2NF)

第二范式要求表满足 1NF,并且所有非主键字段都完全依赖于主键。

不符合 2NF 的设计

sql
-- 错误示例:存在部分依赖
CREATE TABLE order_items (
    order_id INT,
    product_id INT,
    order_date DATE,
    customer_name VARCHAR(50),
    product_name VARCHAR(100),
    quantity INT,
    price DECIMAL(10,2),
    PRIMARY KEY (order_id, product_id)
);

-- 问题:
-- order_date, customer_name 只依赖于 order_id(部分依赖)
-- product_name 只依赖于 product_id(部分依赖)

符合 2NF 的设计

sql
-- 正确示例:分解为多个表
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    order_date DATE,
    customer_name VARCHAR(50)
);

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100),
    price DECIMAL(10,2)
);

CREATE TABLE order_items (
    order_id INT,
    product_id INT,
    quantity INT,
    PRIMARY KEY (order_id, product_id),
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

第三范式(3NF)

第三范式要求表满足 2NF,并且所有非主键字段都不传递依赖于主键。

不符合 3NF 的设计

sql
-- 错误示例:存在传递依赖
CREATE TABLE students (
    student_id INT PRIMARY KEY,
    name VARCHAR(50),
    major_id INT,
    major_name VARCHAR(50),  -- 依赖于 major_id,而非 student_id
    department VARCHAR(50)    -- 依赖于 major_id,而非 student_id
);

-- 传递依赖:student_id → major_id → (major_name, department)

符合 3NF 的设计

sql
-- 正确示例:消除传递依赖
CREATE TABLE majors (
    major_id INT PRIMARY KEY,
    major_name VARCHAR(50),
    department VARCHAR(50)
);

CREATE TABLE students (
    student_id INT PRIMARY KEY,
    name VARCHAR(50),
    major_id INT,
    FOREIGN KEY (major_id) REFERENCES majors(major_id)
);

高级范式

巴科斯-科德范式(BCNF)

BCNF 是 3NF 的加强版,要求所有函数依赖的决定因素都是候选键。

第四范式(4NF)

4NF 处理多值依赖问题,消除非平凡的多值依赖。

第五范式(5NF)

5NF 处理连接依赖问题,确保表不能进一步无损分解。

反范式化设计

在某些情况下,为了提高查询性能,可以适当违反范式原则,进行反范式化设计。

1. 增加冗余字段

sql
-- 订单表中冗余存储客户姓名,避免连接查询
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    customer_name VARCHAR(50),  -- 冗余字段
    order_date DATE,
    total_amount DECIMAL(10,2)
);

2. 预计算字段

sql
-- 订单表中预计算总金额,避免实时计算
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    item_count INT,      -- 预计算字段
    total_amount DECIMAL(10,2)  -- 预计算字段
);

3. 历史数据快照

sql
-- 订单表中保存下单时的商品信息快照
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    product_id INT,
    product_name VARCHAR(100),  -- 快照字段
    product_price DECIMAL(10,2) -- 快照字段
);

实战练习:电商系统数据库设计

让我们通过设计一个电商系统的数据库来实践所学知识。

1. 需求分析

电商系统需要管理:

  • 用户信息
  • 商品信息
  • 订单信息
  • 购物车
  • 支付信息
  • 库存管理

2. 概念设计(ER 图)

[用户] ----<下单>---- [订单] ----<包含>---- [订单项]
  |                     |
  |                     |
[购物车]              [支付]
  |
  |
[购物车项] ----<关联>---- [商品] ----<属于>---- [商品分类]

3. 逻辑设计

sql
-- 创建电商数据库
CREATE DATABASE ecommerce_system 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

USE ecommerce_system;

-- 用户表
CREATE TABLE users (
    user_id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    phone VARCHAR(20),
    real_name VARCHAR(50),
    gender ENUM('男', '女', '未知') DEFAULT '未知',
    birthday DATE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    status ENUM('active', 'inactive', 'banned') DEFAULT 'active'
);

-- 用户地址表
CREATE TABLE user_addresses (
    address_id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    receiver_name VARCHAR(50) NOT NULL,
    phone VARCHAR(20) NOT NULL,
    province VARCHAR(50) NOT NULL,
    city VARCHAR(50) NOT NULL,
    district VARCHAR(50),
    detail_address VARCHAR(200) NOT NULL,
    is_default BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

-- 商品分类表
CREATE TABLE categories (
    category_id INT AUTO_INCREMENT PRIMARY KEY,
    category_name VARCHAR(50) NOT NULL,
    parent_id INT DEFAULT NULL,
    sort_order INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (parent_id) REFERENCES categories(category_id)
);

-- 品牌表
CREATE TABLE brands (
    brand_id INT AUTO_INCREMENT PRIMARY KEY,
    brand_name VARCHAR(50) NOT NULL,
    logo_url VARCHAR(200),
    description TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 商品表
CREATE TABLE products (
    product_id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(100) NOT NULL,
    brand_id INT,
    category_id INT NOT NULL,
    product_code VARCHAR(50) UNIQUE,
    description TEXT,
    price DECIMAL(10,2) NOT NULL,
    stock_quantity INT NOT NULL DEFAULT 0,
    sales_count INT NOT NULL DEFAULT 0,
    status ENUM('on_sale', 'off_sale', 'discontinued') DEFAULT 'on_sale',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (brand_id) REFERENCES brands(brand_id),
    FOREIGN KEY (category_id) REFERENCES categories(category_id)
);

-- 商品规格表
CREATE TABLE product_specs (
    spec_id INT AUTO_INCREMENT PRIMARY KEY,
    product_id INT NOT NULL,
    spec_name VARCHAR(50) NOT NULL,
    spec_value VARCHAR(100) NOT NULL,
    price_adjustment DECIMAL(10,2) DEFAULT 0,
    stock_quantity INT NOT NULL DEFAULT 0,
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

-- 购物车表
CREATE TABLE shopping_carts (
    cart_id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

-- 购物车项表
CREATE TABLE cart_items (
    cart_item_id INT AUTO_INCREMENT PRIMARY KEY,
    cart_id INT NOT NULL,
    product_id INT NOT NULL,
    spec_id INT,
    quantity INT NOT NULL DEFAULT 1,
    added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (cart_id) REFERENCES shopping_carts(cart_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id),
    FOREIGN KEY (spec_id) REFERENCES product_specs(spec_id)
);

-- 订单表
CREATE TABLE orders (
    order_id INT AUTO_INCREMENT PRIMARY KEY,
    order_number VARCHAR(50) NOT NULL UNIQUE,
    user_id INT NOT NULL,
    address_id INT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    discount_amount DECIMAL(10,2) DEFAULT 0,
    shipping_fee DECIMAL(10,2) DEFAULT 0,
    final_amount DECIMAL(10,2) NOT NULL,
    order_status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
    payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid',
    payment_method ENUM('alipay', 'wechat', 'bank_card') DEFAULT 'alipay',
    shipping_company VARCHAR(50),
    tracking_number VARCHAR(100),
    remark TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (address_id) REFERENCES user_addresses(address_id)
);

-- 订单项表
CREATE TABLE order_items (
    order_item_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    product_id INT NOT NULL,
    spec_id INT,
    product_name VARCHAR(100) NOT NULL,  -- 快照字段
    product_price DECIMAL(10,2) NOT NULL, -- 快照字段
    quantity INT NOT NULL,
    subtotal DECIMAL(10,2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(order_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id),
    FOREIGN KEY (spec_id) REFERENCES product_specs(spec_id)
);

-- 支付记录表
CREATE TABLE payments (
    payment_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT NOT NULL,
    payment_number VARCHAR(50) NOT NULL UNIQUE,
    payment_method ENUM('alipay', 'wechat', 'bank_card') NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    payment_status ENUM('pending', 'success', 'failed', 'refunded') DEFAULT 'pending',
    transaction_id VARCHAR(100),
    paid_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

4. 创建索引

sql
-- 为常用查询字段创建索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_brand ON products(brand_id);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(order_status);
CREATE INDEX idx_orders_created ON orders(created_at);
CREATE INDEX idx_order_items_order ON order_items(order_id);

5. 插入测试数据

sql
-- 插入测试数据
INSERT INTO users (username, email, password_hash, phone, real_name) VALUES
('zhangsan', 'zhangsan@example.com', 'hashed_password_1', '13800138001', '张三'),
('lisi', 'lisi@example.com', 'hashed_password_2', '13800138002', '李四');

INSERT INTO categories (category_name, parent_id) VALUES
('电子产品', NULL),
('手机', 1),
('电脑', 1),
('服装', NULL),
('男装', 4),
('女装', 4);

INSERT INTO brands (brand_name, description) VALUES
('苹果', 'Apple品牌'),
('华为', '华为品牌'),
('小米', '小米品牌');

INSERT INTO products (product_name, brand_id, category_id, product_code, price, stock_quantity) VALUES
('iPhone 15', 1, 2, 'IP15-001', 5999.00, 100),
('MacBook Pro', 1, 3, 'MBP-001', 12999.00, 50),
('华为P50', 2, 2, 'HP50-001', 4999.00, 80);

INSERT INTO user_addresses (user_id, receiver_name, phone, province, city, detail_address, is_default) VALUES
(1, '张三', '13800138001', '北京市', '北京市', '朝阳区某某街道123号', TRUE);

6. 常用查询示例

sql
-- 查询用户订单及订单项
SELECT 
    o.order_number,
    o.final_amount,
    o.order_status,
    oi.product_name,
    oi.product_price,
    oi.quantity
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.user_id = 1
ORDER BY o.created_at DESC;

-- 查询商品及其分类和品牌信息
SELECT 
    p.product_name,
    p.price,
    b.brand_name,
    c.category_name
FROM products p
JOIN brands b ON p.brand_id = b.brand_id
JOIN categories c ON p.category_id = c.category_id
WHERE p.status = 'on_sale'
ORDER BY p.sales_count DESC;

-- 查询用户购物车信息
SELECT 
    p.product_name,
    ps.spec_value,
    ci.quantity,
    p.price + IFNULL(ps.price_adjustment, 0) AS final_price
FROM shopping_carts sc
JOIN cart_items ci ON sc.cart_id = ci.cart_id
JOIN products p ON ci.product_id = p.product_id
LEFT JOIN product_specs ps ON ci.spec_id = ps.spec_id
WHERE sc.user_id = 1;

数据库设计最佳实践

1. 命名规范

sql
-- 表名使用复数形式
CREATE TABLE users (...);
CREATE TABLE orders (...);

-- 字段名使用下划线分隔
user_id, first_name, created_at

-- 主键统一命名为 id 或 table_name_id
PRIMARY KEY (user_id)

2. 数据类型选择

sql
-- 使用合适的数据类型
TINYINT  -- 0-255
SMALLINT -- 0-65535
INT      -- 大整数
DECIMAL  -- 精确小数
VARCHAR  -- 可变长度字符串
TEXT     -- 长文本
DATE     -- 日期
TIMESTAMP -- 时间戳

3. 约束和索引

sql
-- 合理使用约束
NOT NULL, UNIQUE, PRIMARY KEY, FOREIGN KEY, CHECK

-- 为常用查询字段创建索引
CREATE INDEX idx_users_email ON users(email);

小结

在本章中,我们学习了数据库设计与范式的重要内容:

  1. 设计原则 - 完整性、一致性、可扩展性、性能原则
  2. ER 模型 - 实体、属性、关系的概念和表示
  3. 设计步骤 - 需求分析、概念设计、逻辑设计、物理设计
  4. 范式理论 - 1NF、2NF、3NF 及其应用
  5. 反范式化 - 为了性能适当违反范式原则
  6. 实战练习 - 电商系统数据库设计

良好的数据库设计是构建高质量应用的基础。在下一章中,我们将学习 MySQL 的高级特性,包括分区表、JSON 数据类型等现代数据库功能。