一、数据库概述
1.1 什么是数据库管理系统(DBMS)
数据库管理系统(Database Management System,简称 DBMS)是用于定义、创建、查询、更新和管理数据库的软件系统。它充当用户或应用程序与数据之间的中介层,提供数据的高效存储、安全访问和并发控制能力。
DBMS 的核心功能包括:
- 数据定义:通过数据定义语言(DDL)定义数据库结构
- 数据操纵:通过数据操纵语言(DML)插入、查询、更新和删除数据
- 数据控制:通过数据控制语言(DCL)管理用户权限
- 事务管理:确保数据的一致性和完整性
- 并发控制:多用户同时访问时的协调机制
- 恢复管理:系统故障后的数据恢复
1.2 关系型 vs 非关系型数据库
| 特性 | 关系型数据库(RDBMS) | 非关系型数据库(NoSQL) |
|---|---|---|
| 数据模型 | 表、行、列(结构化) | 文档、键值、列族、图 |
| Schema | 严格(预定义) | 灵活(无 schema 或动态 schema) |
| 扩展方式 | 垂直扩展为主 | 水平扩展(分布式)为主 |
| ACID | 完整支持 | 通常支持 BASE(最终一致性) |
| 查询语言 | SQL(标准化) | 厂商自定义 API / 类 SQL |
| 适合场景 | 金融、ERP、CRM 等强一致场景 | 大数据、实时分析、IoT、社交网络 |
1.3 常见数据库产品对比
| 特性 | MySQL | PostgreSQL | SQLite | Oracle | SQL Server | MongoDB | Redis |
|---|---|---|---|---|---|---|---|
| 类型 | 关系型 | 关系型(对象关系) | 嵌入式关系型 | 关系型 | 关系型 | 文档型 | 键值/缓存 |
| 开源 | ✅ GPL | ✅ PostgreSQL 许可证 | ✅ Public Domain | ❌ 商业 | ❌ 商业 | ✅ SSPL | ✅ BSD |
| SQL 标准 | 较好 | 优秀(最接近标准) | 部分支持 | 优秀 | 优秀 | 类 SQL 查询 | 不支持 SQL |
| ACID | ✅(InnoDB) | ✅ | ✅ | ✅ | ✅ | ⚠️ 多文档事务 | ❌ |
| 存储引擎 | InnoDB/MyISAM 等 | 单一 | 单一 | 单一 | 单一 | WiredTiger | 多种持久化策略 |
| 并发控制 | MVCC | MVCC | 文件锁 | MVCC | MVCC + 锁 | MVCC | 单线程事件循环 |
| JSON 支持 | 较好(JSON 类型) | 优秀(JSONB) | 有限 | 较好 | 较好 | 原生 BSON | JSON 模块 |
| 主从复制 | ✅ 异步/半同步 | ✅ 流复制 | ❌ | ✅ Data Guard | ✅ Always On | ✅ 副本集 | ✅ 主从/Sentinel/Cluster |
| 全文搜索 | InnoDB 全文索引 | ✅ tsvector | ✅ FTS5 | ✅ Oracle Text | ✅ | ✅ Atlas Search | ✅ RediSearch |
| 典型用户 | Facebook、GitHub | Instagram、Reddit | Android、Chrome | 大型企业 | 企业客户 | eBay、Adobe | Twitter、GitHub |
二、关系模型
2.1 表、行、列
关系模型的核心概念可以用三要素概括:
- 表(Table):也称关系(Relation),是一组具有相同结构的数据集合
- 行(Row):也称元组(Tuple),表示一条完整的记录
- 列(Column):也称属性(Attribute),表示记录的某个特征
-- 创建一个用户表
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入一行数据
INSERT INTO users (user_id, username, email)
VALUES (1, 'alice', 'alice@example.com');
-- 查询
SELECT * FROM users;2.2 主键(Primary Key)
主键是表中唯一标识每一行的一列或多列组合。其特性包括:
- 唯一性:没有两行具有相同的主键值
- 非空:主键列不能为 NULL
- 不变性:主键值应尽量不被修改
-- 单列主键
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
order_date DATE NOT NULL,
total DECIMAL(10,2)
);
-- 复合主键(多列联合)
CREATE TABLE order_items (
order_id INTEGER,
product_id INTEGER,
quantity INTEGER NOT NULL,
price DECIMAL(10,2),
PRIMARY KEY (order_id, product_id)
);
-- 使用序列/自增主键(PostgreSQL)
CREATE TABLE products_postgres (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
-- 使用 AUTO_INCREMENT(MySQL)
CREATE TABLE products_mysql (
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);2.3 外键(Foreign Key)
外键用于建立和维护两个表之间的参照完整性。子表的外键列引用父表的主键列。
-- 创建带外键的表
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL,
dept_id INTEGER,
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
-- 外键约束行为:ON DELETE CASCADE
CREATE TABLE tasks (
task_id INTEGER PRIMARY KEY,
emp_id INTEGER,
title VARCHAR(200),
FOREIGN KEY (emp_id) REFERENCES employees(emp_id)
ON DELETE CASCADE
);
-- 外键约束行为:ON DELETE SET NULL
CREATE TABLE comments (
comment_id INTEGER PRIMARY KEY,
post_id INTEGER,
content TEXT,
FOREIGN KEY (post_id) REFERENCES posts(post_id)
ON DELETE SET NULL
);2.4 索引概念
索引是加速数据检索的数据库结构,类似书籍的目录。它使用额外的存储空间换取更快的查询速度。
-- 单列索引
CREATE INDEX idx_users_email ON users(email);
-- 复合索引(多列)
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
-- 唯一索引(自动由 UNIQUE 约束创建)
CREATE UNIQUE INDEX idx_products_sku
ON products(sku);
-- 部分索引(PostgreSQL)
CREATE INDEX idx_active_users
ON users(email)
WHERE status = 'active';
-- 删除索引
DROP INDEX IF EXISTS idx_users_email;复合索引的最左原则:对于复合索引 (a, b, c),以下查询可以利用索引:
WHERE a = 1✅WHERE a = 1 AND b = 2✅WHERE a = 1 AND b = 2 AND c = 3✅WHERE b = 2❌(未包含最左列 a)WHERE a = 1 AND c = 3⚠️(仅使用 a 列的索引部分)
2.5 关系类型
一对一(1:1)
一个表中的一行对应另一个表中的最多一行。
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE user_profiles (
profile_id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE,
bio TEXT,
avatar_url VARCHAR(500),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);一对多(1:N)
一个表中的一行对应另一个表中的多行。
CREATE TABLE departments (
dept_id INTEGER PRIMARY KEY,
dept_name VARCHAR(100)
);
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name VARCHAR(100),
dept_id INTEGER REFERENCES departments(dept_id)
);多对多(M:N)
需要通过中间表(关联表)来实现。
CREATE TABLE students (
student_id INTEGER PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE courses (
course_id INTEGER PRIMARY KEY,
title VARCHAR(100)
);
-- 中间表
CREATE TABLE enrollments (
student_id INTEGER REFERENCES students(student_id),
course_id INTEGER REFERENCES courses(course_id),
enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id)
);
-- 查询某学生选修的所有课程
SELECT s.name, c.title, e.enrolled_at
FROM students s
JOIN enrollments e ON s.student_id = e.student_id
JOIN courses c ON e.course_id = c.course_id
WHERE s.student_id = 1;2.6 ER 图与关系建模
实体-关系(ER)图是数据建模的图形化工具:
- 实体(Entity):矩形表示,如
用户、订单 - 属性(Attribute):椭圆表示,如
用户名、订单金额 - 关系(Relationship):菱形表示,连接相关实体
关系建模步骤:
- 概念设计:分析业务需求,识别实体和关系,绘制 ER 图
- 逻辑设计:将 ER 图转换为关系模式(表结构)
- 物理设计:确定数据类型、索引策略、分区方案
-- 一个典型的电商数据模型示例
-- 用户
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE
);
-- 产品
CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
name VARCHAR(200),
price DECIMAL(10,2),
stock_qty INTEGER DEFAULT 0
);
-- 订单
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(customer_id),
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'pending'
);
-- 订单明细(多对多关系表)
CREATE TABLE order_details (
order_id INTEGER REFERENCES orders(order_id),
product_id INTEGER REFERENCES products(product_id),
quantity INTEGER NOT NULL,
unit_price DECIMAL(10,2),
PRIMARY KEY (order_id, product_id)
);三、SQL 语言
3.1 DDL(数据定义语言)
DDL 用于定义数据库结构:创建、修改、删除数据库对象。
-- CREATE:创建数据库、表、视图
CREATE DATABASE shop;
CREATE TABLE categories (
cat_id INTEGER PRIMARY KEY,
cat_name VARCHAR(100) NOT NULL
);
-- ALTER:修改表结构
ALTER TABLE categories ADD COLUMN description TEXT;
ALTER TABLE categories ALTER COLUMN cat_name TYPE VARCHAR(200);
ALTER TABLE categories DROP COLUMN description;
ALTER TABLE categories RENAME TO product_categories;
-- DROP:删除对象
DROP TABLE IF EXISTS temp_data;
DROP DATABASE IF EXISTS old_shop;3.2 DML(数据操纵语言)
DML 用于操作表中的数据:增、删、改、查。
示例数据准备
-- 创建示例表
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name VARCHAR(50),
dept VARCHAR(30),
salary DECIMAL(10,2),
hire_date DATE
);
-- 插入数据
INSERT INTO employees VALUES
(1, '张三', '技术部', 15000.00, '2020-03-15'),
(2, '李四', '市场部', 12000.00, '2021-06-01'),
(3, '王五', '技术部', 18000.00, '2019-01-10'),
(4, '赵六', '人事部', 10000.00, '2022-09-20'),
(5, '孙七', '市场部', 13000.00, '2020-11-05'),
(6, '周八', '技术部', 22000.00, '2018-05-12'),
(7, '吴九', '财务部', 11000.00, '2023-02-28');SELECT 查询
-- 基本查询
SELECT * FROM employees;
-- 投影:选择特定列
SELECT name, salary FROM employees;
-- 别名
SELECT name AS 姓名, salary AS 薪资 FROM employees;
-- 条件过滤
SELECT * FROM employees
WHERE dept = '技术部' AND salary > 15000;
-- 排序
SELECT * FROM employees
ORDER BY salary DESC;
-- 分页(PostgreSQL / MySQL)
SELECT * FROM employees
ORDER BY id
LIMIT 3 OFFSET 0; -- 第1页
SELECT * FROM employees
ORDER BY id
LIMIT 3 OFFSET 3; -- 第2页INSERT 插入
-- 单行插入
INSERT INTO employees (id, name, dept, salary, hire_date)
VALUES (8, '陈十', '技术部', 16000.00, '2024-01-15');
-- 多行插入
INSERT INTO employees VALUES
(9, '林十一', '财务部', 11500.00, '2024-03-01'),
(10, '黄十二', '市场部', 12500.00, '2024-04-10');
-- 从查询结果插入
CREATE TABLE high_salary AS
SELECT * FROM employees WHERE salary > 15000;UPDATE 更新
-- 更新单行
UPDATE employees
SET salary = 16000.00
WHERE id = 4;
-- 更新多行
UPDATE employees
SET salary = salary * 1.10
WHERE dept = '技术部';
-- 使用子查询更新
UPDATE employees
SET salary = (
SELECT AVG(salary) FROM employees
)
WHERE id = 1;DELETE 删除
-- 删除特定行
DELETE FROM employees
WHERE id = 10;
-- 删除所有行
DELETE FROM employees;
-- TRUNCATE(速度更快,不可回滚)
TRUNCATE TABLE employees;3.3 DCL(数据控制语言)
-- 创建用户(PostgreSQL)
CREATE USER app_user WITH PASSWORD 'secure_password';
CREATE USER analyst WITH PASSWORD 'read_only_pass';
-- GRANT:授予权限(PostgreSQL)
GRANT SELECT, INSERT, UPDATE ON employees TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst;
GRANT USAGE ON SCHEMA public TO app_user;
-- GRANT(MySQL)
GRANT SELECT, INSERT ON shop.* TO 'app_user'@'localhost';
GRANT ALL PRIVILEGES ON shop.* TO 'admin'@'localhost';
-- REVOKE:撤销权限
REVOKE DELETE ON employees FROM app_user;
REVOKE ALL PRIVILEGES ON shop.* FROM 'app_user'@'localhost';
-- 刷新权限(MySQL)
FLUSH PRIVILEGES;3.4 JOIN 详解
JOIN 用于从多个表中组合数据。以下用两个表演示所有 JOIN 类型。
-- 准备数据
CREATE TABLE customers (
cid INTEGER PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE orders (
oid INTEGER PRIMARY KEY,
cid INTEGER,
product VARCHAR(50),
amount DECIMAL(10,2)
);
INSERT INTO customers VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');
INSERT INTO orders VALUES
(101, 1, 'Laptop', 5000),
(102, 1, 'Mouse', 100),
(103, 2, 'Keyboard', 300),
(104, NULL, 'Monitor', 1500); -- 无客户的订单INNER JOIN(内连接)
返回两表中匹配的行。
-- 只返回有订单的客户
SELECT c.name, o.product, o.amount
FROM customers c
INNER JOIN orders o ON c.cid = o.cid;
-- 结果:
-- Alice | Laptop | 5000
-- Alice | Mouse | 100
-- Bob | Keyboard | 300LEFT JOIN(左外连接)
返回左表所有行,右表无匹配则返回 NULL。
-- 返回所有客户及其订单(Charlie 无订单也出现)
SELECT c.name, o.product, o.amount
FROM customers c
LEFT JOIN orders o ON c.cid = o.cid;
-- 结果:
-- Alice | Laptop | 5000
-- Alice | Mouse | 100
-- Bob | Keyboard | 300
-- Charlie | NULL | NULLRIGHT JOIN(右外连接)
返回右表所有行,左表无匹配则返回 NULL。
-- 返回所有订单,无客户的订单也出现
SELECT c.name, o.product, o.amount
FROM customers c
RIGHT JOIN orders o ON c.cid = o.cid;
-- 结果:
-- Alice | Laptop | 5000
-- Alice | Mouse | 100
-- Bob | Keyboard | 300
-- NULL | Monitor | 1500FULL JOIN(全外连接)
返回两表所有行,无匹配部分为 NULL。
SELECT c.name, o.product, o.amount
FROM customers c
FULL JOIN orders o ON c.cid = o.cid;
-- 结果:
-- Alice | Laptop | 5000
-- Alice | Mouse | 100
-- Bob | Keyboard | 300
-- Charlie | NULL | NULL
-- NULL | Monitor | 1500CROSS JOIN(交叉连接)
返回两表的笛卡尔积,即所有可能的行组合。
SELECT c.name, o.product
FROM customers c
CROSS JOIN orders o;
-- 3 个客户 × 4 个订单 = 12 行结果3.5 子查询与 CTE
子查询
-- WHERE 子句中的子查询
SELECT name, salary
FROM employees
WHERE salary > (
SELECT AVG(salary) FROM employees
);
-- FROM 子句中的子查询(派生表)
SELECT dept, avg_salary
FROM (
SELECT dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept
) AS dept_stats
WHERE avg_salary > 13000;
-- 关联子查询
SELECT e1.name, e1.salary, e1.dept
FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.dept = e1.dept
);CTE(公用表表达式,WITH 语句)
CTE 提供更清晰、可重用的子查询定义。
-- 基本 CTE
WITH dept_avg AS (
SELECT dept, AVG(salary) AS avg_sal
FROM employees
GROUP BY dept
)
SELECT e.name, e.dept, e.salary, d.avg_sal
FROM employees e
JOIN dept_avg d ON e.dept = d.dept;
-- 多 CTE
WITH
dept_avg AS (
SELECT dept, AVG(salary) AS avg_sal
FROM employees
GROUP BY dept
),
top_earners AS (
SELECT name, dept, salary
FROM employees
WHERE salary > 15000
)
SELECT t.name, t.dept, t.salary, d.avg_sal
FROM top_earners t
JOIN dept_avg d ON t.dept = d.dept
ORDER BY t.salary DESC;
-- 递归 CTE(PostgreSQL):生成数字序列
WITH RECURSIVE numbers AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM numbers WHERE n < 10
)
SELECT * FROM numbers;3.6 聚合函数与 GROUP BY
-- 常用聚合函数
SELECT
COUNT(*) AS 总人数,
AVG(salary) AS 平均薪资,
MAX(salary) AS 最高薪资,
MIN(salary) AS 最低薪资,
SUM(salary) AS 薪资总额,
COUNT(DISTINCT dept) AS 部门数量
FROM employees;
-- GROUP BY 分组
SELECT
dept,
COUNT(*) AS 人数,
AVG(salary) AS 平均薪资,
MAX(salary) AS 部门最高薪
FROM employees
GROUP BY dept;
-- HAVING 过滤分组(WHERE 过滤行,HAVING 过滤组)
SELECT
dept,
AVG(salary) AS avg_salary
FROM employees
GROUP BY dept
HAVING AVG(salary) > 13000;
-- GROUPING SETS(PostgreSQL)
SELECT dept, AVG(salary) AS avg_salary
FROM employees
GROUP BY GROUPING SETS ((dept), ());
-- 窗口函数:ROW_NUMBER
SELECT
name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rank
FROM employees;四、范式理论
范式(Normal Form)是数据库设计中用于减少数据冗余、避免数据异常的理论基础。
4.1 第一范式(1NF):列原子性
规则:每个列都必须包含原子值(不可再分),每列的值是单一值。
-- ❌ 违反 1NF:phone 字段包含多个值
CREATE TABLE students_bad (
id INTEGER PRIMARY KEY,
name VARCHAR(50),
phone VARCHAR(100) -- '13800138000, 13900139000'
);
-- ✅ 符合 1NF:将电话拆分为多行或独立表
CREATE TABLE students_good (
id INTEGER PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE student_phones (
student_id INTEGER REFERENCES students_good(id),
phone VARCHAR(20),
PRIMARY KEY (student_id, phone)
);
-- 或是在同一行中用多列(有限情况)
CREATE TABLE students_alt (
id INTEGER PRIMARY KEY,
name VARCHAR(50),
phone_pri VARCHAR(20), -- 主要电话
phone_sec VARCHAR(20) -- 备用电话
);4.2 第二范式(2NF):消除部分依赖
规则:满足 1NF,且每个非主键列完全依赖于整个主键(而非主键的一部分)。
适用场景:复合主键表。
-- ❌ 违反 2NF:复合主键 (student_id, course_id)
-- course_name 只依赖于 course_id(部分依赖),而非整个主键
CREATE TABLE enrollments_bad (
student_id INTEGER,
course_id INTEGER,
course_name VARCHAR(100), -- ❌ 部分依赖
student_name VARCHAR(50), -- ❌ 部分依赖
grade CHAR(1),
PRIMARY KEY (student_id, course_id)
);
-- ✅ 符合 2NF:拆分表
-- 表1:选课信息(完全依赖复合主键)
CREATE TABLE enrollments (
student_id INTEGER,
course_id INTEGER,
grade CHAR(1),
PRIMARY KEY (student_id, course_id)
);
-- 表2:课程信息
CREATE TABLE courses (
course_id INTEGER PRIMARY KEY,
course_name VARCHAR(100)
);
-- 表3:学生信息
CREATE TABLE students (
student_id INTEGER PRIMARY KEY,
student_name VARCHAR(50)
);4.3 第三范式(3NF):消除传递依赖
规则:满足 2NF,且非主键列不依赖于其他非主键列(禁止传递依赖)。
-- ❌ 违反 3NF:dept_location 依赖于 dept_name,而 dept_name 依赖于 emp_id
-- 存在传递依赖:emp_id → dept_name → dept_location
CREATE TABLE employees_bad (
emp_id INTEGER PRIMARY KEY,
name VARCHAR(50),
dept_name VARCHAR(50),
dept_location VARCHAR(100) -- ❌ 传递依赖
);
-- ✅ 符合 3NF:拆分部门信息
CREATE TABLE employees (
emp_id INTEGER PRIMARY KEY,
name VARCHAR(50),
dept_id INTEGER REFERENCES departments(dept_id)
);
CREATE TABLE departments (
dept_id INTEGER PRIMARY KEY,
dept_name VARCHAR(50),
location VARCHAR(100)
);4.4 BC 范式(BCNF)
规则:满足 3NF,且每个决定因素(函数依赖的左侧)都是超键。BCNF 是 3NF 的强化版本。
-- 场景:一个学生可以参加多个项目,每个项目有多个学生,
-- 每个学生在一个项目中只有一个导师,但一个导师可以指导多个项目
-- ❌ 违反 BCNF:假设 (student, project) 是主键
-- 依赖:student + project → mentor(主键决定所有,没问题)
-- 但还有:mentor → project(导师决定项目,这不是超键!)
CREATE TABLE mentorship_bad (
student VARCHAR(50),
project VARCHAR(50),
mentor VARCHAR(50),
PRIMARY KEY (student, project)
);
-- ✅ 符合 BCNF:拆分为两个表
CREATE TABLE mentorship (
student VARCHAR(50),
mentor VARCHAR(50),
project VARCHAR(50),
PRIMARY KEY (student, project),
UNIQUE (mentor, project) -- mentor 决定 project 的约束显式化
);
-- 或拆分:
CREATE TABLE student_mentor (
student VARCHAR(50) PRIMARY KEY,
mentor VARCHAR(50)
);
CREATE TABLE mentor_project (
mentor VARCHAR(50),
project VARCHAR(50),
PRIMARY KEY (mentor, project)
);4.5 反范式化:何时打破范式
过高的范式化会导致过多表连接,影响查询性能。反范式化(Denormalization)是有意识地在某些场景下降低范式等级,以换取查询性能。
适合反范式化的场景:
- 读密集型应用:频繁 JOIN 的大表,引入冗余字段减少 JOIN
- 数据仓库 / OLAP:分析型查询需要大量聚合,星型模型本身就是反范式化设计
- 缓存层:如将用户名称冗余存储在订单表中,避免每次查询都 JOIN 用户表
-- 反范式化示例:订单表冗余存储客户名
-- 范式化设计需要 JOIN:
SELECT o.order_id, c.name, o.total
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;
-- 反范式化:orders 表直接存储 customer_name
CREATE TABLE orders_denorm (
order_id INTEGER PRIMARY KEY,
customer_id INTEGER,
customer_name VARCHAR(100), -- 🚀 冗余字段,避免 JOIN
total DECIMAL(10,2)
);
-- 代价:customer 改名时需要同步更新 orders 表反范式化的代价:
- 更新异常:冗余数据需要同步更新
- 存储膨胀:更多的磁盘空间
- 数据不一致风险:更新遗漏导致数据矛盾
五、事务(Transaction)
5.1 ACID 四大特性
事务是数据库执行的一个工作单元,要么全部成功,要么全部失败。ACID 是事务的四大基本特性:
| 特性 | 全称 | 含义 | 类比 |
|---|---|---|---|
| A | Atomicity(原子性) | 事务不可分割,要么全做,要么全不做 | 银行转账:扣钱和加钱必须同时发生 |
| C | Consistency(一致性) | 事务前后数据符合所有约束规则 | 转账前后总金额不变 |
| I | Isolation(隔离性) | 并发事务互不干扰 | 两个转账同时进行,互不影响 |
| D | Durability(持久性) | 提交的事务永久保存 | 转账成功即使断电也不会丢失 |
-- 事务示例:银行转账
BEGIN;
-- 从 A 账户扣款
UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 'A';
-- 向 B 账户加款
UPDATE accounts
SET balance = balance + 1000
WHERE account_id = 'B';
-- 如果其中任意一步失败:
ROLLBACK; -- 撤销所有更改
-- 如果全部成功:
COMMIT; -- 永久保存更改一致性检查:可以在事务中添加显式约束验证
-- 使用保存点(SAVEPOINT)实现部分回滚
BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 'A';
SAVEPOINT after_deduction;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 'B';
-- 检测到目标账户异常(如被冻结)
ROLLBACK TO SAVEPOINT after_deduction;
-- A 的扣款被撤销,回到事务开始前的状态
-- 修复后重试...
COMMIT;5.2 隔离级别
SQL 标准定义了四种隔离级别,隔离强度从低到高:
-- 设置隔离级别(PostgreSQL)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置隔离级别(MySQL)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最高 |
| READ COMMITTED | ❌ 避免 | ✅ 可能 | ✅ 可能 | ↑ |
| REPEATABLE READ | ❌ 避免 | ❌ 避免 | ✅ 可能 | ↓ |
| SERIALIZABLE | ❌ 避免 | ❌ 避免 | ❌ 避免 | 最低 |
READ UNCOMMITTED(读未提交)
允许读取其他事务未提交的数据。这是最危险的隔离级别。
-- 事务 A(隔离级别:READ UNCOMMITTED)| 事务 B
BEGIN; |
UPDATE accounts SET balance = 5000 |
WHERE id = 1; -- 未提交 |
| BEGIN;
| SELECT balance FROM accounts WHERE id = 1;
| -- 读到 5000(脏读!)
ROLLBACK; -- A 回滚,balance 回到 1000 |READ COMMITTED(读已提交)
只能读取其他事务已提交的数据。PostgreSQL 的默认隔离级别。
-- 事务 A(隔离级别:READ COMMITTED) | 事务 B
BEGIN; |
UPDATE accounts SET balance = 5000 |
WHERE id = 1; |
| BEGIN;
| SELECT balance FROM accounts WHERE id = 1;
| -- 读到 1000(未提交的数据不可见)
COMMIT; |
| SELECT balance FROM accounts WHERE id = 1;
| -- 读到 5000(不可重复读!同一事务中两次读不同)REPEATABLE READ(可重复读)
同一事务中多次读取同一数据,结果一致。MySQL InnoDB 的默认隔离级别。
-- 事务 A(隔离级别:REPEATABLE READ) | 事务 B
BEGIN; |
SELECT * FROM products WHERE id = 1; |
-- 读到 price = 100 |
| BEGIN;
| UPDATE products SET price = 200 WHERE id = 1;
| COMMIT;
SELECT * FROM products WHERE id = 1; |
-- 仍然读到 price = 100(可重复读) |
| -- 但可能出现幻读:
| INSERT INTO products (id, price) VALUES (10, 300);
| COMMIT;
SELECT * FROM products WHERE price > 250; |
-- 可能读到新插入的行(幻读) |SERIALIZABLE(可序列化)
最高的隔离级别,事务完全串行执行,通过强制事务排序避免所有并发问题。
-- 设置最高隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 或 PostgreSQL 的 SSI(可序列化快照隔离)
BEGIN ISOLATION LEVEL SERIALIZABLE;
UPDATE inventory SET qty = qty - 1 WHERE product_id = 1;
UPDATE orders SET status = 'confirmed' WHERE order_id = 100;
COMMIT;
-- 如果并发冲突,PostgreSQL 会抛出:
-- ERROR: could not serialize access due to read/write dependencies5.3 脏读、不可重复读、幻读详解
脏读(Dirty Read)
读取到其他事务未提交的数据,如果该事务回滚,则读取的是"脏数据"。
时间线:
T1: BEGIN
T1: UPDATE product SET price = 0 WHERE id = 1 (未提交)
T2: BEGIN
T2: SELECT price FROM product WHERE id = 1 → 读到 0 (脏读)
T1: ROLLBACK (实际 price 应还是 100)
T2: 基于 price=0 做了错误决策...脏读只在 READ UNCOMMITTED 下发生,其他三种隔离级别都避免脏读。
不可重复读(Non-repeatable Read)
同一事务内,两次读取同一行数据得到不同结果。原因是其他事务在两次读取之间修改了该行并提交。
时间线:
T1: BEGIN
T1: SELECT balance FROM account WHERE id = 1 → 1000
T2: UPDATE account SET balance = 2000 WHERE id = 1; COMMIT
T1: SELECT balance FROM account WHERE id = 1 → 2000 (不一样了!)
T1: COMMIT不可重复读在 READ COMMITTED 下可能发生,REPEATABLE READ 及以上级别避免。
幻读(Phantom Read)
同一事务内,两次执行相同范围查询,第二次返回了新的行(幻影行)。原因是其他事务插入了符合条件的新行。
时间线:
T1: BEGIN
T1: SELECT * FROM orders WHERE amount > 100 → 返回 2 行
T2: INSERT INTO orders (id, amount) VALUES (3, 200); COMMIT
T1: SELECT * FROM orders WHERE amount > 100 → 返回 3 行 (多了一行!)
T1: COMMIT幻读在 REPEATABLE READ 下可能发生,SERIALIZABLE 级别通过锁或谓词锁避免。
5.4 MVCC(多版本并发控制)
MVCC 是现代关系数据库实现高并发事务的核心技术。它不是直接加锁阻塞读取,而是通过数据的多版本让读写互不阻塞。
核心原理
-- MVCC 的每行数据在内部包含两个系统列(以 PostgreSQL 为例):
-- xmin:创建该行版本的事务 ID
-- xmax:删除/更新该行版本的事务 ID
-- 创建表并插入数据
CREATE TABLE items (
id INTEGER PRIMARY KEY,
name TEXT,
price NUMERIC
);
INSERT INTO items VALUES (1, 'Widget', 100);MVCC 下的 UPDATE 本质是 INSERT + DELETE 标记:
-- 事务 T1 执行更新
BEGIN; -- T1 的 xid = 100
UPDATE items SET price = 120 WHERE id = 1;
-- 物理操作:
-- 1. 将原行标记为已删除(xmax = 100)
-- 2. 插入新行(xmin = 100),price = 120
COMMIT;
-- 事务 T2 并发读取(T2 的 xid = 101)
BEGIN;
SELECT * FROM items WHERE id = 1;
-- 根据快照可见性规则:
-- 如果 T2 启动时 T1 尚未提交 → 看到旧版本(price = 100)
-- 如果 T2 启动时 T1 已提交 → 看到新版本(price = 120)
COMMIT;可见性规则
MVCC 的快照(Snapshot)记录事务启动时所有活跃的事务 ID。数据的可见性判断规则:
- 行版本可见:
xmin的事务已提交,且在快照创建时该事务已结束 - 行版本不可见:
xmin的事务在快照创建时仍活跃,或xmax的事务已提交
MVCC 的优势与代价
| 优势 | 代价 |
|---|---|
| 读操作从不阻塞写操作 | 旧版本数据产生存储开销 |
| 写操作仅在高冲突时阻塞写 | 需要定期清理死元组(VACUUM) |
| 每个事务看到一致的快照 | 事务 ID 可能耗尽(需冻结处理) |
| 不需要复杂的锁机制 | 索引维护成本增加 |
-- PostgreSQL 查看事务和死元组信息
SELECT
schemaname,
tablename,
n_dead_tup,
n_live_tup,
last_autovacuum
FROM pg_stat_user_tables
WHERE tablename = 'items';
-- 手动清理死元组
VACUUM items;
VACUUM FULL items; -- 更彻底但会锁表六、基本优化
6.1 EXPLAIN 执行计划
执行计划是数据库优化器生成的查询操作步骤。通过分析执行计划可以定位性能瓶颈。
-- PostgreSQL 查看执行计划
EXPLAIN SELECT * FROM employees WHERE dept = '技术部';
-- 显示实际执行时间和行数
EXPLAIN ANALYZE SELECT * FROM employees WHERE dept = '技术部';
-- MySQL
EXPLAIN SELECT * FROM employees WHERE dept = '技术部'\G常见执行计划节点及解读:
| 节点类型 | 含义 | 优化方向 |
|---|---|---|
| Seq Scan | 全表顺序扫描 | 添加索引 |
| Index Scan | 索引扫描(回表) | 检查覆盖索引 |
| Index Only Scan | 仅索引扫描,无需回表 | 理想状态 |
| Bitmap Heap Scan | 位图扫描后回表 | 适合大量匹配 |
| Nested Loop | 嵌套循环连接 | 小表驱动大表 |
| Hash Join | 哈希连接 | 适合等值连接 |
| Merge Join | 排序合并连接 | 已排序数据 |
-- 示例:比较有索引和无索引的执行计划
-- 无索引时
EXPLAIN ANALYZE SELECT * FROM employees WHERE salary > 15000;
-- Seq Scan on employees (cost=0.00..35.50 rows=3 width=120)
-- 实际执行时间: 0.523 ms
-- 创建索引后
CREATE INDEX idx_salary ON employees(salary);
EXPLAIN ANALYZE SELECT * FROM employees WHERE salary > 15000;
-- Index Scan using idx_salary on employees (cost=0.28..8.29 rows=3 width=120)
-- 实际执行时间: 0.089 ms ✅6.2 索引类型
B+树索引
最常用的索引类型,底层使用平衡多路搜索树(B+树)。
- 特点:支持等值查询和范围查询,有序
- 适用场景:
=、>、<、>=、<=、BETWEEN、LIKE 'prefix%' - MySQL InnoDB:默认索引类型
B+树结构示意图:
[50, 80]
/ | \
[10,30,40] [60,70] [90,100]
/ | \ | \ | \
数据 数据 数据 数据 数据 数据 数据
内部节点只存键值,不存数据 → 树更矮 → IO更少
叶子节点通过链表相连 → 范围查询高效-- B+树索引示例
CREATE INDEX idx_employees_salary ON employees(salary);
-- 范围查询利用 B+树的有序性
SELECT * FROM employees
WHERE salary BETWEEN 10000 AND 20000
ORDER BY salary;哈希索引
基于哈希表实现,只支持等值查询。
| 特性 | B+树 | 哈希 |
|---|---|---|
| 等值查询 | O(log n) | O(1) |
| 范围查询 | ✅ 高效 | ❌ 不支持 |
| 排序 | ✅ 有序 | ❌ 无序 |
| LIKE 查询 | ✅ 前缀匹配 | ❌ 不支持 |
-- MySQL 支持显式指定哈希索引(Memory 引擎)
CREATE TABLE lookup (
id INTEGER PRIMARY KEY,
code VARCHAR(10)
) ENGINE=MEMORY;
CREATE INDEX idx_code USING HASH ON lookup(code);
-- PostgreSQL 的哈希索引
CREATE INDEX idx_employees_id_hash ON employees USING HASH (id);全文索引
用于对大量文本内容进行关键词搜索。
-- MySQL 全文索引
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title VARCHAR(200),
body TEXT,
FULLTEXT INDEX ft_idx (title, body)
);
-- 全文搜索查询
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('数据库 性能 优化' IN NATURAL LANGUAGE MODE);
-- PostgreSQL 全文搜索(使用 tsvector + tsquery)
CREATE TABLE documents (
id INTEGER PRIMARY KEY,
title TEXT,
body TEXT
);
-- 生成 tsvector
SELECT to_tsvector('english', 'Database performance optimization');
-- 创建 GIN 索引
CREATE INDEX idx_documents_fts
ON documents
USING GIN (to_tsvector('english', title || ' ' || body));
-- 全文查询
SELECT id, title
FROM documents
WHERE to_tsvector('english', title || ' ' || body)
@@ to_tsquery('english', 'database & performance');6.3 慢查询
慢查询是执行时间超过阈值的 SQL 语句。识别和优化慢查询是数据库性能调优的核心工作。
开启慢查询日志
-- MySQL 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒的查询
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- PostgreSQL 开启慢查询
-- 在 postgresql.conf 中配置:
-- log_min_duration_statement = 1000 -- 超过 1000ms 的查询常见慢查询优化技巧
技巧 1:避免 SELECT *
-- ❌ 慢:读取所有列,可能回表
SELECT * FROM employees;
-- ✅ 快:只取需要的列,可能只用索引
SELECT id, name, salary FROM employees;技巧 2:合理使用索引
-- ❌ 函数在列上,无法使用索引
SELECT * FROM employees
WHERE LOWER(email) = 'alice@example.com';
-- ✅ 改为等值比较
SELECT * FROM employees
WHERE email = 'Alice@Example.com';
-- 或使用表达式索引(PostgreSQL)
CREATE INDEX idx_email_lower ON employees(LOWER(email));技巧 3:优化 JOIN 顺序
-- 小表驱动大表:先将数据量小的结果集作为驱动表
-- 假设 departments 有 10 行,employees 有 100 万行
-- ✅ 高效:先筛选 departments,再 JOIN employees
SELECT e.name, d.dept_name
FROM departments d
JOIN employees e ON e.dept_id = d.dept_id
WHERE d.dept_name = '技术部';技巧 4:避免隐式类型转换
-- ❌ 列是 INTEGER,传入字符串 → 隐式转换,无法使用索引
SELECT * FROM employees WHERE id = '100'; -- 隐式转换
-- ✅ 正确匹配类型
SELECT * FROM employees WHERE id = 100;技巧 5:分解大查询
-- ❌ 一个复杂的子查询
SELECT *
FROM employees
WHERE dept_id IN (
SELECT dept_id
FROM departments
WHERE location = '北京'
)
AND salary > (
SELECT AVG(salary)
FROM employees
WHERE dept_id IN (
SELECT dept_id
FROM departments
WHERE location = '北京'
)
);
-- ✅ 使用 CTE 分解,更易优化
WITH beijing_depts AS (
SELECT dept_id FROM departments WHERE location = '北京'
),
beijing_avg AS (
SELECT AVG(salary) AS avg_sal
FROM employees
WHERE dept_id IN (SELECT dept_id FROM beijing_depts)
)
SELECT e.*
FROM employees e
JOIN beijing_depts d ON e.dept_id = d.dept_id
WHERE e.salary > (SELECT avg_sal FROM beijing_avg);技巧 6:分页优化
-- ❌ 传统分页:OFFSET 越大越慢
SELECT * FROM employees
ORDER BY id
LIMIT 20 OFFSET 100000; -- 需要扫描 100020 行
-- ✅ 游标分页 / 键集分页
SELECT * FROM employees
WHERE id > 100000 -- 记录上一页最后一条的 id
ORDER BY id
LIMIT 20;慢查询诊断流程
- 启用慢查询日志,设置合理的阈值
- 收集慢查询 SQL,按频率和执行时间排序
- 使用 EXPLAIN ANALYZE 分析执行计划
- 定位瓶颈:全表扫描?排序?JOIN 效率低?
- 针对性优化:加索引、改写 SQL、调整参数
- 验证效果:再次 EXPLAIN,确认成本降低
-- PostgreSQL 查看当前正在运行的慢查询
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC
LIMIT 10;
-- MySQL 查看正在运行的线程
SHOW FULL PROCESSLIST;总结
本文从数据库基础概念出发,系统性地介绍了关系模型的表、键、索引和关系设计,通过完整的 SQL 代码示例涵盖了 DDL、DML、DCL 以及 JOIN、子查询、CTE 和聚合等核心语法。范式理论部分演示了从 1NF 到 BCNF 的规范化过程以及反范式化的权衡,事务一章深入剖析了 ACID 特性、四种隔离级别及其产生的并发问题,并阐释了 MVCC 的实现原理。最后,通过 EXPLAIN 执行计划分析、索引选型和慢查询优化实战,建立了从理论到实践的完整知识体系。
掌握这些基础是深入理解和使用任何数据库系统的起点,也是后端开发工程师的必备技能。