数据库基础
数据库是后端开发中非常重要的基础。用户、订单、商品、支付、评论、文章等业务数据,最终都需要被稳定地存储、查询、修改和维护。
这篇文章主要记录关系型数据库的基础知识,适合用来打底学习 MySQL、PostgreSQL、Oracle 等数据库。
什么是数据库
数据库可以理解为专门管理数据的软件系统。它不仅负责保存数据,还提供查询、修改、事务、权限、备份恢复等能力。
常见数据库大致可以分为两类:
- 关系型数据库:MySQL、PostgreSQL、Oracle、SQL Server。
- 非关系型数据库:Redis、MongoDB、Elasticsearch、HBase。
关系型数据库使用表来组织数据。表中的每一行是一条记录,每一列是一个字段。
例如用户表:
| id | username | age | |
|---|---|---|---|
| 1 | zhangsan | 18 | zhangsan@example.com |
| 2 | lisi | 20 | lisi@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 userSET age = 19WHERE id = 1;
DELETE FROM userWHERE id = 1;常见语句:
INSERT:新增数据。UPDATE:修改数据。DELETE:删除数据。
DQL:数据查询语言
DQL 主要指 SELECT 查询。
SELECT id, username, ageFROM userWHERE age >= 18ORDER BY created_at DESCLIMIT 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)数据库范式
数据库范式用于指导表结构设计,主要目标是减少数据冗余,避免插入异常、修改异常和删除异常。
常见范式包括第一范式、第二范式、第三范式。
第一范式
第一范式要求字段具有原子性,也就是属性不可再分。
不符合第一范式的例子:
contact 同时包含手机号和邮箱,字段内部还可以继续拆分。
更合理的设计:
| id | username | phone | |
|---|---|---|---|
| 1 | zhangsan | 13800000000 | a@example.com |
第二范式
第二范式建立在第一范式基础上,要求非主属性必须完全依赖于主键,不能只依赖联合主键的一部分。
例如学生选课表:
| student_id | course_id | student_name | course_name | score |
|---|---|---|---|---|
| 1 | 100 | 张三 | 数据库 | 90 |
如果主键是 (student_id, course_id),那么:
score依赖完整主键。student_name只依赖student_id。course_name只依赖course_id。
这就存在部分函数依赖,不符合第二范式。
更合理的拆分方式:
- 学生表:
student_id、student_name - 课程表:
course_id、course_name - 成绩表:
student_id、course_id、score
第三范式
第三范式建立在第二范式基础上,要求非主属性不能传递依赖于主键。
例如:
| student_id | student_name | dept_name | dept_leader |
|---|---|---|---|
| 1 | 张三 | 计算机系 | 王老师 |
这里存在:
student_id -> dept_namedept_name -> dept_leader也就是说 dept_leader 不是直接依赖学生,而是依赖院系。此时应该拆分为学生表和院系表。
范式不是越高越好
范式可以减少数据冗余,但过度拆表会导致查询时需要频繁 JOIN,影响性能和开发复杂度。
实际项目中通常会在规范化和性能之间做平衡:
- 核心数据尽量满足第三范式。
- 高频查询可以适当冗余字段。
- 冗余字段要通过业务代码、定时任务或消息队列保证一致性。
事务
事务是一组数据库操作的集合,这组操作要么全部成功,要么全部失败。
例如转账场景:
A账户扣款100B账户加款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 userWHERE age BETWEEN 18 AND 25;聚簇索引与非聚簇索引
在 InnoDB 中,主键索引就是聚簇索引。表数据本身按照主键索引的结构存储。
非主键索引也叫二级索引,它的叶子节点保存的是主键值。通过二级索引查到主键后,再根据主键去聚簇索引中查询完整记录,这个过程叫回表。
例如:
SELECT *FROM userWHERE username = 'zhangsan';如果 username 上有普通索引,查询过程通常是:
username索引 -> 找到主键id -> 根据主键id回表查询完整数据如果查询字段都在索引中,就不需要回表,这叫覆盖索引。
SELECT id, usernameFROM userWHERE username = 'zhangsan';索引失效场景
常见索引失效场景包括:
- 对索引列使用函数。
- 对索引列进行计算。
- 字符串字段没有加引号,导致隐式类型转换。
LIKE以%开头。- 联合索引没有遵守最左前缀原则。
- 查询条件选择性太低,优化器认为全表扫描更快。
示例:
-- 可能导致索引失效SELECT *FROM userWHERE YEAR(created_at) = 2026;
-- 更推荐SELECT *FROM userWHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';锁
锁用于保证并发访问下的数据一致性。
表锁
表锁会锁住整张表,粒度大,并发性能较低,但实现简单。
行锁
行锁只锁住具体的数据行,粒度小,并发性能更好。InnoDB 支持行锁。
共享锁
共享锁也叫读锁。多个事务可以同时持有共享锁。
SELECT *FROM userWHERE id = 1LOCK IN SHARE MODE;排他锁
排他锁也叫写锁。一个事务持有排他锁时,其他事务不能再对同一行加共享锁或排他锁。
SELECT *FROM userWHERE id = 1FOR UPDATE;间隙锁和临键锁
InnoDB 为了解决并发插入导致的幻读问题,引入了间隙锁和临键锁。
- 记录锁:锁住已经存在的记录。
- 间隙锁:锁住记录之间的范围。
- 临键锁:记录锁 + 间隙锁。
SQL 执行顺序
一条查询 SQL 的逻辑执行顺序通常可以理解为:
FROMWHEREGROUP BYHAVINGSELECTORDER BYLIMIT例如:
SELECT department_id, COUNT(*) AS totalFROM employeeWHERE status = 1GROUP BY department_idHAVING COUNT(*) > 10ORDER BY total DESCLIMIT 5;执行过程大致是:
- 从
employee表中取数据。 - 使用
WHERE过滤状态。 - 按
department_id分组。 - 使用
HAVING过滤分组结果。 - 查询需要返回的字段。
- 根据
total排序。 - 返回前 5 条。
常见 Join
Inner Join
内连接只返回两张表都匹配的数据。
SELECT u.username, o.order_noFROM user uINNER JOIN orders o ON u.id = o.user_id;Left Join
左连接返回左表全部数据,右表匹配不到时返回 NULL。
SELECT u.username, o.order_noFROM user uLEFT JOIN orders o ON u.id = o.user_id;Right Join
右连接返回右表全部数据,左表匹配不到时返回 NULL。
SELECT u.username, o.order_noFROM user uRIGHT JOIN orders o ON u.id = o.user_id;数据库设计建议
字段设计
字段设计要尽量清晰、稳定、可扩展。
常见建议:
- 主键尽量使用无业务含义的 ID。
- 金额不要使用浮点数,推荐使用
DECIMAL或以分为单位的整数。 - 时间字段建议统一使用
created_at、updated_at。 - 状态字段可以使用
TINYINT,并在代码中定义枚举。 - 重要字段加
NOT NULL和默认值。
表设计
表设计要围绕业务对象展开。
例如订单系统通常会拆分为:
- 订单主表:保存订单整体信息。
- 订单明细表:保存商品明细。
- 支付记录表:保存支付流水。
- 物流记录表:保存物流状态。
这样可以避免一张表字段过多,也能让不同业务数据边界更清楚。
索引设计
索引设计要根据查询场景来决定,不是越多越好。
适合建立索引的字段:
- 经常作为查询条件的字段。
- 经常用于排序的字段。
- 经常用于分组的字段。
- 表关联字段。
- 唯一业务字段。
不适合建立索引的字段:
- 数据量很小的表。
- 区分度很低的字段,例如性别。
- 经常更新但很少查询的字段。
索引会提高查询速度,但也会增加写入成本,因为新增、修改、删除数据时,数据库需要同步维护索引结构。
慢 SQL 优化思路
遇到慢 SQL 时,不要只凭感觉改,可以按以下步骤排查。
使用 EXPLAIN 分析执行计划
EXPLAINSELECT *FROM userWHERE username = 'zhangsan';重点关注:
type:访问类型,通常越接近const、ref越好。key:实际使用的索引。rows:预计扫描行数。Extra:是否出现Using filesort、Using temporary。
检查是否命中索引
如果查询没有使用索引,要检查:
- 查询条件字段是否有索引。
- 是否触发了索引失效。
- 联合索引顺序是否合理。
减少返回数据量
不要习惯性写:
SELECT *FROM user;更推荐只查询需要的字段:
SELECT id, username, emailFROM user;避免深分页
深分页会导致数据库扫描大量数据。
SELECT *FROM articleORDER BY idLIMIT 100000, 20;可以改为基于上一页最后一条 ID 查询:
SELECT *FROM articleWHERE id > 100000ORDER BY idLIMIT 20;备份与恢复
数据库数据非常重要,生产环境必须有备份机制。
常见备份方式:
- 全量备份:定期备份完整数据。
- 增量备份:只备份上次备份之后发生变化的数据。
- binlog:记录数据库写操作,可用于恢复到某个时间点。
备份不是最终目的,真正重要的是恢复能力。只做备份但从不演练恢复,风险仍然很高。
分库分表
当单表数据量过大、单库写入压力过高时,可以考虑分库分表。
垂直拆分
按照业务模块拆分。
例如:
- 用户库
- 订单库
- 商品库
- 支付库
水平拆分
按照某个规则把同一张表的数据拆到多张表中。
例如根据用户 ID 取模:
user_id % 4拆分为:
- order_0
- order_1
- order_2
- order_3
分库分表可以提升扩展能力,但也会带来复杂度:
- 跨库事务更难处理。
- 跨库 Join 更复杂。
- 全局唯一 ID 需要单独设计。
- 分页、排序、统计会更麻烦。
所以分库分表通常是数据量和并发压力达到一定程度后的方案,不应该一开始就过度设计。
总结
数据库基础可以从几个方向理解:
- 表、字段、记录、主键、外键是关系型数据库的基本组成。
- SQL 是操作数据库的核心语言。
- 范式用于指导表结构设计,但实际项目需要兼顾查询性能。
- 事务通过 ACID 保证数据一致性。
- 索引可以提升查询速度,但也会增加写入成本。
- 锁和 MVCC 用来解决并发访问下的数据一致性问题。
- 慢 SQL 优化要结合执行计划、索引设计和业务查询场景。
对于后端开发来说,数据库不是只会写 SELECT、INSERT、UPDATE、DELETE 就够了,更重要的是理解数据如何组织、如何保证一致性,以及如何在高并发场景下保持性能稳定。
If this article helped you, please share it with others!
Some information may be outdated






