mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4303 words
11 minutes
数据库基础
2023-12-06

数据库基础#

数据库是后端开发中非常重要的基础。用户、订单、商品、支付、评论、文章等业务数据,最终都需要被稳定地存储、查询、修改和维护。

这篇文章主要记录关系型数据库的基础知识,适合用来打底学习 MySQL、PostgreSQL、Oracle 等数据库。

什么是数据库#

数据库可以理解为专门管理数据的软件系统。它不仅负责保存数据,还提供查询、修改、事务、权限、备份恢复等能力。

常见数据库大致可以分为两类:

  • 关系型数据库:MySQL、PostgreSQL、Oracle、SQL Server。
  • 非关系型数据库:Redis、MongoDB、Elasticsearch、HBase。

关系型数据库使用表来组织数据。表中的每一行是一条记录,每一列是一个字段。

例如用户表:

idusernameageemail
1zhangsan18zhangsan@example.com
2lisi20lisi@example.com

常见概念#

#

表是关系型数据库中最基本的数据组织形式,通常对应一个业务对象,例如用户表、订单表、商品表。

字段#

字段是表中的一列,用来描述某个属性,例如用户名、密码、手机号、创建时间。

记录#

记录是表中的一行,代表一条具体数据。

主键#

主键用于唯一标识一条记录,不能重复,也不能为 NULL

常见主键设计:

  • 自增整数:简单、查询效率高。
  • UUID:全局唯一,但长度较长,索引成本高。
  • 雪花算法 ID:适合分布式系统,能保证全局唯一,并且整体趋势递增。

外键#

外键用于建立表与表之间的关系。例如订单表中的 user_id 可以关联用户表的 id

外键可以保证数据完整性,但在高并发项目中,有时会选择不使用数据库外键,而是在业务代码中维护关联关系,减少数据库约束带来的性能和维护成本。

SQL 基础#

SQL 是操作关系型数据库的标准语言。

DDL:数据定义语言#

DDL 用来定义数据库结构,例如创建表、修改表、删除表。

CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
age INT,
email VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

常见语句:

  • CREATE:创建数据库、表、索引。
  • ALTER:修改表结构。
  • DROP:删除数据库、表、索引。

DML:数据操作语言#

DML 用来操作表中的数据。

INSERT INTO user(username, age, email)
VALUES ('zhangsan', 18, 'zhangsan@example.com');
UPDATE user
SET age = 19
WHERE id = 1;
DELETE FROM user
WHERE id = 1;

常见语句:

  • INSERT:新增数据。
  • UPDATE:修改数据。
  • DELETE:删除数据。

DQL:数据查询语言#

DQL 主要指 SELECT 查询。

SELECT id, username, age
FROM user
WHERE age >= 18
ORDER BY created_at DESC
LIMIT 10;

常见关键字:

  • WHERE:过滤条件。
  • GROUP BY:分组。
  • HAVING:对分组后的结果继续过滤。
  • ORDER BY:排序。
  • LIMIT:限制返回条数。

DCL:数据控制语言#

DCL 用来控制用户权限。

GRANT SELECT, INSERT ON blog.* TO 'blog_user'@'%';
REVOKE INSERT ON blog.* FROM 'blog_user'@'%';

数据库约束#

约束用于保证数据的正确性和完整性。

主键约束#

主键约束保证字段唯一且不为空。

id BIGINT PRIMARY KEY

唯一约束#

唯一约束保证字段值不能重复。

email VARCHAR(100) UNIQUE

适合加唯一约束的字段:

  • 用户名
  • 邮箱
  • 手机号
  • 订单号

非空约束#

非空约束保证字段必须有值。

username VARCHAR(50) NOT NULL

默认值约束#

默认值约束用于字段没有显式赋值时自动填充。

status TINYINT DEFAULT 1

外键约束#

外键约束用于保证关联表之间的数据一致性。

user_id BIGINT,
FOREIGN KEY (user_id) REFERENCES user(id)

数据库范式#

数据库范式用于指导表结构设计,主要目标是减少数据冗余,避免插入异常、修改异常和删除异常。

常见范式包括第一范式、第二范式、第三范式。

第一范式#

第一范式要求字段具有原子性,也就是属性不可再分。

不符合第一范式的例子:

idusernamecontact
1zhangsan手机号:13800000000,邮箱@example.com

contact 同时包含手机号和邮箱,字段内部还可以继续拆分。

更合理的设计:

idusernamephoneemail
1zhangsan13800000000a@example.com

第二范式#

第二范式建立在第一范式基础上,要求非主属性必须完全依赖于主键,不能只依赖联合主键的一部分。

例如学生选课表:

student_idcourse_idstudent_namecourse_namescore
1100张三数据库90

如果主键是 (student_id, course_id),那么:

  • score 依赖完整主键。
  • student_name 只依赖 student_id
  • course_name 只依赖 course_id

这就存在部分函数依赖,不符合第二范式。

更合理的拆分方式:

  • 学生表:student_idstudent_name
  • 课程表:course_idcourse_name
  • 成绩表:student_idcourse_idscore

第三范式#

第三范式建立在第二范式基础上,要求非主属性不能传递依赖于主键。

例如:

student_idstudent_namedept_namedept_leader
1张三计算机系王老师

这里存在:

student_id -> dept_name
dept_name -> dept_leader

也就是说 dept_leader 不是直接依赖学生,而是依赖院系。此时应该拆分为学生表和院系表。

范式不是越高越好#

范式可以减少数据冗余,但过度拆表会导致查询时需要频繁 JOIN,影响性能和开发复杂度。

实际项目中通常会在规范化和性能之间做平衡:

  • 核心数据尽量满足第三范式。
  • 高频查询可以适当冗余字段。
  • 冗余字段要通过业务代码、定时任务或消息队列保证一致性。

事务#

事务是一组数据库操作的集合,这组操作要么全部成功,要么全部失败。

例如转账场景:

A账户扣款100
B账户加款100

这两个操作必须作为一个整体执行。如果 A 扣款成功,但 B 加款失败,就会导致数据错误。

ACID 特性#

事务具有四个重要特性,简称 ACID。

特性含义
原子性事务中的操作要么全部成功,要么全部失败
一致性事务执行前后,数据库必须从一个正确状态变成另一个正确状态
隔离性多个事务并发执行时,彼此之间不能随意干扰
持久性事务提交后,数据修改应该被永久保存

并发事务问题#

多个事务同时操作数据库时,可能出现并发问题。

脏读#

一个事务读取到了另一个事务尚未提交的数据。

例如事务 A 修改了用户余额但还没提交,事务 B 就读取到了这个临时余额。如果事务 A 回滚,事务 B 读到的数据就是脏数据。

不可重复读#

同一个事务中,多次读取同一条数据,结果不一致。

例如事务 A 第一次读取余额是 100,事务 B 修改并提交余额为 200,事务 A 再次读取时变成 200。

幻读#

同一个事务中,多次按条件查询,查询到的记录数量不一致。

例如事务 A 查询年龄大于 18 的用户有 10 条,事务 B 新增了一条符合条件的数据并提交,事务 A 再次查询时变成 11 条。

事务隔离级别#

数据库通过隔离级别来控制并发事务之间的影响。

隔离级别脏读不可重复读幻读
Read Uncommitted可能可能可能
Read Committed避免可能可能
Repeatable Read避免避免可能
Serializable避免避免避免

MySQL InnoDB 默认隔离级别是 Repeatable Read。在 InnoDB 中,Repeatable Read 通过 MVCC 和锁机制解决了大部分幻读问题。

MVCC#

MVCC 全称是 Multi-Version Concurrency Control,也就是多版本并发控制。

它的核心思想是:读操作不直接阻塞写操作,写操作也不直接阻塞普通快照读,而是通过数据的多个版本来实现并发控制。

在 MySQL InnoDB 中,每一行记录背后会维护一些隐藏字段,例如事务 ID、回滚指针等。查询时会根据当前事务生成的 Read View 判断某个版本的数据是否可见。

MVCC 的好处:

  • 提高并发性能。
  • 减少读写冲突。
  • 实现可重复读。

需要注意:MVCC 主要解决普通 SELECT 的一致性读问题。如果使用 SELECT ... FOR UPDATE 这类当前读,仍然会涉及锁。

索引#

索引可以理解为数据库为了加快查询速度而维护的数据结构。

如果没有索引,数据库查询数据时可能需要从第一行扫描到最后一行,这叫全表扫描。有了索引后,数据库可以快速定位到目标数据。

常见索引类型#

  • 主键索引:主键自动创建索引,用于唯一定位一条记录。
  • 唯一索引:保证字段值唯一,同时提升查询速度。
  • 普通索引:用于提升查询效率。
  • 联合索引:多个字段组成的索引。

示例:

CREATE UNIQUE INDEX uk_user_email ON user(email);
CREATE INDEX idx_user_age ON user(age);
CREATE INDEX idx_user_age_name ON user(age, username);

使用联合索引时要注意最左前缀原则。

例如索引是 (age, username)

  • WHERE age = 18 可以使用索引。
  • WHERE age = 18 AND username = 'zhangsan' 可以使用索引。
  • WHERE username = 'zhangsan' 通常不能充分使用这个联合索引。

B+ 树索引#

MySQL InnoDB 常用 B+ 树作为索引结构。

B+ 树适合数据库索引的原因:

  • 树的高度较低,查询磁盘 IO 次数少。
  • 叶子节点之间通过链表连接,适合范围查询。
  • 非叶子节点只存储索引信息,可以容纳更多键值。

例如范围查询非常适合使用 B+ 树索引:

SELECT *
FROM user
WHERE age BETWEEN 18 AND 25;

聚簇索引与非聚簇索引#

在 InnoDB 中,主键索引就是聚簇索引。表数据本身按照主键索引的结构存储。

非主键索引也叫二级索引,它的叶子节点保存的是主键值。通过二级索引查到主键后,再根据主键去聚簇索引中查询完整记录,这个过程叫回表。

例如:

SELECT *
FROM user
WHERE username = 'zhangsan';

如果 username 上有普通索引,查询过程通常是:

username索引 -> 找到主键id -> 根据主键id回表查询完整数据

如果查询字段都在索引中,就不需要回表,这叫覆盖索引。

SELECT id, username
FROM user
WHERE username = 'zhangsan';

索引失效场景#

常见索引失效场景包括:

  • 对索引列使用函数。
  • 对索引列进行计算。
  • 字符串字段没有加引号,导致隐式类型转换。
  • LIKE% 开头。
  • 联合索引没有遵守最左前缀原则。
  • 查询条件选择性太低,优化器认为全表扫描更快。

示例:

-- 可能导致索引失效
SELECT *
FROM user
WHERE YEAR(created_at) = 2026;
-- 更推荐
SELECT *
FROM user
WHERE created_at >= '2026-01-01'
AND created_at < '2027-01-01';

#

锁用于保证并发访问下的数据一致性。

表锁#

表锁会锁住整张表,粒度大,并发性能较低,但实现简单。

行锁#

行锁只锁住具体的数据行,粒度小,并发性能更好。InnoDB 支持行锁。

共享锁#

共享锁也叫读锁。多个事务可以同时持有共享锁。

SELECT *
FROM user
WHERE id = 1
LOCK IN SHARE MODE;

排他锁#

排他锁也叫写锁。一个事务持有排他锁时,其他事务不能再对同一行加共享锁或排他锁。

SELECT *
FROM user
WHERE id = 1
FOR UPDATE;

间隙锁和临键锁#

InnoDB 为了解决并发插入导致的幻读问题,引入了间隙锁和临键锁。

  • 记录锁:锁住已经存在的记录。
  • 间隙锁:锁住记录之间的范围。
  • 临键锁:记录锁 + 间隙锁。

SQL 执行顺序#

一条查询 SQL 的逻辑执行顺序通常可以理解为:

FROM
WHERE
GROUP BY
HAVING
SELECT
ORDER BY
LIMIT

例如:

SELECT department_id, COUNT(*) AS total
FROM employee
WHERE status = 1
GROUP BY department_id
HAVING COUNT(*) > 10
ORDER BY total DESC
LIMIT 5;

执行过程大致是:

  1. employee 表中取数据。
  2. 使用 WHERE 过滤状态。
  3. department_id 分组。
  4. 使用 HAVING 过滤分组结果。
  5. 查询需要返回的字段。
  6. 根据 total 排序。
  7. 返回前 5 条。

常见 Join#

Inner Join#

内连接只返回两张表都匹配的数据。

SELECT u.username, o.order_no
FROM user u
INNER JOIN orders o ON u.id = o.user_id;

Left Join#

左连接返回左表全部数据,右表匹配不到时返回 NULL

SELECT u.username, o.order_no
FROM user u
LEFT JOIN orders o ON u.id = o.user_id;

Right Join#

右连接返回右表全部数据,左表匹配不到时返回 NULL

SELECT u.username, o.order_no
FROM user u
RIGHT JOIN orders o ON u.id = o.user_id;

数据库设计建议#

字段设计#

字段设计要尽量清晰、稳定、可扩展。

常见建议:

  • 主键尽量使用无业务含义的 ID。
  • 金额不要使用浮点数,推荐使用 DECIMAL 或以分为单位的整数。
  • 时间字段建议统一使用 created_atupdated_at
  • 状态字段可以使用 TINYINT,并在代码中定义枚举。
  • 重要字段加 NOT NULL 和默认值。

表设计#

表设计要围绕业务对象展开。

例如订单系统通常会拆分为:

  • 订单主表:保存订单整体信息。
  • 订单明细表:保存商品明细。
  • 支付记录表:保存支付流水。
  • 物流记录表:保存物流状态。

这样可以避免一张表字段过多,也能让不同业务数据边界更清楚。

索引设计#

索引设计要根据查询场景来决定,不是越多越好。

适合建立索引的字段:

  • 经常作为查询条件的字段。
  • 经常用于排序的字段。
  • 经常用于分组的字段。
  • 表关联字段。
  • 唯一业务字段。

不适合建立索引的字段:

  • 数据量很小的表。
  • 区分度很低的字段,例如性别。
  • 经常更新但很少查询的字段。

索引会提高查询速度,但也会增加写入成本,因为新增、修改、删除数据时,数据库需要同步维护索引结构。

慢 SQL 优化思路#

遇到慢 SQL 时,不要只凭感觉改,可以按以下步骤排查。

使用 EXPLAIN 分析执行计划#

EXPLAIN
SELECT *
FROM user
WHERE username = 'zhangsan';

重点关注:

  • type:访问类型,通常越接近 constref 越好。
  • key:实际使用的索引。
  • rows:预计扫描行数。
  • Extra:是否出现 Using filesortUsing temporary

检查是否命中索引#

如果查询没有使用索引,要检查:

  • 查询条件字段是否有索引。
  • 是否触发了索引失效。
  • 联合索引顺序是否合理。

减少返回数据量#

不要习惯性写:

SELECT *
FROM user;

更推荐只查询需要的字段:

SELECT id, username, email
FROM user;

避免深分页#

深分页会导致数据库扫描大量数据。

SELECT *
FROM article
ORDER BY id
LIMIT 100000, 20;

可以改为基于上一页最后一条 ID 查询:

SELECT *
FROM article
WHERE id > 100000
ORDER BY id
LIMIT 20;

备份与恢复#

数据库数据非常重要,生产环境必须有备份机制。

常见备份方式:

  • 全量备份:定期备份完整数据。
  • 增量备份:只备份上次备份之后发生变化的数据。
  • binlog:记录数据库写操作,可用于恢复到某个时间点。

备份不是最终目的,真正重要的是恢复能力。只做备份但从不演练恢复,风险仍然很高。

分库分表#

当单表数据量过大、单库写入压力过高时,可以考虑分库分表。

垂直拆分#

按照业务模块拆分。

例如:

  • 用户库
  • 订单库
  • 商品库
  • 支付库

水平拆分#

按照某个规则把同一张表的数据拆到多张表中。

例如根据用户 ID 取模:

user_id % 4

拆分为:

  • order_0
  • order_1
  • order_2
  • order_3

分库分表可以提升扩展能力,但也会带来复杂度:

  • 跨库事务更难处理。
  • 跨库 Join 更复杂。
  • 全局唯一 ID 需要单独设计。
  • 分页、排序、统计会更麻烦。

所以分库分表通常是数据量和并发压力达到一定程度后的方案,不应该一开始就过度设计。

总结#

数据库基础可以从几个方向理解:

  • 表、字段、记录、主键、外键是关系型数据库的基本组成。
  • SQL 是操作数据库的核心语言。
  • 范式用于指导表结构设计,但实际项目需要兼顾查询性能。
  • 事务通过 ACID 保证数据一致性。
  • 索引可以提升查询速度,但也会增加写入成本。
  • 锁和 MVCC 用来解决并发访问下的数据一致性问题。
  • 慢 SQL 优化要结合执行计划、索引设计和业务查询场景。

对于后端开发来说,数据库不是只会写 SELECTINSERTUPDATEDELETE 就够了,更重要的是理解数据如何组织、如何保证一致性,以及如何在高并发场景下保持性能稳定。

Share

If this article helped you, please share it with others!

数据库基础
https://mizuki.mysqil.com/posts/数据库基础/
Author
梦幻晨风
Published at
2023-12-06
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents