数据库基础与关系模型:从 SQL 到事务

2024-09-11 后端

一、数据库概述

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):菱形表示,连接相关实体

关系建模步骤:

  1. 概念设计:分析业务需求,识别实体和关系,绘制 ER 图
  2. 逻辑设计:将 ER 图转换为关系模式(表结构)
  3. 物理设计:确定数据类型、索引策略、分区方案

三、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 | 300

LEFT 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     | NULL

RIGHT 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  | 1500

FULL 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  | 1500

CROSS 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 提供更清晰、可重用的子查询定义。

3.6 聚合函数与 GROUP BY


四、范式理论

范式(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,且每个非主键列完全依赖于整个主键(而非主键的一部分)。

适用场景:复合主键表。

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 的强化版本。

4.5 反范式化:何时打破范式

过高的范式化会导致过多表连接,影响查询性能。反范式化(Denormalization)是有意识地在某些场景下降低范式等级,以换取查询性能。

适合反范式化的场景:

  1. 读密集型应用:频繁 JOIN 的大表,引入冗余字段减少 JOIN
  2. 数据仓库 / OLAP:分析型查询需要大量聚合,星型模型本身就是反范式化设计
  3. 缓存层:如将用户名称冗余存储在订单表中,避免每次查询都 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 dependencies

5.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);

全文索引

用于对大量文本内容进行关键词搜索。

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:分解大查询
技巧 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;

慢查询诊断流程

  1. 启用慢查询日志,设置合理的阈值
  2. 收集慢查询 SQL,按频率和执行时间排序
  3. 使用 EXPLAIN ANALYZE 分析执行计划
  4. 定位瓶颈:全表扫描?排序?JOIN 效率低?
  5. 针对性优化:加索引、改写 SQL、调整参数
  6. 验证效果:再次 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 执行计划分析、索引选型和慢查询优化实战,建立了从理论到实践的完整知识体系。

掌握这些基础是深入理解和使用任何数据库系统的起点,也是后端开发工程师的必备技能。