一、数据库概述

1.1 什么是数据库

数据库(Database)是指按照一定结构组织、存储和管理数据的仓库。它并不是简单地把数据堆积在磁盘上,而是通过规范化的数据模型、约束规则和查询语言,让数据能够被高效地存取、维护和共享。一个完整的数据库系统通常包含数据本身、数据库管理系统(DBMS)以及基于其上的应用程序。

数据库管理系统的英文是 Database Management System,简称 DBMS。它是介于操作系统和应用程序之间的一层软件,负责数据的定义、存储、查询、更新、安全控制和并发管理。常见的数据库管理系统包括 MySQL、PostgreSQL、Oracle、SQL Server、SQLite、MongoDB、Redis 等。需要注意,很多人习惯把数据库管理系统直接称为数据库,例如说“我们公司用的是 MySQL 数据库”,这里的 MySQL 实际上指的是 DBMS,而其中存储的具体数据才是一个个数据库实例。

与实际生活类比,数据库类似于一个大型仓库。仓库里有货架、分区、编号、出入库记录和盘点流程,数据库里则有表、索引、主键、事务和日志。仓库管理员相当于 DBMS,负责接收指令、搬运货物并保证货物不丢失、不混乱;而仓库里的货物就是数据本身。

1.2 为什么要使用数据库

在数据库出现之前,程序往往直接使用文件系统保存数据,例如把用户信息写入文本文件或自定义格式的二进制文件。文件存储方式在数据量小、并发低的时候尚可应付,但随着系统复杂度的提高,它暴露出越来越多的问题:

  • 数据冗余与不一致:同一个数据可能被多个程序分别保存,修改一处难以同步到其他位置,容易产生不一致。
  • 并发访问困难:多个用户同时读写同一文件时,需要开发者自己处理锁和同步,稍有不慎就会导致数据损坏。
  • 数据完整性难以保证:文件系统无法方便地定义字段类型、取值范围和关联关系,错误数据容易被写入。
  • 查询效率低下:要在海量文件中寻找符合条件的数据,往往需要逐个读取和解析,性能很差。
  • 安全与权限控制薄弱:文件系统级别的权限粒度较粗,很难实现“某用户只能查看但不能修改某类数据”这样的精细控制。

数据库正是为了解决上述问题而诞生的。它提供了统一的数据定义与操作接口、事务机制、索引结构、权限体系、备份恢复等一系列能力,让开发者能够把精力集中在业务逻辑上,而不是底层数据管理上。

1.3 数据库的发展简史

数据库技术的发展大致经历了以下几个重要阶段:

  1. 层次模型与网状模型阶段:最早期的数据库采用层次模型和网状模型组织数据。层次模型像一棵倒置的树,数据通过父子关系连接;网状模型则允许更复杂的多对多关系。IBM 的 IMS 是层次数据库的代表。这一阶段的主要问题是数据模型复杂、缺乏灵活性,应用程序必须深度依赖物理存储结构。
  2. 关系模型阶段:1970 年,IBM 研究员 Edgar F. Codd 发表论文《A Relational Model of Data for Large Shared Data Banks》,提出了关系模型。关系模型把数据组织为二维表,并通过集合理论和谓词逻辑来描述数据操作。随后,SQL 语言逐渐成为关系数据库的标准查询语言。Oracle、MySQL、PostgreSQL、SQL Server 等关系型数据库相继出现并统治市场至今。
  3. 面向对象与对象关系阶段:随着面向对象编程的流行,出现了面向对象数据库和对象关系数据库,试图把对象模型映射到存储层。不过这一方向并没有大规模取代传统关系数据库,而是以对象关系映射(ORM)框架的形式在应用层解决阻抗失配问题。
  4. NoSQL 阶段:进入互联网时代后,数据规模爆炸式增长,对水平扩展、高并发读写和灵活数据模型的需求日益强烈。NoSQL 数据库应运而生,包括键值存储(Redis)、文档存储(MongoDB)、列族存储(HBase、Cassandra)、图数据库(Neo4j)等。
  5. NewSQL 与云原生数据库阶段:NewSQL 试图在保留关系模型和 ACID 事务的同时,提供类似 NoSQL 的水平扩展能力,代表产品有 TiDB、CockroachDB 等。同时,云数据库(如 Amazon Aurora、阿里云 RDS、腾讯云 TDSQL)以服务化形式交付,进一步降低了运维成本。

1.4 数据库的分类

按照数据模型的不同,数据库主要可以分为关系型数据库和非关系型数据库两大类。

关系型数据库(RDBMS)以关系模型为基础,数据存储在由行和列组成的表中,表与表之间通过外键建立关联。它的优势在于强大的事务能力、数据一致性和成熟的 SQL 生态。典型产品包括 MySQL、PostgreSQL、Oracle、SQL Server、SQLite 等。

非关系型数据库(NoSQL)则泛指不采用传统关系模型的数据库,常见类型如下:

类型代表产品主要特点典型场景
键值存储Redis、Memcached以键值对形式存储,读写极快缓存、会话、计数器、排行榜
文档存储MongoDB、CouchDB以 JSON 或 BSON 文档为基本单位,结构灵活内容管理、用户资料、日志
列族存储HBase、Cassandra按列族组织数据,适合大规模分布式存储海量数据分析、时序数据
图数据库Neo4j、JanusGraph以节点和边表示实体及关系,擅长深度关联查询社交网络、推荐系统、知识图谱
搜索引擎Elasticsearch基于倒排索引,全文检索能力强日志搜索、商品搜索、全文检索
时序数据库InfluxDB、TimescaleDB针对时间戳数据优化写入和聚合查询监控指标、物联网数据、行情数据

在实际工程中,关系型数据库与非关系型数据库常常混合使用,形成多数据库协同的架构。例如,用 MySQL 存储核心交易数据,用 Redis 缓存热点数据,用 Elasticsearch 承担全文搜索任务,用时序数据库记录监控指标。理解各类数据库的适用场景,是架构设计的基本功。

二、关系型数据库核心概念

2.1 关系模型

关系模型是关系型数据库的理论基础。在关系模型中,数据被组织为一张张二维表,术语上称为关系(Relation)。每一张表都有一个唯一的表名,由若干行和若干列组成。列称为属性(Attribute),行称为元组(Tuple),而元组的集合就是关系本身。

关系模型具有以下重要特性:

  • 每一列都是原子的:列的值不可再分,不允许多值或嵌套结构。这也被称为第一范式的要求。
  • 每一列有唯一名称:同一张表内不允许出现同名列。
  • 行的顺序无关紧要:关系是元组的集合,集合没有顺序。
  • 列的顺序同样无关紧要:只要列名和定义一致,物理顺序不影响逻辑关系。
  • 候选键唯一标识行:表中应当存在一个或多个属性组合,能够唯一确定一行数据。

关系模型之所以强大,是因为它建立在严谨的数学理论之上。E. F. Codd 把集合论和一阶谓词逻辑引入数据管理领域,使得数据的查询、插入、删除和更新都可以用声明式语言表达。SQL 语言正是关系模型思想的具体实现。

2.2 表、行与列

表(Table)是关系数据库中数据存储的基本单位。以一张用户表为例:

idnameageemail
1张三28zhangsan@example.com
2李四32lisi@example.com
3王五25wangwu@example.com

这张表包含四列,分别是 id、name、age 和 email,每一行代表一个用户。列定义了数据的类型、长度、是否允许为空以及默认值等信息;行则是实际存储的数据记录。

列的数据类型非常重要,它直接影响存储空间和运算行为。常见的数据类型包括:整数类型(INT、BIGINT、SMALLINT)、小数类型(DECIMAL、FLOAT、DOUBLE)、字符类型(CHAR、VARCHAR、TEXT)、日期时间类型(DATE、TIME、DATETIME、TIMESTAMP)、布尔类型(BOOLEAN)以及二进制类型(BLOB)等。例如,VARCHAR(50) 表示最多存储 50 个字符的变长字符串,DECIMAL(10,2) 表示总共 10 位数字、其中小数部分 2 位的精确小数,常用于金额字段。

2.3 主键与外键

主键(Primary Key)是表中用来唯一标识每一行的一列或一组列。主键必须满足两个条件:非空且唯一。每张表最多只能有一个主键,但主键可以由多个列组合而成,称为复合主键。例如用户表中的 id 列就是典型的主键。

主键在物理存储上也扮演重要角色。在 MySQL 的 InnoDB 引擎中,表数据按照主键的顺序组织存储,这种结构称为聚簇索引。因此,选择一个合适的、稳定的、尽量短小的主键,对性能和存储都有正面影响。常见的做法是使用自增整数或 UUID 作为主键。自增整数更节省空间且写入效率高,但不利于分布式合并;UUID 全局唯一、便于分布式生成,但占用空间较大且随机性会导致索引页分裂。

外键(Foreign Key)用来建立表与表之间的关联。外键是某张表中的一个或多个列,其取值必须引用另一张表的主键或唯一键。例如,订单表 orders 中的 user_id 列可以定义为外键,引用用户表 users 的 id 列,表示“每一笔订单都属于某个用户”。

外键对数据完整性有两方面作用:一是保证引用有效,不允许插入一个不存在的用户 ID;二是控制关联变更行为,例如在删除用户时,可以配置为级联删除其订单(CASCADE)、拒绝删除(RESTRICT)、把订单中的外键置空(SET NULL)或置为默认值(SET DEFAULT)。

不过在互联网高并发系统中,很多团队选择在应用层维护关联约束,而不用数据库物理外键。原因包括:物理外键会增加写入开销、影响分库分表、锁粒度较粗、难以做灵活的数据归档等。是否使用物理外键需要结合业务规模和团队规范权衡,但理解外键概念对设计数据模型依然必不可少。

2.4 约束

约束(Constraint)是数据库保证数据完整性的一组规则。除了主键和外键,常见的约束还包括:

  • 唯一约束(UNIQUE):保证某列或某组列的值在表中不重复。与主键的区别是唯一约束允许存在 NULL 值,且一张表可以定义多个唯一约束。
  • 非空约束(NOT NULL):保证某列不能存储 NULL 值。
  • 检查约束(CHECK):保证列的值满足指定条件,例如 CHECK (age >= 0 AND age <= 150)。不同数据库对检查约束的支持程度不同,MySQL 从 8.0.16 起才真正强制执行检查约束。
  • 默认值(DEFAULT):当插入数据时未显式指定某列的值,数据库自动填入默认值。

约束本质上是对业务的规则固化。合理使用约束可以让数据库在数据入口处拦截非法数据,避免脏数据进入系统。但约束并非越多越好,过度使用检查约束和触发器也可能拖慢写入性能并增加维护复杂度。

三、数据库设计

3.1 数据库设计流程

数据库设计是把业务需求转化为规范数据模型的过程。一个典型的数据库设计流程包括以下阶段:

  1. 需求分析:明确系统要管理哪些数据、用户如何使用这些数据、有哪些查询和报表需求。
  2. 概念结构设计:使用实体关系模型(ER 模型)抽象现实世界的实体、属性和联系,得到全局概念模型。
  3. 逻辑结构设计:把 ER 模型转换为具体数据库支持的关系模式,确定表、字段、主键、外键。
  4. 物理结构设计:为逻辑模型选择合适的存储引擎、索引策略、分区方案和文件组织方式。
  5. 实施与维护:建库建表、加载数据、调优索引,并在后续运行中持续监控和调整。

数据库设计是整个软件系统的根基。表结构设计一旦确定并上线,后期修改的代价往往很高,因此应当从一开始就充分考虑业务扩展性和查询模式。

3.2 实体关系模型

实体关系模型(Entity-Relationship Model,简称 ER 模型)是进行概念设计的常用工具。它用三个核心元素来描述现实世界:

  • 实体(Entity):现实世界中可区分的事物,如用户、商品、订单、部门。实体在数据库设计中通常对应一张表。
  • 属性(Attribute):实体具有的特征,如用户的姓名、年龄、邮箱,商品的名称、价格、库存。
  • 联系(Relationship):实体之间的关联。根据关联的数量关系,联系可分为一对一(1:1)、一对多(1:N)和多对多(M:N)。

例如,在一个电商系统中,用户和订单是一对多关系(一个用户可以下多笔订单),订单和商品是多对多关系(一笔订单包含多个商品,一个商品也可以出现在多笔订单中),而用户和收货地址是一对多关系。多对多关系在关系数据库中通常需要拆分为一张中间表,例如订单明细表 order_items,它同时关联订单表与商品表。

3.3 数据库范式

范式(Normal Form)是一组用来减少数据冗余、避免更新异常的规则。范式分为多个等级,级别越高,数据结构越规范,冗余越少,但也可能带来更多的表连接和查询复杂度。

第一范式(1NF):要求表中的每个字段都是原子的,不可再分。例如,不能把“手机号1、手机号2”放在同一个字段中,而应拆分到独立记录或独立列。

第二范式(2NF):在满足第一范式的基础上,要求所有非主键字段完全依赖于主键,而不是只依赖主键的一部分。这条规则主要针对复合主键。例如,一张成绩表如果以(学生ID,课程ID)作为复合主键,那么课程名称只依赖课程ID、不依赖学生ID,这就属于部分依赖,应把课程信息拆到课程表。

第三范式(3NF):在满足第二范式的基础上,要求非主键字段之间不存在传递依赖。即非主键字段不能依赖于其他非主键字段。例如,员工表中有部门ID和部门名称两个字段,部门名称依赖于部门ID,而部门ID又依赖于员工主键,这就是传递依赖,应把部门信息拆到独立的部门表。

BC 范式(BCNF):是第三范式的加强版,要求每一个决定因素都包含候选键。它在主键为复合键且存在多个候选键时才有明显意义。

此外还有第四范式(4NF)和第五范式(5NF),它们处理多值依赖和连接依赖等更复杂的情况,在实际业务设计中使用较少。

通常情况下,设计到第三范式已经能够满足大多数业务系统的要求。需要注意的是,范式化虽然能减少冗余,但过度范式化会导致查询时需要频繁进行多表连接,影响性能。因此,在实际项目中常常会进行有意的反范式化(Denormalization),例如在订单表中冗余存储商品名称和单价,以换取更快的查询速度。设计原则是“先规范化,再有针对性地反规范化”。

3.4 一个设计实例

以一个简单的博客系统为例,核心实体包括用户(User)、文章(Post)、分类(Category)和评论(Comment)。它们之间的关系大致如下:

  • 一个用户可以发布多篇文章,一个分类下可以有多篇文章,因此用户与文章是一对多、分类与文章也是一对多。
  • 一篇文章可以有多条评论,一条评论属于一篇文章,也属于一个用户,因此评论与文章、评论与用户都是一对多关系。

对应的基础表结构可以设计为:users 表存储用户信息,categories 表存储分类信息,posts 表存储文章并包含 user_id 和 category_id 两个外键,comments 表存储评论并包含 post_id 和 user_id 外键。通过这样的结构,我们可以方便地查询某用户的所有文章、某分类下的所有文章、某文章下的所有评论,以及评论对应的用户信息。

四、SQL 语言详解

4.1 SQL 概述与分类

SQL(Structured Query Language,结构化查询语言)是操作关系型数据库的标准语言。它由美国国家标准协会(ANSI)和国际标准化组织(ISO)制定标准,各数据库厂商在标准基础上又做了各自的扩展。尽管方言存在差异,但核心语法高度一致,掌握标准 SQL 基本可以迁移到任意关系型数据库。

SQL 通常按功能分为以下几类:

  • DDL(数据定义语言):用于定义和修改数据库结构,包括 CREATE、ALTER、DROP、TRUNCATE 等。
  • DML(数据操作语言):用于操作表中的数据,包括 INSERT、UPDATE、DELETE 等。
  • DQL(数据查询语言):用于查询数据,核心是 SELECT 语句。
  • DCL(数据控制语言):用于管理权限和访问控制,包括 GRANT、REVOKE 等。
  • TCL(事务控制语言):用于管理事务,包括 COMMIT、ROLLBACK、SAVEPOINT 等。

下面分别介绍这些语句的常用用法。

4.2 DDL:数据定义语言

创建数据库和表的典型语法如下:

CREATE DATABASE blog_db DEFAULT CHARACTER SET utf8mb4;

USE blog_db;

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

上述语句创建了一个名为 blog_db 的数据库,并指定默认字符集为 utf8mb4,以支持完整的 Unicode 字符(包括 emoji)。随后创建了 users 表,其中 id 是自增主键,username 非空且唯一,created_at 默认取当前时间。

ALTER 语句用于修改已有表结构,例如添加列、修改列类型、删除列或添加索引:

ALTER TABLE users ADD COLUMN phone VARCHAR(20);

ALTER TABLE users MODIFY COLUMN email VARCHAR(150) NOT NULL;

ALTER TABLE users DROP COLUMN age;

ALTER TABLE users ADD INDEX idx_username (username);

DROP 语句用于删除表或数据库,TRUNCATE 用于清空表数据但保留表结构:

DROP TABLE users;

TRUNCATE TABLE users;

需要注意,DROP 和 TRUNCATE 都是不可逆的危险操作,执行前务必确认。它们与 DELETE 也有重要区别:DELETE 可以带 WHERE 条件逐行删除,通常写入日志、可回滚,但速度较慢;TRUNCATE 直接释放整表数据页,不记录逐行日志,速度极快但不能按条件删除,且会重置自增计数。

4.3 DML:数据操作语言

插入数据的 INSERT 语句有以下几种常见形式:

INSERT INTO users (username, email, age) VALUES ('zhangsan', 'zhangsan@example.com', 28);

INSERT INTO users (username, email) VALUES
('lisi', 'lisi@example.com'),
('wangwu', 'wangwu@example.com');

更新数据的 UPDATE 语句:

UPDATE users SET age = 29 WHERE username = 'zhangsan';

删除数据的 DELETE 语句:

DELETE FROM users WHERE username = 'lisi';

特别需要强调的是,执行 UPDATE 和 DELETE 时必须提供准确的 WHERE 条件。如果忘记 WHERE 条件,将会更新或删除整张表的所有数据。生产环境中建议先执行对应的 SELECT 查询确认影响范围,再执行写入操作。

4.4 DQL:数据查询语言

SELECT 是 SQL 中使用最频繁、功能最丰富的语句。下面结合示例逐一展开。

假设有一张商品表 products,结构如下:

CREATE TABLE products (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    category VARCHAR(50),
    price DECIMAL(10,2),
    stock INT,
    status VARCHAR(20)
);

基础查询与条件过滤

SELECT name, price FROM products WHERE price > 100 AND status = 'on_sale';

WHERE 子句支持比较运算符(=、!=、>、<、>=、<=)、逻辑运算符(AND、OR、NOT)、范围判断(BETWEEN … AND)、集合判断(IN)以及模糊匹配(LIKE)。

SELECT * FROM products WHERE price BETWEEN 50 AND 200;

SELECT * FROM products WHERE category IN ('手机', '电脑', '平板');

SELECT * FROM products WHERE name LIKE '%手机%';

LIKE 中的百分号表示任意多个字符,下划线表示任意单个字符。需要注意的是,以通配符开头的模糊查询(如百分号手机百分号)通常无法利用普通索引,大数据量下应谨慎使用,必要时改用全文索引或搜索引擎。

排序与分页

SELECT name, price FROM products ORDER BY price DESC, id ASC;

SELECT name, price FROM products ORDER BY price DESC LIMIT 10 OFFSET 20;

LIMIT 子句用于限制返回行数,OFFSET 表示跳过多少行。分页查询在数据量很大时,OFFSET 过大会导致性能下降,此时可考虑使用基于游标的分页方式,例如记住上一页最后一条记录的主键,用 WHERE id > last_id ORDER BY id LIMIT 10 的方式继续翻页。

聚合函数

SELECT COUNT(*) AS total_count,
       AVG(price) AS avg_price,
       MAX(price) AS max_price,
       MIN(price) AS min_price,
       SUM(stock) AS total_stock
FROM products;

常用聚合函数包括 COUNT(计数)、SUM(求和)、AVG(平均值)、MAX(最大值)和 MIN(最小值)。聚合函数通常与 GROUP BY 配合使用。

分组与过滤

SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 5
ORDER BY cnt DESC;

GROUP BY 把相同分类的行归为一组,然后对每组计算聚合值。HAVING 用于过滤分组后的结果,它和 WHERE 的区别在于:WHERE 在分组前过滤原始行,HAVING 在分组后过滤聚合结果。因此,涉及聚合函数的条件只能写在 HAVING 中。

连接查询

当数据分布在多张表中时,需要用 JOIN 把相关表连接起来。连接分为内连接、左外连接、右外连接和全外连接等。假设有用户表 users 和订单表 orders,orders 中的 user_id 引用 users 的 id。

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

内连接只返回两表中满足连接条件的行。左外连接返回左表全部行,右表无匹配时以 NULL 填充:

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

这样即使某用户没有订单,他的记录也会出现在结果中,订单字段为空。左连接常用于“找出没有下单的用户”这类需求:

SELECT u.username
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

子查询

子查询是嵌套在另一个查询中的查询,可以出现在 SELECT、FROM、WHERE 等位置。例如查询价格高于平均价格的商品:

SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

再比如,找出下过订单金额超过 1000 元的用户:

SELECT username
FROM users
WHERE id IN (
    SELECT user_id
    FROM orders
    WHERE amount > 1000
);

子查询在逻辑上直观,但部分数据库对相关子查询的优化有限。在实际优化中,很多子查询可以改写为连接查询,以获得更好的执行计划。

联合查询

UNION 可以把多个查询的结果集合并。UNION 会去重,UNION ALL 不去重:

SELECT name FROM products WHERE category = '手机'
UNION
SELECT name FROM products WHERE category = '电脑';

使用 UNION 时,各查询的列数和列类型必须兼容,列名以第一个查询为准。如果能确定各查询结果不会重复,优先使用 UNION ALL 以减少去重开销。

4.5 DCL 与 TCL

DCL 用于权限管理。例如创建一个数据库用户并授权:

CREATE USER 'app_user'@'%' IDENTIFIED BY 'StrongPass123';

GRANT SELECT, INSERT, UPDATE ON blog_db.* TO 'app_user'@'%';

REVOKE DELETE ON blog_db.* FROM 'app_user'@'%';

TCL 用于事务控制。事务是一组要么全部成功、要么全部失败的操作单元。例如转账操作:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

如果中间的某个语句失败,可以执行 ROLLBACK 撤销事务内已做的全部修改。事务的详细特性将在后面的章节展开。

五、索引

5.1 为什么需要索引

索引(Index)是数据库为了加速数据检索而建立的一种数据结构。没有索引时,数据库要根据条件查找数据,通常只能逐行扫描整张表,这称为全表扫描。当数据量达到百万、千万级别时,全表扫描的耗时将无法接受。

索引的作用类似于书籍的目录。一本几百页的书中,如果没有目录,想找到某个章节只能一页页翻;有了目录,就能根据页码直接定位到目标内容。数据库索引通过维护“索引键值到数据位置”的映射,把查询复杂度从线性级降低到对数级,性能提升非常显著。

当然,索引并非没有代价。每建立一个索引,都会额外占用存储空间,并且在插入、更新、删除数据时,数据库需要同步维护索引结构,增加写入开销。因此,索引应当建在高频查询、高区分度的列上,而不是越多越好。

5.2 索引的常见数据结构

关系数据库中,索引最常用也最重要的数据结构是 B+ 树。此外还有哈希索引、全文索引、空间索引等。

哈希索引基于哈希表实现,对等值查询(如 WHERE id = 5)极快,时间复杂度接近 O(1),但它不支持范围查询和排序。因此哈希索引通常用于内存数据库或作为辅助索引。

B+ 树索引是关系数据库索引的主流实现。B+ 树是一棵多路平衡搜索树,具有以下特点:

  • 所有数据记录都存储在叶子节点上,非叶子节点只存储键值和指针,用于导航。
  • 叶子节点之间通过双向链表按顺序相连,支持高效的范围扫描。
  • 树的高度较低,通常在 3 到 4 层,即使数据量巨大,也能保证很少的磁盘 I/O 次数即可定位数据。
  • 插入和删除时通过节点分裂与合并保持平衡,保证了查询性能的稳定。

相比普通的 B 树,B+ 树把更多键值塞进一个节点,扇出更大,树高更低;同时叶子节点形成有序链表,范围查询性能更好。因此 MySQL InnoDB、Oracle、PostgreSQL 等数据库大多采用 B+ 树结构。

5.3 聚簇索引与二级索引

在 MySQL 的 InnoDB 存储引擎中,索引分为聚簇索引(Clustered Index)和二级索引(Secondary Index)。

聚簇索引的叶子节点直接存储整行数据。InnoDB 表的数据按主键顺序组织在聚簇索引中,因此一张表只有一个聚簇索引。如果表没有显式定义主键,InnoDB 会依次尝试使用第一个非空唯一索引作为聚簇索引;如果都没有,则生成一个隐藏的 6 字节行 ID 作为聚簇索引。

二级索引(也叫辅助索引)的叶子节点存储的是索引键值和对应的主键值。当通过二级索引查询时,数据库先在二级索引中找到匹配记录的主键,再回到聚簇索引中查找完整行数据,这个过程称为回表。例如,在 users 表的 username 列上建立二级索引,查询 WHERE username = 'zhangsan' 时,先通过二级索引找到 id,再通过 id 到聚簇索引读取整行。

如果查询的列全部包含在二级索引中,就不再需要回表,这称为覆盖索引。例如建立 (username, email) 的联合索引,查询 SELECT username, email FROM users WHERE username = 'zhangsan' 时,直接读二级索引即可返回结果,性能更好。

5.4 索引的类型

按功能和使用方式划分,索引主要包括以下几类:

  • 普通索引:最基础的索引,没有任何唯一性限制,用于加速查询。
  • 唯一索引:索引列的值必须唯一,但允许 NULL。唯一索引同时承担数据唯一性校验和查询加速两个职责。
  • 主键索引:特殊的唯一索引,不允许 NULL,每张表只能有一个。
  • 联合索引:也叫复合索引,由多个列组成。它遵循最左前缀原则,即查询条件必须包含联合索引最左边的列才能有效利用该索引。
  • 全文索引:用于对长文本进行分词检索,InnoDB 从 MySQL 5.6 起支持全文索引。对于复杂的中文全文检索,更常见的方案是使用 Elasticsearch 等搜索引擎。
  • 空间索引:用于地理位置数据,配合空间函数进行距离计算和区域查询。

创建索引的语法如下:

CREATE INDEX idx_users_email ON users (email);

CREATE UNIQUE INDEX idx_users_username ON users (username);

CREATE INDEX idx_orders_user_time ON orders (user_id, created_at);

5.5 索引优化原则与失效场景

正确使用索引可以大幅提升查询性能,但索引也存在失效的情况。以下是几条重要的索引优化原则:

  1. 选择高区分度列:区分度高的列(如用户 ID、订单号)建索引效果好;区分度极低的列(如性别、是否删除)建索引收益很小,数据库优化器可能直接选择全表扫描。
  2. 联合索引遵循最左前缀:对于 (a, b, c) 的联合索引,WHERE 条件中依次使用 a、a 和 b、a 和 b 和 c 时可以利用索引;只使用 b 或 c 时无法利用。
  3. 避免在索引列上做运算:WHERE YEAR(created_at) = 2024 这样的写法会导致索引失效,应改写为 WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'。
  4. 避免隐式类型转换:如果索引列是字符串类型,查询时传入数字会导致类型转换进而无法使用索引。例如 phone 是 VARCHAR,写成 WHERE phone = 13800138000 可能失效,应写成 WHERE phone = '13800138000'。
  5. 慎用前导通配符:LIKE '%关键词' 无法利用普通索引,LIKE '关键词%' 可以。
  6. 注意 NULL 与范围查询:联合索引中,范围查询(如 >、<、BETWEEN)会中断后续列对索引的使用。例如索引 (status, price, created_at),WHERE status = 'on_sale' AND price > 100 AND created_at > '2024-01-01' 中,created_at 可能无法继续用于索引范围收紧。
  7. 利用覆盖索引避免回表:尽量让查询所需的列都包含在索引中,减少回表带来的额外 I/O。
  8. 定期分析冗余索引:冗余和重复索引不仅浪费存储,还会拖慢写入。应定期通过慢查询日志和数据库自带工具检查并清理。

六、事务与并发控制

6.1 事务的概念

事务(Transaction)是数据库操作的最小逻辑工作单元,由一条或多条 SQL 语句组成。事务要么全部执行成功,要么全部不执行,不允许出现“执行了一半”的中间状态。典型的例子是银行转账:从账户 A 扣款和向账户 B 加款必须作为一个不可分割的整体,否则就会出现钱凭空消失或凭空多出的严重问题。

事务通过 ACID 四个特性来保证数据的正确性和可靠性。

6.2 ACID 特性

原子性(Atomicity):事务中的操作要么全部成功,要么全部失败回滚。即使事务执行到一半时发生崩溃,数据库也必须能恢复到执行前的状态。原子性主要由数据库的撤销日志(Undo Log)机制保证。

一致性(Consistency):事务执行前后,数据库都必须处于一致性状态,即满足所有已定义的约束、触发器和业务规则。例如转账前后,两个账户的总金额必须保持不变。一致性是事务的最终目标,其他三个特性共同支撑它。

隔离性(Isolation):多个事务并发执行时,彼此之间不能相互干扰。一个事务的中间状态对其他事务不可见。隔离性通过锁机制和多版本并发控制(MVCC)来实现。

持久性(Durability):一旦事务提交,其对数据的修改就是永久的,即使随后发生宕机、断电,数据也不会丢失。持久性由重做日志(Redo Log)和定期刷盘机制保证。

6.3 并发问题与隔离级别

如果完全不控制并发事务的执行顺序,可能出现以下典型问题:

  • 脏读(Dirty Read):事务 A 读到了事务 B 尚未提交的修改。如果事务 B 随后回滚,事务 A 读到的是无效的“脏数据”。
  • 不可重复读(Non-Repeatable Read):事务 A 在同一个事务内两次读取同一条记录,结果不同,因为期间事务 B 修改并提交了该记录。
  • 幻读(Phantom Read):事务 A 在同一个事务内两次执行相同范围查询,第二次多出或少了几行,因为期间事务 B 插入或删除了满足条件的记录。

SQL 标准定义了四种事务隔离级别,从低到高依次为:

隔离级别脏读不可重复读幻读
读未提交(READ UNCOMMITTED)可能可能可能
读已提交(READ COMMITTED)不会可能可能
可重复读(REPEATABLE READ)不会不会可能
串行化(SERIALIZABLE)不会不会不会

隔离级别越高,数据一致性越强,但并发性能越低。MySQL 的 InnoDB 默认隔离级别是可重复读;Oracle 和 PostgreSQL 默认是读已提交。MySQL 在可重复读级别下,通过 MVCC 和间隙锁,在很大程度上也解决了幻读问题。

设置隔离级别的语法:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

6.4 锁机制

数据库通过锁(Lock)来控制并发访问。按锁的粒度可分为表锁、行锁和页锁;按锁的性质可分为共享锁和排他锁。

  • 共享锁(Shared Lock,S 锁):允许多个事务同时读取同一资源,读取期间其他事务可以再加共享锁,但不能加排他锁。普通 SELECT 在 InnoDB 默认 MVCC 下不加锁,若要显式加共享锁可使用 SELECT ... LOCK IN SHARE MODE(MySQL 8.0 后为 SELECT ... FOR SHARE)。
  • 排他锁(Exclusive Lock,X 锁):独占资源,加上排他锁后,其他事务既不能读也不能写。UPDATE、DELETE、INSERT 以及 SELECT ... FOR UPDATE 都会加排他锁。

InnoDB 的行锁是基于索引实现的。如果 UPDATE 或 DELETE 的 WHERE 条件没有命中索引,行锁会升级为表锁(实际是锁定所有记录),导致并发度急剧下降,这也是强调索引设计重要性的原因之一。

InnoDB 还引入了间隙锁(Gap Lock)和临键锁(Next-Key Lock)。间隙锁锁定的是索引记录之间的间隙,防止其他事务在该范围内插入新记录,从而在可重复读级别下抑制幻读。临键锁则同时锁定记录本身和它前面的间隙。

锁虽然保证了隔离性,但也可能带来死锁。两个事务相互等待对方持有的锁时,就会发生死锁。数据库通常会自动检测死锁,选择一个代价较小的事务回滚,并向应用抛出死锁错误。应用层应做好重试处理。

6.5 MVCC 多版本并发控制

MVCC(Multi-Version Concurrency Control)是 InnoDB 和 PostgreSQL 等数据库实现高并发读写的核心机制。它的基本思想是:不对读取操作加锁,而是通过保存数据的历史版本,让每个事务都能看到一个一致的快照。

在 InnoDB 中,每行记录都隐式包含两个字段:创建版本号和删除版本号,分别对应创建或删除该行的事务 ID。同时每条普通 SELECT 在事务开始时都会生成一个一致性读视图(Read View),记录当前活跃事务列表。读取数据时,数据库根据行记录的版本号和读视图判断该行对当前事务是否可见:

  • 如果行版本早于当前事务开始且不在活跃列表中,则可见;
  • 如果行版本由当前事务自己创建,则可见;
  • 如果行版本在活跃列表中,表示由未提交事务修改,则不可见,需读取更早的历史版本。

历史版本保存在撤销日志(Undo Log)中,并通过回滚指针串联成版本链。MVCC 使得读操作不必阻塞写操作,写操作也不必阻塞普通的快照读,大幅提升了并发性能。这也解释了为什么在 MySQL 的读已提交和可重复读级别下,普通 SELECT 不会阻塞其他事务的更新。

七、视图、存储过程与触发器

7.1 视图

视图(View)是基于一条或多条 SELECT 语句定义的虚拟表。它本身不存储数据,只是在查询时动态生成结果。视图的作用包括简化复杂查询、隐藏底层表结构、提供安全的数据访问层。

创建视图的语法:

CREATE VIEW active_users AS
SELECT id, username, email
FROM users
WHERE status = 'active';

之后可以像查询普通表一样查询视图:

SELECT * FROM active_users WHERE email LIKE '%@example.com';

视图带来的好处很明显。对于经常重复使用的多表连接,可以封装成视图减少应用层代码量;对于敏感字段,可以通过视图只暴露部分列,限制用户直接访问底层表。不过视图也有局限:部分复杂视图不支持更新,过多嵌套视图可能导致性能下降,因为每次查询视图都会执行底层 SQL。

7.2 存储过程

存储过程(Stored Procedure)是一组预编译的 SQL 语句集合,存储在数据库服务器端,可以接收参数并执行复杂的业务逻辑。它通常包含流程控制语句,如 IF、CASE、LOOP、WHILE 等。

创建存储过程的简单示例:

DELIMITER //

CREATE PROCEDURE transfer_money(
    IN from_user BIGINT,
    IN to_user BIGINT,
    IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
    END;

    START TRANSACTION;

    UPDATE accounts SET balance = balance - amount WHERE user_id = from_user;
    UPDATE accounts SET balance = balance + amount WHERE user_id = to_user;

    COMMIT;
END//

DELIMITER ;

调用存储过程:

CALL transfer_money(1, 2, 100.00);

存储过程的优点是把业务逻辑放到数据库端,减少了应用与数据库之间的网络往返,并且由于过程经过预编译,执行效率较高。但它的缺点也很明显:调试困难、可移植性差、代码维护不便,并且把过多业务逻辑塞进数据库会加重数据库负担。在互联网架构中,存储过程使用得相对较少,多数团队倾向于把业务逻辑放在应用层。

7.3 触发器

触发器(Trigger)是一种特殊的存储过程,它在指定表上发生 INSERT、UPDATE 或 DELETE 操作时自动执行。触发器常用于审计日志、数据校验和级联更新等场景。

创建触发器的示例:当向 orders 表插入订单时,自动记录一条审计日志:

CREATE TRIGGER trg_orders_after_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    INSERT INTO order_audit_log (order_id, action, created_at)
    VALUES (NEW.id, 'INSERT', NOW());
END;

在触发器中,NEW 表示插入或更新后的新行,OLD 表示删除或更新前的旧行。触发器虽然能自动化一些维护工作,但也会增加维护复杂度和隐式行为,过度使用可能让数据变更的原因难以追踪。因此建议仅在确有必要时使用。

八、数据库性能优化

8.1 找出慢查询

性能优化的第一步是找到真正慢的 SQL。主要手段包括:

  • 慢查询日志:数据库可以记录执行时间超过阈值的 SQL。以 MySQL 为例,可通过 slow_query_log 和 long_query_time 参数开启并设置阈值。
  • 性能监控平台:借助数据库自带的性能视图(如 MySQL 的 performance_schema、information_schema,Oracle 的 AWR 报告)或第三方监控工具,持续观察连接数、CPU、磁盘、锁等待和慢查询趋势。
  • 请求链路追踪:在应用层记录每条数据库请求的耗时,定位是哪条业务链路拖慢了整体响应。

8.2 使用执行计划分析 SQL

执行计划是数据库优化器为一条 SQL 选择的实际执行路径。在 MySQL 中,使用 EXPLAIN 关键字可以查看执行计划:

EXPLAIN SELECT u.username, o.order_no
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;

执行计划的关键字段包括:

  • type:访问类型,从好到差大致为 system、const、eq_ref、ref、range、index、ALL。通常应尽量避免 ALL(全表扫描)和 index(全索引扫描)。
  • possible_keys:优化器认为可能用到的索引。
  • key:实际使用的索引。
  • rows:优化器估计需要扫描的行数,数值越小越好。
  • Extra:额外信息,例如 Using index 表示覆盖索引,Using filesort 表示需要额外排序,Using temporary 表示使用了临时表。后两者通常是优化重点。

通过执行计划,可以判断索引是否命中、连接顺序是否合理、是否存在不必要的排序和临时表,从而有针对性地改写 SQL 或调整索引。

8.3 常见优化手段

除了索引优化外,还有以下常用手段:

  • 减少查询返回的数据量:只查询需要的列,不要滥用 SELECT *;使用 LIMIT 限制结果行数。
  • 避免在循环中执行 SQL:把多次单条插入合并为批量插入,把 N+1 查询改写为一次连接或 IN 查询。
  • 合理使用缓存:将热点数据放入 Redis 等缓存,降低数据库读压力。需要注意缓存一致性策略,例如采用旁路缓存、延迟双删等方案。
  • 归档冷数据:把历史数据迁移到归档表或离线存储,控制单表数据量,避免主表无限膨胀。
  • 读写分离:主库负责写入,从库承担大部分读请求,利用复制机制实现水平扩展。
  • 连接池管理:使用数据库连接池复用连接,避免频繁建立和销毁连接的开销,同时合理设置池大小防止连接耗尽。
  • 避免长事务和大事务:长事务会长时间占用锁和回滚段,影响其他事务;应尽量缩短事务时间,必要时拆分为多个小事务。

8.4 垂直拆分、水平拆分与分库分表

当单机数据库达到性能瓶颈时,除了调优,还需要从架构上做拆分。

垂直拆分:按业务模块把不同的表拆分到不同的数据库。例如把用户模块的几张表放在用户库,把订单模块的表放在订单库。垂直拆分降低了单个库的并发压力和耦合度,但跨库查询变得复杂,需要分布式事务或应用层聚合。

水平拆分:把同一张表中的数据按某种规则拆分到多个库或多个表中。常见的分片键包括用户 ID、订单号等。水平拆分可以突破单表数据量上限,提升写入吞吐,但给跨分片查询、排序分页、事务和聚合带来了巨大挑战。

分库分表是复杂度极高的演进,只有在确实无法通过常规优化和硬件扩容解决时才会采用。在决定分库分表之前,应首先确认索引、SQL、缓存、读写分离、归档等基础优化手段已经充分应用。

九、主流数据库对比与选型

9.1 主流关系型数据库

MySQL是目前最流行的开源关系型数据库,凭借性能稳定、社区活跃、部署简单和成本低廉,成为互联网公司的首选。它支持多种存储引擎,其中 InnoDB 最常用,提供事务、行级锁和 MVCC。MySQL 适合大部分在线交易、内容管理和通用业务场景。

PostgreSQL被称为功能最强大的开源关系型数据库。它在标准 SQL 兼容性、复杂查询优化、JSON 支持、窗口函数、扩展机制等方面非常出色,也拥有良好的扩展生态。对于复杂数据处理、地理信息和希望严格遵循 SQL 标准的项目,PostgreSQL 是很好的选择。

Oracle是传统企业级商业数据库的标杆,以极高的稳定性、强大的性能和完善的企业级功能著称,在金融、电信、政府等大型系统中占据重要地位。其缺点是商业授权成本高、运维门槛较高。

SQL Server是微软推出的商业关系数据库,与 Windows 生态和 .NET 技术栈集成良好,提供友好的管理工具(SSMS)和商业智能能力,在企业内部应用中较为常见。

SQLite是轻量级嵌入式数据库,整个数据库就是一个文件,不需要独立服务进程。它非常适合移动应用、桌面软件和原型开发,但不适合高并发写入的大规模服务端场景。

9.2 主流非关系型数据库

Redis是高性能的内存键值数据库,支持字符串、哈希、列表、集合、有序集合等多种数据结构,并提供持久化、主从复制和哨兵、集群等高可用机制。它广泛用于缓存、分布式锁、计数器和排行榜等场景。

MongoDB是文档型数据库,以类似 JSON 的 BSON 格式存储数据,结构灵活、易于水平扩展,适合快速迭代的业务和内容管理类应用。

Elasticsearch是基于 Apache Lucene 的分布式搜索和分析引擎,凭借强大的全文检索和聚合分析能力,已成为日志检索、商品搜索和数据分析的事实标准之一。

Neo4j是图数据库的代表,以节点和边作为核心数据模型,对多跳关系查询有天然优势,适合社交网络、欺诈检测和知识图谱等场景。

9.3 选型建议

数据库选型没有绝对正确的答案,需要综合考虑业务特点、团队技术栈、数据规模、一致性要求、扩展性和运维成本等因素。以下是一些通用建议:

  • 通用业务系统、在线交易:优先选择 MySQL 或 PostgreSQL。团队更熟悉哪个就选哪个,二者都能覆盖绝大多数需求。
  • 需要极致复杂查询和数据清洗:优先考虑 PostgreSQL 或专门的 OLAP 数据库。
  • 热点缓存、临时数据、排行榜:选择 Redis。
  • 文档结构多变、快速原型:选择 MongoDB。
  • 全文搜索、日志分析:选择 Elasticsearch。
  • 深度关联分析、知识图谱:选择图数据库如 Neo4j。
  • 海量监控指标、物联网时序数据:选择时序数据库如 InfluxDB 或 TimescaleDB。

需要特别提醒的是,不要为了技术新颖而盲目引入多种数据库。多数据库混合架构增加了数据同步、一致性和运维的复杂度。只有在明确业务需求确实无法由单一数据库满足时,才考虑引入新的数据库组件。

十、数据库安全与运维基础

10.1 账号与权限管理

账号安全是数据库安全的第一道防线。核心原则包括:

  • 最小权限原则:只授予应用和用户完成任务所需的最小权限。应用账号不应拥有 DROP、ALTER 等高危权限。
  • 禁止使用弱密码:强制高强度密码策略,定期更换。
  • 限制远程访问:数据库服务默认只监听内网地址,通过防火墙和安全组限制来源 IP。
  • 禁用 root 远程登录:为日常操作创建独立账号,root 仅在紧急维护时使用。

10.2 数据备份与恢复

备份是应对数据丢失的最后保障。常见的备份类型包括:

  • 全量备份:备份整个数据库的完整数据。方式简单,恢复时直接从一份备份还原,但备份耗时和空间占用较大。
  • 增量备份:只备份自上次备份以来变化的数据,依赖二进制日志(如 MySQL 的 binlog)实现。增量备份节省空间,但恢复时要先还原全量备份,再逐段应用增量日志,恢复链路较长。
  • 逻辑备份:以 SQL 语句或文本形式导出数据,例如 mysqldump,便于跨版本迁移和部分数据恢复。
  • 物理备份:直接复制数据文件,例如 MySQL 的 XtraBackup,备份和恢复速度快,适合大数据量。

任何备份策略都必须经过恢复演练。一份从未被验证过可恢复的备份,在灾难发生时可能毫无价值。同时,备份文件应异地存放,并设置合理的保留周期。

10.3 监控与告警

数据库运维需要持续关注以下指标:

  • 可用性:服务是否正常运行、主从状态是否健康。
  • 性能:连接数、查询吞吐、响应时间、慢查询数量、缓冲池命中率、锁等待和死锁次数。
  • 容量:磁盘使用率、表空间增长、binlog 和备份空间占用。
  • 安全:登录失败次数、异常 IP 访问、权限变更记录。

一旦指标超过阈值,告警系统应及时通知相关人员,结合执行计划和日志快速定位问题,避免小问题演变成线上事故。

十一、总结

数据库基础知识看似庞杂,但核心主线可以归纳为四条:如何设计合理的数据结构、如何用 SQL 高效地操作数据、如何保证并发场景下的数据正确性、如何在性能不足时科学地优化和扩展。

首先是数据结构设计。掌握关系模型、主键外键、约束和范式,能够把业务需求转化为规范且可扩展的表结构,这是一切工作的起点。其次是 SQL 能力。熟练运用查询、连接、聚合、子查询和索引,才能把数据真正用起来。第三是事务与并发控制。理解 ACID、隔离级别、锁和 MVCC,是编写可靠后端代码的基础。第四是性能优化与架构演进。从索引调优、慢查询分析到读写分离和分库分表,需要循序渐进,切忌一上来就做大拆分。

同时还要了解现代数据生态。关系型数据库之外,Redis、MongoDB、Elasticsearch、时序数据库等非关系型数据库在不同场景下各有优势。一个成熟的架构师应当能够根据业务需求做出合理的数据库选型,并设计出简洁、可维护、可扩展的存储方案。

学习数据库最好的方式,是理论与实践结合。建议读者在理解本文概念的基础上,亲手安装一个 MySQL 或 PostgreSQL,建几张有业务含义的表,写入一定量的测试数据,针对每个知识点写 SQL 验证查询结果,观察执行计划的变化,再逐步尝试事务、索引和优化。当你能用数据库解决真实项目中的数据存储和查询问题时,才算真正掌握了这些基础知识。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐