跳转至

数据库设计基础:从零开始学习关系型数据库设计

概述

数据库设计是后端开发的核心技能之一。无论是Web应用、移动应用还是物联网系统,都需要通过数据库来存储和管理数据。良好的数据库设计能够提高系统性能、保证数据完整性、简化开发维护。

完成本文学习后,你将能够:

  • 理解关系型数据库的基本概念和设计原则
  • 掌握表结构设计和字段类型选择
  • 理解主键、外键和索引的作用
  • 学会编写基本的SQL查询语句
  • 了解数据库性能优化的基本方法
  • 设计符合规范的数据库结构

背景知识

什么是数据库?

数据库(Database)是按照数据结构来组织、存储和管理数据的仓库。它是一个长期存储在计算机内、有组织的、可共享的数据集合。

数据库的特点: - 持久化存储:数据保存在磁盘上,程序关闭后数据不会丢失 - 结构化管理:数据按照特定的结构组织 - 高效访问:通过索引等机制快速查询数据 - 并发控制:支持多个用户同时访问 - 数据完整性:通过约束保证数据的正确性

关系型数据库 vs 非关系型数据库

特性 关系型数据库 非关系型数据库
数据模型 表格(行和列) 键值对、文档、图等
数据结构 固定的表结构 灵活的数据结构
查询语言 SQL 各自的查询API
事务支持 强事务支持(ACID) 部分支持或不支持
扩展性 垂直扩展为主 水平扩展容易
适用场景 结构化数据、复杂查询 大数据、高并发、灵活数据
典型产品 MySQL、PostgreSQL、Oracle MongoDB、Redis、Cassandra

本文主要讲解关系型数据库的设计。

常见的关系型数据库

MySQL: - 最流行的开源数据库 - 性能好,易于使用 - 适合Web应用和中小型系统

PostgreSQL: - 功能强大的开源数据库 - 支持复杂查询和高级特性 - 适合企业级应用

SQLite: - 轻量级嵌入式数据库 - 无需服务器,直接读写文件 - 适合移动应用和嵌入式系统

Oracle: - 企业级商业数据库 - 功能最全面,性能最强 - 适合大型企业应用

SQL Server: - 微软的商业数据库 - 与Windows生态集成好 - 适合.NET应用

核心概念

数据库的基本组成

1. 数据库(Database)

数据库是表、视图、存储过程等对象的集合。一个数据库系统可以包含多个数据库。

-- 创建数据库
CREATE DATABASE blog_system;

-- 使用数据库
USE blog_system;

-- 删除数据库
DROP DATABASE blog_system;

2. 表(Table)

表是数据库中存储数据的基本单位,由行和列组成。

表的组成: - 列(Column):也称为字段(Field),定义数据的类型和属性 - 行(Row):也称为记录(Record),表示一条完整的数据

示例:用户表(users)

id username email created_at
1 zhangsan zhang@example.com 2024-01-01
2 lisi li@example.com 2024-01-02
3 wangwu wang@example.com 2024-01-03

3. 字段(Field)

字段定义了数据的类型、长度和约束。

常用数据类型

数值类型: - INT:整数,范围 -2147483648 到 2147483647 - BIGINT:大整数,范围更大 - DECIMAL(M,D):精确小数,M是总位数,D是小数位数 - FLOAT:单精度浮点数 - DOUBLE:双精度浮点数

字符串类型: - CHAR(N):固定长度字符串,最大255字符 - VARCHAR(N):可变长度字符串,最大65535字符 - TEXT:长文本,最大65535字符 - LONGTEXT:超长文本,最大4GB

日期时间类型: - DATE:日期,格式 YYYY-MM-DD - TIME:时间,格式 HH:MM:SS - DATETIME:日期时间,格式 YYYY-MM-DD HH:MM:SS - TIMESTAMP:时间戳,自动更新

其他类型: - BOOLEAN:布尔值,实际存储为 TINYINT(1) - ENUM:枚举类型,从预定义的值中选择 - JSON:JSON格式数据(MySQL 5.7+)

4. 约束(Constraint)

约束用于限制表中数据的规则,保证数据的完整性。

主键约束(PRIMARY KEY): - 唯一标识表中的每一行 - 不能为NULL - 一个表只能有一个主键

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50)
);

唯一约束(UNIQUE): - 保证字段值唯一 - 可以为NULL - 一个表可以有多个唯一约束

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    email VARCHAR(100) UNIQUE
);

非空约束(NOT NULL): - 字段值不能为NULL

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL
);

默认值约束(DEFAULT): - 为字段设置默认值

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    status VARCHAR(20) DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

检查约束(CHECK): - 限制字段值的范围

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10,2) CHECK (price > 0),
    stock INT CHECK (stock >= 0)
);

外键约束(FOREIGN KEY): - 建立表之间的关联关系 - 保证引用完整性

CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

数据库设计原则

三大范式

数据库设计遵循范式(Normal Form)可以减少数据冗余,提高数据一致性。

第一范式(1NF):原子性

每个字段都是不可再分的原子值。

❌ 违反第一范式

用户表
| id | name | contact |
|----|------|---------|
| 1  | 张三 | 电话:13800138000,邮箱:zhang@example.com |

✅ 符合第一范式

用户表
| id | name | phone | email |
|----|------|-------|-------|
| 1  | 张三 | 13800138000 | zhang@example.com |

第二范式(2NF):完全依赖

在满足1NF的基础上,非主键字段完全依赖于主键,不能只依赖主键的一部分。

❌ 违反第二范式

订单明细表(主键:订单ID + 商品ID)
| 订单ID | 商品ID | 商品名称 | 数量 | 单价 |
|--------|--------|----------|------|------|
| 1      | 101    | 键盘     | 2    | 100  |
商品名称只依赖于商品ID,不依赖订单ID。

✅ 符合第二范式

订单明细表
| 订单ID | 商品ID | 数量 |
|--------|--------|------|
| 1      | 101    | 2    |

商品表
| 商品ID | 商品名称 | 单价 |
|--------|----------|------|
| 101    | 键盘     | 100  |

第三范式(3NF):消除传递依赖

在满足2NF的基础上,非主键字段之间不能有依赖关系。

❌ 违反第三范式

订单表
| 订单ID | 用户ID | 用户名 | 用户地址 |
|--------|--------|--------|----------|
| 1      | 1001   | 张三   | 北京市   |
用户名和用户地址依赖于用户ID,而不是直接依赖订单ID。

✅ 符合第三范式

订单表
| 订单ID | 用户ID |
|--------|--------|
| 1      | 1001   |

用户表
| 用户ID | 用户名 | 用户地址 |
|--------|--------|----------|
| 1001   | 张三   | 北京市   |

反范式化

在某些情况下,为了提高查询性能,可以适当违反范式,增加数据冗余。

使用场景: - 频繁查询的统计数据 - 需要避免多表关联的场景 - 读多写少的场景

示例

-- 在订单表中冗余用户名,避免每次都关联用户表
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    user_name VARCHAR(50),  -- 冗余字段
    total_amount DECIMAL(10,2)
);

命名规范

表名: - 使用小写字母和下划线 - 使用复数形式 - 见名知意

✅ users, products, order_items
❌ User, product, OrderItem

字段名: - 使用小写字母和下划线 - 避免使用保留字 - 布尔字段使用 is_ 前缀

✅ user_id, created_at, is_active
❌ userId, CreateDate, active

索引名: - 普通索引:idx_表名_字段名 - 唯一索引:uk_表名_字段名 - 外键索引:fk_表名_字段名

✅ idx_users_email, uk_users_username, fk_orders_user_id

实践示例:设计博客系统数据库

让我们通过一个实际案例来学习数据库设计。我们将设计一个简单的博客系统,包含用户、文章、评论和标签功能。

需求分析

功能需求: 1. 用户可以注册、登录 2. 用户可以发布文章 3. 用户可以对文章发表评论 4. 文章可以添加标签 5. 用户可以点赞文章

实体识别

根据需求,我们识别出以下实体: - 用户(User) - 文章(Article) - 评论(Comment) - 标签(Tag)

关系分析

实体之间的关系: - 用户 - 文章:一对多(一个用户可以发布多篇文章) - 文章 - 评论:一对多(一篇文章可以有多条评论) - 用户 - 评论:一对多(一个用户可以发表多条评论) - 文章 - 标签:多对多(一篇文章可以有多个标签,一个标签可以属于多篇文章) - 用户 - 文章(点赞):多对多(一个用户可以点赞多篇文章,一篇文章可以被多个用户点赞)

表结构设计

1. 用户表(users)

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
    email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
    password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',
    avatar VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
    bio TEXT DEFAULT NULL COMMENT '个人简介',
    status ENUM('active', 'inactive', 'banned') DEFAULT 'active' COMMENT '状态',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    INDEX idx_username (username),
    INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

字段说明: - id:主键,自增 - username:用户名,唯一 - email:邮箱,唯一 - password_hash:密码哈希值(不存储明文密码) - avatar:头像URL - bio:个人简介 - status:用户状态(活跃、未激活、封禁) - created_at:创建时间,自动设置 - updated_at:更新时间,自动更新

2. 文章表(articles)

CREATE TABLE articles (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '文章ID',
    user_id INT NOT NULL COMMENT '作者ID',
    title VARCHAR(200) NOT NULL COMMENT '标题',
    content TEXT NOT NULL COMMENT '内容',
    summary VARCHAR(500) DEFAULT NULL COMMENT '摘要',
    cover_image VARCHAR(255) DEFAULT NULL COMMENT '封面图',
    status ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态',
    view_count INT DEFAULT 0 COMMENT '浏览量',
    like_count INT DEFAULT 0 COMMENT '点赞数',
    comment_count INT DEFAULT 0 COMMENT '评论数',
    published_at TIMESTAMP NULL DEFAULT NULL COMMENT '发布时间',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_published_at (published_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章表';

字段说明: - user_id:外键,关联用户表 - title:文章标题 - content:文章内容 - summary:文章摘要 - status:文章状态(草稿、已发布、已归档) - view_count:浏览量(冗余字段,提高查询性能) - like_count:点赞数(冗余字段) - comment_count:评论数(冗余字段) - published_at:发布时间

3. 评论表(comments)

CREATE TABLE comments (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '评论ID',
    article_id INT NOT NULL COMMENT '文章ID',
    user_id INT NOT NULL COMMENT '用户ID',
    parent_id INT DEFAULT NULL COMMENT '父评论ID(用于回复)',
    content TEXT NOT NULL COMMENT '评论内容',
    status ENUM('normal', 'hidden', 'deleted') DEFAULT 'normal' COMMENT '状态',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE,
    INDEX idx_article_id (article_id),
    INDEX idx_user_id (user_id),
    INDEX idx_parent_id (parent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论表';

字段说明: - article_id:外键,关联文章表 - user_id:外键,关联用户表 - parent_id:父评论ID,用于实现评论回复功能 - content:评论内容 - status:评论状态

4. 标签表(tags)

CREATE TABLE tags (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '标签ID',
    name VARCHAR(50) NOT NULL UNIQUE COMMENT '标签名',
    slug VARCHAR(50) NOT NULL UNIQUE COMMENT '标签别名',
    description VARCHAR(200) DEFAULT NULL COMMENT '标签描述',
    article_count INT DEFAULT 0 COMMENT '文章数量',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    INDEX idx_name (name),
    INDEX idx_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签表';

5. 文章标签关联表(article_tags)

CREATE TABLE article_tags (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
    article_id INT NOT NULL COMMENT '文章ID',
    tag_id INT NOT NULL COMMENT '标签ID',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
    UNIQUE KEY uk_article_tag (article_id, tag_id),
    INDEX idx_article_id (article_id),
    INDEX idx_tag_id (tag_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章标签关联表';

说明: - 多对多关系需要中间表 - 使用联合唯一索引防止重复关联

6. 文章点赞表(article_likes)

CREATE TABLE article_likes (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
    article_id INT NOT NULL COMMENT '文章ID',
    user_id INT NOT NULL COMMENT '用户ID',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    UNIQUE KEY uk_article_user (article_id, user_id),
    INDEX idx_article_id (article_id),
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章点赞表';

ER图

erDiagram
    users ||--o{ articles : "发布"
    users ||--o{ comments : "发表"
    articles ||--o{ comments : "包含"
    articles }o--o{ tags : "标记"
    articles }o--o{ users : "点赞"

    users {
        int id PK
        string username
        string email
        string password_hash
        timestamp created_at
    }

    articles {
        int id PK
        int user_id FK
        string title
        text content
        int view_count
        timestamp created_at
    }

    comments {
        int id PK
        int article_id FK
        int user_id FK
        int parent_id FK
        text content
        timestamp created_at
    }

    tags {
        int id PK
        string name
        string slug
    }

    article_tags {
        int article_id FK
        int tag_id FK
    }

    article_likes {
        int article_id FK
        int user_id FK
    }

SQL基础操作

创建和删除

创建数据库

-- 创建数据库
CREATE DATABASE blog_system 
    DEFAULT CHARACTER SET utf8mb4 
    DEFAULT COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE blog_system;

创建表

-- 创建用户表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

删除表和数据库

-- 删除表
DROP TABLE IF EXISTS users;

-- 删除数据库
DROP DATABASE IF EXISTS blog_system;

插入数据(INSERT)

-- 插入单条数据
INSERT INTO users (username, email) 
VALUES ('zhangsan', 'zhang@example.com');

-- 插入多条数据
INSERT INTO users (username, email) VALUES
    ('lisi', 'li@example.com'),
    ('wangwu', 'wang@example.com'),
    ('zhaoliu', 'zhao@example.com');

-- 插入并返回ID(MySQL)
INSERT INTO users (username, email) 
VALUES ('qianqi', 'qian@example.com');
SELECT LAST_INSERT_ID();

查询数据(SELECT)

基本查询

-- 查询所有字段
SELECT * FROM users;

-- 查询指定字段
SELECT id, username, email FROM users;

-- 查询并重命名字段
SELECT 
    id AS user_id,
    username AS name,
    email
FROM users;

-- 查询去重
SELECT DISTINCT status FROM users;

条件查询(WHERE)

-- 等于
SELECT * FROM users WHERE id = 1;

-- 不等于
SELECT * FROM users WHERE status != 'banned';

-- 大于、小于
SELECT * FROM articles WHERE view_count > 1000;

-- 范围查询
SELECT * FROM articles WHERE view_count BETWEEN 100 AND 1000;

-- IN查询
SELECT * FROM users WHERE status IN ('active', 'inactive');

-- 模糊查询
SELECT * FROM articles WHERE title LIKE '%数据库%';

-- 多条件查询
SELECT * FROM articles 
WHERE status = 'published' 
  AND view_count > 100 
  AND created_at > '2024-01-01';

排序(ORDER BY)

-- 升序排序
SELECT * FROM articles ORDER BY created_at ASC;

-- 降序排序
SELECT * FROM articles ORDER BY view_count DESC;

-- 多字段排序
SELECT * FROM articles 
ORDER BY status ASC, view_count DESC;

分页(LIMIT)

-- 查询前10条
SELECT * FROM articles LIMIT 10;

-- 分页查询(跳过前20条,查询10条)
SELECT * FROM articles LIMIT 20, 10;

-- 或者使用OFFSET
SELECT * FROM articles LIMIT 10 OFFSET 20;

聚合函数

-- 计数
SELECT COUNT(*) FROM users;
SELECT COUNT(DISTINCT user_id) FROM articles;

-- 求和
SELECT SUM(view_count) FROM articles;

-- 平均值
SELECT AVG(view_count) FROM articles;

-- 最大值和最小值
SELECT MAX(view_count), MIN(view_count) FROM articles;

分组查询(GROUP BY)

-- 按用户分组统计文章数
SELECT user_id, COUNT(*) as article_count
FROM articles
GROUP BY user_id;

-- 分组后筛选(HAVING)
SELECT user_id, COUNT(*) as article_count
FROM articles
GROUP BY user_id
HAVING article_count > 5;

多表查询(JOIN)

-- 内连接(INNER JOIN)
SELECT 
    a.id,
    a.title,
    u.username as author
FROM articles a
INNER JOIN users u ON a.user_id = u.id;

-- 左连接(LEFT JOIN)
SELECT 
    u.username,
    COUNT(a.id) as article_count
FROM users u
LEFT JOIN articles a ON u.id = a.user_id
GROUP BY u.id;

-- 右连接(RIGHT JOIN)
SELECT 
    a.title,
    u.username
FROM articles a
RIGHT JOIN users u ON a.user_id = u.id;

-- 多表连接
SELECT 
    a.title,
    u.username as author,
    c.content as comment
FROM articles a
INNER JOIN users u ON a.user_id = u.id
LEFT JOIN comments c ON a.id = c.article_id;

更新数据(UPDATE)

-- 更新单个字段
UPDATE users 
SET email = 'newemail@example.com' 
WHERE id = 1;

-- 更新多个字段
UPDATE users 
SET 
    username = 'newname',
    email = 'newemail@example.com',
    updated_at = CURRENT_TIMESTAMP
WHERE id = 1;

-- 批量更新
UPDATE articles 
SET status = 'archived' 
WHERE created_at < '2023-01-01';

-- 基于计算更新
UPDATE articles 
SET view_count = view_count + 1 
WHERE id = 1;

删除数据(DELETE)

-- 删除指定记录
DELETE FROM users WHERE id = 1;

-- 批量删除
DELETE FROM comments WHERE created_at < '2023-01-01';

-- 删除所有记录(保留表结构)
DELETE FROM users;

-- 清空表(更快,重置自增ID)
TRUNCATE TABLE users;

索引优化

什么是索引?

索引是一种数据结构,用于快速查找数据。类似于书的目录,可以快速定位到需要的内容。

索引的优点: - 大大加快数据查询速度 - 可以保证数据的唯一性 - 加速表与表之间的连接

索引的缺点: - 占用额外的存储空间 - 降低数据插入、更新、删除的速度 - 需要维护索引

索引类型

1. 主键索引(PRIMARY KEY)

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50)
);

2. 唯一索引(UNIQUE)

-- 创建表时添加
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    email VARCHAR(100) UNIQUE
);

-- 后续添加
CREATE UNIQUE INDEX uk_username ON users(username);
ALTER TABLE users ADD UNIQUE INDEX uk_email (email);

3. 普通索引(INDEX)

-- 创建表时添加
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    user_id INT,
    INDEX idx_user_id (user_id),
    INDEX idx_title (title)
);

-- 后续添加
CREATE INDEX idx_created_at ON articles(created_at);
ALTER TABLE articles ADD INDEX idx_status (status);

4. 组合索引(复合索引)

-- 创建组合索引
CREATE INDEX idx_user_status ON articles(user_id, status);

-- 查询时可以利用组合索引
SELECT * FROM articles WHERE user_id = 1 AND status = 'published';
SELECT * FROM articles WHERE user_id = 1;  -- 也能使用索引

-- 但这个查询不能使用索引(不符合最左前缀原则)
SELECT * FROM articles WHERE status = 'published';

5. 全文索引(FULLTEXT)

-- 创建全文索引
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    content TEXT,
    FULLTEXT INDEX ft_content (content)
);

-- 使用全文索引搜索
SELECT * FROM articles 
WHERE MATCH(content) AGAINST('数据库设计' IN NATURAL LANGUAGE MODE);

索引使用原则

1. 最左前缀原则

组合索引遵循最左前缀原则,查询条件必须从索引的最左列开始。

-- 索引:(user_id, status, created_at)

 可以使用索引:
WHERE user_id = 1
WHERE user_id = 1 AND status = 'published'
WHERE user_id = 1 AND status = 'published' AND created_at > '2024-01-01'

 不能使用索引:
WHERE status = 'published'
WHERE created_at > '2024-01-01'
WHERE status = 'published' AND created_at > '2024-01-01'

2. 选择性高的列建索引

选择性 = 不同值的数量 / 总行数

-- 性别字段选择性低(只有男/女),不适合建索引
-- 用户ID选择性高,适合建索引

3. 避免在索引列上使用函数

-- ❌ 不能使用索引
SELECT * FROM users WHERE YEAR(created_at) = 2024;

-- ✅ 可以使用索引
SELECT * FROM users 
WHERE created_at >= '2024-01-01' 
  AND created_at < '2025-01-01';

4. 避免使用 != 和 NOT IN

-- ❌ 不能使用索引
SELECT * FROM users WHERE status != 'banned';

-- ✅ 可以使用索引
SELECT * FROM users WHERE status IN ('active', 'inactive');

5. 使用覆盖索引

查询的字段都在索引中,不需要回表查询。

-- 创建覆盖索引
CREATE INDEX idx_user_email ON users(id, username, email);

-- 这个查询可以直接从索引获取数据,不需要回表
SELECT id, username, email FROM users WHERE id = 1;

查看和管理索引

-- 查看表的索引
SHOW INDEX FROM users;

-- 查看索引使用情况
EXPLAIN SELECT * FROM users WHERE username = 'zhangsan';

-- 删除索引
DROP INDEX idx_username ON users;
ALTER TABLE users DROP INDEX idx_email;

EXPLAIN分析查询

EXPLAIN SELECT 
    a.title,
    u.username
FROM articles a
INNER JOIN users u ON a.user_id = u.id
WHERE a.status = 'published'
ORDER BY a.created_at DESC
LIMIT 10;

EXPLAIN输出字段: - type:连接类型(ALL < index < range < ref < eq_ref < const) - possible_keys:可能使用的索引 - key:实际使用的索引 - rows:扫描的行数 - Extra:额外信息

性能优化

查询优化

1. 只查询需要的字段

-- ❌ 不推荐
SELECT * FROM articles;

-- ✅ 推荐
SELECT id, title, summary FROM articles;

2. 避免在WHERE子句中使用函数

-- ❌ 不能使用索引
SELECT * FROM users WHERE LOWER(username) = 'zhangsan';

-- ✅ 可以使用索引
SELECT * FROM users WHERE username = 'zhangsan';

3. 使用LIMIT限制返回行数

-- 分页查询
SELECT * FROM articles 
WHERE status = 'published'
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

4. 避免使用子查询,改用JOIN

-- ❌ 子查询性能较差
SELECT * FROM articles 
WHERE user_id IN (
    SELECT id FROM users WHERE status = 'active'
);

-- ✅ 使用JOIN性能更好
SELECT a.* FROM articles a
INNER JOIN users u ON a.user_id = u.id
WHERE u.status = 'active';

5. 使用EXISTS代替IN

-- ❌ IN性能较差
SELECT * FROM users 
WHERE id IN (SELECT user_id FROM articles);

-- ✅ EXISTS性能更好
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM articles a WHERE a.user_id = u.id
);

表结构优化

1. 选择合适的数据类型

-- ❌ 使用过大的类型
CREATE TABLE users (
    id BIGINT,  -- 如果用户数不会超过21亿,INT就够了
    age VARCHAR(10),  -- 年龄应该用TINYINT
    is_active VARCHAR(10)  -- 布尔值应该用TINYINT(1)
);

-- ✅ 使用合适的类型
CREATE TABLE users (
    id INT,
    age TINYINT,
    is_active TINYINT(1)
);

2. 使用NOT NULL

-- ✅ 尽量使用NOT NULL
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    bio TEXT DEFAULT ''  -- 使用默认值代替NULL
);

3. 适当使用冗余字段

-- 在文章表中冗余统计数据,避免频繁COUNT
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    view_count INT DEFAULT 0,  -- 冗余字段
    like_count INT DEFAULT 0,  -- 冗余字段
    comment_count INT DEFAULT 0  -- 冗余字段
);

4. 垂直分表

将不常用的大字段分离到另一个表。

-- 主表(常用字段)
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(200),
    summary VARCHAR(500),
    user_id INT,
    created_at TIMESTAMP
);

-- 扩展表(不常用的大字段)
CREATE TABLE article_contents (
    article_id INT PRIMARY KEY,
    content LONGTEXT,
    FOREIGN KEY (article_id) REFERENCES articles(id)
);

5. 水平分表

当单表数据量过大时,按照某个规则分成多个表。

-- 按年份分表
CREATE TABLE articles_2023 (...);
CREATE TABLE articles_2024 (...);
CREATE TABLE articles_2025 (...);

-- 按用户ID范围分表
CREATE TABLE users_0 (...);  -- user_id 0-999999
CREATE TABLE users_1 (...);  -- user_id 1000000-1999999

缓存策略

1. 查询缓存

MySQL的查询缓存(MySQL 8.0已移除):

-- 查看查询缓存状态
SHOW VARIABLES LIKE 'query_cache%';

2. 应用层缓存

使用Redis等缓存热点数据:

# Python示例
import redis

r = redis.Redis(host='localhost', port=6379)

# 缓存文章详情
def get_article(article_id):
    # 先从缓存获取
    cache_key = f'article:{article_id}'
    cached = r.get(cache_key)

    if cached:
        return json.loads(cached)

    # 缓存未命中,从数据库查询
    article = db.query('SELECT * FROM articles WHERE id = ?', article_id)

    # 写入缓存,过期时间1小时
    r.setex(cache_key, 3600, json.dumps(article))

    return article

连接池

使用连接池减少数据库连接开销:

# Python示例(使用SQLAlchemy)
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
    'mysql://user:password@localhost/blog_system',
    poolclass=QueuePool,
    pool_size=10,  # 连接池大小
    max_overflow=20,  # 最大溢出连接数
    pool_timeout=30,  # 获取连接超时时间
    pool_recycle=3600  # 连接回收时间
)

数据库安全

SQL注入防护

SQL注入是最常见的数据库安全问题。

危险示例

# ❌ 危险:直接拼接SQL
username = request.get('username')
password = request.get('password')

sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
result = db.execute(sql)

# 攻击者输入:username = "admin' OR '1'='1"
# 实际执行的SQL:SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = ''

安全做法

# ✅ 安全:使用参数化查询
username = request.get('username')
password = request.get('password')

sql = "SELECT * FROM users WHERE username = ? AND password = ?"
result = db.execute(sql, (username, password))

密码存储

永远不要存储明文密码。

import hashlib
import os

# ❌ 危险:存储明文密码
password = "user_password"
db.execute("INSERT INTO users (password) VALUES (?)", (password,))

# ✅ 安全:使用哈希存储
def hash_password(password):
    # 生成随机盐值
    salt = os.urandom(32)
    # 使用PBKDF2哈希
    key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
    # 存储盐值和哈希值
    return salt + key

def verify_password(stored_password, provided_password):
    salt = stored_password[:32]
    stored_key = stored_password[32:]
    key = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000)
    return key == stored_key

权限控制

1. 最小权限原则

-- 创建只读用户
CREATE USER 'readonly'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON blog_system.* TO 'readonly'@'localhost';

-- 创建应用用户(有限权限)
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE, DELETE ON blog_system.* TO 'app_user'@'localhost';

-- 不要使用root用户连接应用

2. 行级权限

-- 用户只能查看自己的数据
SELECT * FROM articles WHERE user_id = current_user_id;

-- 用户只能修改自己的数据
UPDATE articles 
SET title = 'new title' 
WHERE id = 1 AND user_id = current_user_id;

数据备份

1. 定期备份

# 备份整个数据库
mysqldump -u root -p blog_system > backup_$(date +%Y%m%d).sql

# 备份指定表
mysqldump -u root -p blog_system users articles > backup_tables.sql

# 只备份结构
mysqldump -u root -p --no-data blog_system > schema.sql

2. 恢复数据

# 恢复数据库
mysql -u root -p blog_system < backup_20240101.sql

# 恢复到新数据库
mysql -u root -p -e "CREATE DATABASE blog_system_restore"
mysql -u root -p blog_system_restore < backup_20240101.sql

数据加密

1. 传输加密

使用SSL/TLS加密数据库连接:

# Python示例
import mysql.connector

conn = mysql.connector.connect(
    host='localhost',
    user='user',
    password='password',
    database='blog_system',
    ssl_ca='/path/to/ca.pem',
    ssl_cert='/path/to/client-cert.pem',
    ssl_key='/path/to/client-key.pem'
)

2. 字段加密

对敏感字段进行加密:

-- 使用AES加密
INSERT INTO users (username, phone) 
VALUES ('zhangsan', AES_ENCRYPT('13800138000', 'encryption_key'));

-- 解密查询
SELECT username, AES_DECRYPT(phone, 'encryption_key') as phone 
FROM users;

常见问题

Q1: 什么时候应该使用索引?

A: 以下情况应该建立索引: - 经常作为WHERE条件的字段 - 经常用于JOIN的字段 - 经常用于ORDER BY的字段 - 经常用于GROUP BY的字段 - 选择性高的字段(不同值多)

不应该建立索引的情况: - 表数据量很小(几百行) - 频繁更新的字段 - 选择性低的字段(如性别) - 很少使用的字段

Q2: 主键应该选择什么类型?

A: 推荐使用自增整数作为主键:

id INT PRIMARY KEY AUTO_INCREMENT

优点: - 占用空间小 - 查询效率高 - 插入性能好 - 避免页分裂

不推荐使用UUID作为主键(除非有特殊需求): - 占用空间大(36字节 vs 4字节) - 无序,导致页分裂 - 索引效率低

Q3: 如何处理软删除?

A: 使用deleted_at字段标记删除:

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    deleted_at TIMESTAMP NULL DEFAULT NULL,
    INDEX idx_deleted_at (deleted_at)
);

-- 软删除
UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = 1;

-- 查询未删除的数据
SELECT * FROM users WHERE deleted_at IS NULL;

-- 恢复数据
UPDATE users SET deleted_at = NULL WHERE id = 1;

Q4: 如何设计树形结构(如评论回复)?

A: 常见的三种方案:

方案1:邻接列表(Adjacency List)

CREATE TABLE comments (
    id INT PRIMARY KEY,
    parent_id INT,
    content TEXT
);
优点:简单易懂 缺点:查询整棵树需要递归

方案2:路径枚举(Path Enumeration)

CREATE TABLE comments (
    id INT PRIMARY KEY,
    path VARCHAR(255),  -- 如:1/3/5
    content TEXT
);
优点:查询子树方便 缺点:路径长度有限

方案3:嵌套集(Nested Set)

CREATE TABLE comments (
    id INT PRIMARY KEY,
    lft INT,
    rgt INT,
    content TEXT
);
优点:查询效率高 缺点:插入和更新复杂

Q5: 如何优化大数据量的分页查询?

A: 使用延迟关联或子查询:

-- ❌ 传统分页(深度分页慢)
SELECT * FROM articles 
ORDER BY id 
LIMIT 1000000, 20;

-- ✅ 使用子查询优化
SELECT * FROM articles 
WHERE id >= (
    SELECT id FROM articles ORDER BY id LIMIT 1000000, 1
)
LIMIT 20;

-- ✅ 使用上次查询的最大ID
SELECT * FROM articles 
WHERE id > last_id 
ORDER BY id 
LIMIT 20;

最佳实践

设计原则

  1. 遵循范式,适度反范式
  2. 基础设计遵循第三范式
  3. 根据性能需求适度反范式
  4. 权衡数据一致性和查询性能

  5. 使用有意义的命名

  6. 表名使用复数形式:users, articles
  7. 字段名见名知意:created_at, user_id
  8. 避免使用保留字和特殊字符

  9. 合理使用数据类型

  10. 选择最小的合适类型
  11. 整数优于字符串
  12. 定长优于变长(在合适的场景)

  13. 添加必要的约束

  14. 主键约束保证唯一性
  15. 外键约束保证引用完整性
  16. 非空约束保证数据完整性
  17. 默认值约束简化插入操作

  18. 合理使用索引

  19. 为常用查询字段建索引
  20. 避免过多索引
  21. 定期分析索引使用情况

开发规范

1. 表设计规范

-- ✅ 推荐的表结构
CREATE TABLE users (
    -- 主键:自增整数
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',

    -- 业务字段:合适的类型和约束
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
    email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
    password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',

    -- 状态字段:使用ENUM
    status ENUM('active', 'inactive', 'banned') DEFAULT 'active' COMMENT '状态',

    -- 时间字段:自动维护
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    -- 索引
    INDEX idx_username (username),
    INDEX idx_email (email),
    INDEX idx_status (status)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

2. SQL编写规范

-- ✅ 推荐的SQL写法
SELECT 
    u.id,
    u.username,
    u.email,
    COUNT(a.id) as article_count
FROM users u
LEFT JOIN articles a ON u.id = a.user_id
WHERE u.status = 'active'
  AND u.created_at >= '2024-01-01'
GROUP BY u.id
HAVING article_count > 0
ORDER BY article_count DESC
LIMIT 20;

-- 规范要点:
-- 1. 关键字大写
-- 2. 字段和表名小写
-- 3. 适当换行和缩进
-- 4. 使用表别名
-- 5. 明确指定字段

3. 事务使用规范

-- 开启事务
START TRANSACTION;

-- 执行多个操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
INSERT INTO transactions (from_id, to_id, amount) VALUES (1, 2, 100);

-- 提交事务
COMMIT;

-- 或者回滚
-- ROLLBACK;

性能监控

1. 慢查询日志

-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;  -- 超过2秒的查询记录

-- 查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';

2. 查询分析

-- 使用EXPLAIN分析查询
EXPLAIN SELECT * FROM articles WHERE user_id = 1;

-- 查看查询执行时间
SET profiling = 1;
SELECT * FROM articles WHERE user_id = 1;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;

3. 表状态监控

-- 查看表状态
SHOW TABLE STATUS LIKE 'articles';

-- 查看表大小
SELECT 
    table_name,
    ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
FROM information_schema.TABLES
WHERE table_schema = 'blog_system'
ORDER BY size_mb DESC;

总结

本文介绍了数据库设计的核心知识:

基础概念: - 理解数据库、表、字段、约束等基本概念 - 掌握常用的数据类型和约束类型 - 了解关系型数据库和非关系型数据库的区别

设计原则: - 遵循三大范式减少数据冗余 - 合理使用反范式提高查询性能 - 使用规范的命名和结构

SQL操作: - 掌握基本的增删改查操作 - 学会使用JOIN进行多表查询 - 了解聚合函数和分组查询

性能优化: - 合理使用索引加速查询 - 优化查询语句提高效率 - 使用缓存减少数据库压力

安全防护: - 防止SQL注入攻击 - 安全存储敏感数据 - 做好权限控制和数据备份

数据库设计是一个需要不断实践和优化的过程。建议你: 1. 多动手实践,设计实际的数据库 2. 学习优秀的开源项目的数据库设计 3. 关注性能监控,持续优化 4. 保持学习,了解新的技术和最佳实践

延伸阅读

推荐进一步学习的资源:

参考资料

  1. MySQL官方文档 - https://dev.mysql.com/doc/
  2. PostgreSQL官方文档 - https://www.postgresql.org/docs/
  3. 数据库设计三大范式 - 数据库理论基础
  4. 阿里巴巴Java开发手册(数据库部分)
  5. 高性能MySQL(第3版)- Baron Schwartz等著

练习题

  1. 设计一个电商系统的数据库,包含用户、商品、订单、购物车等功能
  2. 为你设计的数据库添加合适的索引,并使用EXPLAIN分析查询性能
  3. 编写SQL查询:统计每个用户的订单总金额,按金额降序排列
  4. 实现一个树形评论系统,支持多级回复
  5. 优化一个慢查询,将执行时间从5秒降低到1秒以内

下一步:建议学习 RESTful API设计与开发,了解如何通过API访问数据库。