CS 教程 · 第 10 章

数据库基础

SQL / MySQL / PostgreSQL

数据是数字世界的石油,数据库是储存和提炼石油的炼油厂。

从个人博客到电商平台,从社交网络到金融系统,几乎所有应用都依赖数据库。本章将带你理解关系型数据库的核心概念,掌握 SQL 基本操作,了解 NoSQL 和非关系型存储的适用场景。


10.1 为什么需要数据库?

文件存储的问题

假设你用文件存储用户数据:

# users.txt
张三,25,北京,zhangsan@email.com
李四,30,上海,lisi@email.com
王五,28,广州,wangwu@email.com

当你要"找出所有年龄大于 25 岁的用户"时,你需要:

1. 读取整个文件

2. 逐行解析

3. 手动过滤

4. 数据量大时极其缓慢

数据库解决了这些问题:


10.2 关系型 vs 非关系型数据库

关系型数据库(SQL)

数据以表(Table)的形式组织,表之间通过外键建立关系

┌─────────────────┐      ┌─────────────────┐
│    users        │      │    orders        │
├─────────────────┤      ├─────────────────┤
│ id (主键)       │◄─────│ user_id (外键)   │
│ name            │      │ product          │
│ email           │      │ amount           │
└─────────────────┘      └─────────────────┘

代表产品: MySQL、PostgreSQL、SQLite、Oracle

特点:

非关系型数据库(NoSQL)

放弃传统表结构,采用更灵活的数据模型。

类型 代表产品 数据模型 典型场景
键值存储 Redis、Memcached 键 → 值 缓存、会话、计数器
文档数据库 MongoDB、CouchDB JSON 文档 内容管理、用户画像
列族数据库 Cassandra、HBase 列族 时序数据、日志分析
图数据库 Neo4j 节点+边 社交关系、推荐系统

NoSQL 特点:

如何选择?

结构化数据 + 需要强一致性 → 关系型数据库
非结构化数据 + 高性能读写 → NoSQL
高频访问的热数据         → Redis 缓存
社交关系、推荐系统       → 图数据库
日志、监控数据           → 时序数据库 / 列族数据库
💡 绝大多数应用关系型数据库 + Redis 缓存的组合,这也是本章的重点。

10.3 SQL 基础操作

SQL(Structured Query Language)是操作关系型数据库的标准语言。

准备:创建示例数据库

我们用一家在线书店的场景来演示:

-- 创建用户表
CREATE TABLE users (
    id SERIAL PRIMARY KEY,          -- 自增主键
    name VARCHAR(50) NOT NULL,      -- 姓名,不能为空
    email VARCHAR(100) UNIQUE,      -- 邮箱,唯一
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 创建图书表
CREATE TABLE books (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    author VARCHAR(100),
    price DECIMAL(10, 2),           -- 价格,两位小数
    stock INT DEFAULT 0
);

-- 创建订单表(关联 users 和 books)
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT REFERENCES users(id),   -- 外键,引用 users 表
    book_id INT REFERENCES books(id),   -- 外键,引用 books 表
    quantity INT DEFAULT 1,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

SELECT:查询数据

-- 查询所有列
SELECT * FROM users;

-- 查询指定列
SELECT name, email FROM users;

-- 去重
SELECT DISTINCT author FROM books;

-- 条件查询(WHERE)
SELECT * FROM books WHERE price > 50;

-- 多条件
SELECT * FROM books
WHERE price > 30 AND stock > 0;

-- 模糊查询(LIKE)
SELECT * FROM books WHERE title LIKE '%编程%';
-- % 匹配任意字符,_ 匹配单个字符

-- 排序
SELECT * FROM books ORDER BY price DESC;  -- 降序
SELECT * FROM books ORDER BY price ASC;   -- 升序(默认)

-- 限制数量
SELECT * FROM books ORDER BY price DESC LIMIT 5;

-- 分页(跳过前 10 条,取 10 条)
SELECT * FROM books LIMIT 10 OFFSET 10;

-- 聚合函数
SELECT COUNT(*) FROM users;               -- 用户总数
SELECT AVG(price) FROM books;             -- 平均价格
SELECT SUM(price * stock) FROM books;     -- 库存总价值
SELECT MAX(price), MIN(price) FROM books; -- 最高/最低价格

-- 分组统计
SELECT author, COUNT(*) as book_count
FROM books
GROUP BY author
HAVING COUNT(*) > 3;   -- 只显示出书超过3本的作者
-- WHERE 过滤行 → GROUP BY 分组 → HAVING 过滤组

INSERT:插入数据

-- 插入单行
INSERT INTO users (name, email)
VALUES ('张三', 'zhangsan@example.com');

-- 插入多行
INSERT INTO books (title, author, price, stock) VALUES
    ('Python编程:从入门到实践', 'Eric Matthes', 89.00, 50),
    ('算法导论', 'Thomas Cormen', 128.00, 20),
    ('深入理解计算机系统', 'Randal Bryant', 139.00, 15),
    ('设计数据密集型应用', 'Martin Kleppmann', 99.00, 30);

-- 插入并返回生成的值(PostgreSQL)
INSERT INTO users (name, email)
VALUES ('李四', 'lisi@example.com')
RETURNING id, created_at;

UPDATE:更新数据

-- 更新指定行(一定要加 WHERE!)
UPDATE books SET price = 79.00 WHERE id = 1;

-- 同时更新多列
UPDATE books
SET price = price * 0.9, stock = stock - 1
WHERE id = 1;

-- ⚠️ 忘记 WHERE 会更新所有行!
-- UPDATE books SET price = 0;  ← 灾难!

DELETE:删除数据

-- 删除指定行(一定要加 WHERE!)
DELETE FROM orders WHERE id = 5;

-- 删除某用户的所有订单
DELETE FROM orders WHERE user_id = 3;

-- ⚠️ 清空整张表
DELETE FROM orders;               -- 逐行删除,可回滚
TRUNCATE TABLE orders;             -- 直接清空,不可回滚,更快

10.4 JOIN:多表连接查询

JOIN 是关系型数据库最强大的特性之一,可以将多张表的数据关联起来。

-- 示例数据
-- users:  1|张三, 2|李四, 3|王五
-- orders: 1|1|1|2  (张三买了2本书1)
--         2|1|2|1  (张三买了1本书2)
--         3|2|1|1  (李四买了1本书1)

INNER JOIN(内连接)

只返回两表中匹配的行

-- 查询用户的订单详情
SELECT
    users.name AS 用户名,
    books.title AS 书名,
    orders.quantity AS 数量,
    orders.order_date AS 下单时间
FROM orders
INNER JOIN users ON orders.user_id = users.id
INNER JOIN books ON orders.book_id = books.id;

-- 结果:
-- 张三 | Python编程... | 2 | 2024-01-15
-- 张三 | 算法导论      | 1 | 2024-01-16
-- 李四 | Python编程... | 1 | 2024-01-17
-- (王五没有订单,不出现)

LEFT JOIN(左连接)

左表全部保留,右表无匹配则填 NULL:

SELECT users.name, orders.id AS order_id
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

-- 结果:
-- 张三 | 1
-- 张三 | 2
-- 李四 | 3
-- 王五 | NULL   ← 王五没有订单,但依然出现

JOIN 类型速查

-- INNER JOIN:只返回匹配的行
-- LEFT JOIN:  左表全部 + 右表匹配
-- RIGHT JOIN: 右表全部 + 左表匹配
-- FULL JOIN:  两表全部(PostgreSQL 支持,MySQL 不支持)
-- CROSS JOIN: 笛卡尔积(每行×每行,慎用)

实际应用示例

-- 畅销书排行榜(统计每本书的销量)
SELECT
    books.title,
    COUNT(orders.id) AS total_orders,
    SUM(orders.quantity) AS total_sold
FROM books
LEFT JOIN orders ON books.id = orders.book_id
GROUP BY books.id, books.title
ORDER BY total_sold DESC;

-- 活跃用户(有订单的用户)消费统计
SELECT
    users.name,
    COUNT(orders.id) AS order_count,
    COALESCE(SUM(books.price * orders.quantity), 0) AS total_spent
FROM users
LEFT JOIN orders ON users.id = orders.user_id
LEFT JOIN books ON orders.book_id = books.id
GROUP BY users.id, users.name
ORDER BY total_spent DESC;

10.5 索引原理

为什么需要索引?

-- 没有索引时
SELECT * FROM users WHERE email = 'zhangsan@example.com';
-- 数据库需要逐行扫描(全表扫描),O(n)

-- 创建索引后
CREATE INDEX idx_users_email ON users(email);
-- 通过 B+Tree 结构,O(log n) 找到目标

索引就像书的目录:不用翻完整个书去找某一章,直接查目录就行。

索引原理(B+Tree)

               [50]
              /    \
        [20, 35]  [65, 80]
       /    |   \   |    \
    [10] [25] [40] [55] [90]
     ↓     ↓    ↓    ↓    ↓
   实际数据行(或指向数据行的指针)

查询 25 的过程:
1. 从根节点 [50] 开始,25 < 50 → 走左边
2. 到 [20, 35],20 < 25 < 35 → 走中间
3. 到 [25],找到!
共 3 次查找 vs 全表扫描可能几百次

索引的最佳实践

-- ✅ 为经常查询的列创建索引
CREATE INDEX idx_books_author ON books(author);

-- ✅ 为外键创建索引(JOIN 性能)
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- ✅ 复合索引(多列查询)
CREATE INDEX idx_orders_user_book ON orders(user_id, book_id);

-- ✅ 唯一索引(保证唯一性 + 加速查询)
CREATE UNIQUE INDEX idx_users_email ON users(email);

-- ❌ 不要在小表上建索引(全表扫描可能更快)
-- ❌ 不要为每个列都建索引(写入性能下降)
-- ❌ 不要在频繁更新的列上建太多索引

-- 查看查询是否使用索引
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- PostgreSQL: EXPLAIN ANALYZE  (更详细)

索引的代价

操作 无索引 有索引
查询 SELECT 快 ✅
插入 INSERT 快 ✅ 稍慢(需更新索引)
更新 UPDATE 快 ✅ 稍慢
删除 DELETE 快 ✅ 稍慢
存储空间 小 ✅ 更大
💡 核心原则:为查询优化建索引,不要为每个列建索引。索引的速度提升通常远超写入开销。

10.6 事务与 ACID

什么是事务?

事务是一组数据库操作,要么全部成功,要么全部失败(原子性)。

-- 经典的转账场景:张三转 500 元给李四
BEGIN;  -- 开始事务

-- 步骤1:扣减张三余额
UPDATE accounts SET balance = balance - 500 WHERE name = '张三';

-- 步骤2:增加李四余额
UPDATE accounts SET balance = balance + 500 WHERE name = '李四';

-- 如果任何一步失败,所有操作都将回滚
COMMIT;  -- 提交事务(如果失败则 ROLLBACK 回滚)

ACID 四个特性

特性 含义 例子
原子性 (Atomicity) 事务中的所有操作是不可分割的整体 转账两步要么都执行,要么都不执行
一致性 (Consistency) 事务执行前后,数据库保持一致状态 转账前后总金额不变
隔离性 (Isolation) 并发事务互不干扰 两个用户同时转账不会混乱
持久性 (Durability) 事务提交后,数据永久保存 系统崩溃后数据不丢失

并发问题与隔离级别

-- 查看隔离级别(PostgreSQL)
SHOW TRANSACTION_ISOLATION;
-- PostgreSQL 默认:READ COMMITTED

-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- ... 操作 ...
COMMIT;
隔离级别 脏读 不可重复读 幻读 性能
READ UNCOMMITTED 最快
READ COMMITTED
REPEATABLE READ 中等
SERIALIZABLE 最慢

三种并发问题:

# Python 中使用事务(psycopg2)
import psycopg2

conn = psycopg2.connect("dbname=mydb user=admin")
try:
    cur = conn.cursor()
    cur.execute("UPDATE accounts SET balance = balance - 500 WHERE name = %s", ("张三",))
    cur.execute("UPDATE accounts SET balance = balance + 500 WHERE name = %s", ("李四",))
    conn.commit()  # 提交
    print("转账成功")
except Exception as e:
    conn.rollback()  # 回滚
    print(f"转账失败:{e}")
finally:
    cur.close()
    conn.close()

10.7 PostgreSQL vs MySQL

PostgreSQL

"功能最强大的开源关系型数据库"

-- 特色功能示例

-- 1. 原生 JSON 支持
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    data JSONB   -- JSONB:二进制JSON,支持索引
);
INSERT INTO products (data) VALUES
    ('{"name": "机械键盘", "specs": {"type": "青轴", "layout": "87键"}}');

-- JSON 查询
SELECT data->>'name' FROM products;
SELECT * FROM products WHERE data @> '{"specs": {"type": "青轴"}}';

-- 2. 数组类型
CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    tags TEXT[]  -- 直接用数组存标签
);
INSERT INTO posts (tags) VALUES ('{"python", "数据库", "教程"}');
SELECT * FROM posts WHERE 'python' = ANY(tags);

-- 3. 全文搜索
SELECT * FROM books
WHERE to_tsvector('chinese', title) @@ to_tsquery('chinese', '编程 & Python');

优势: 功能丰富、标准兼容性好、扩展性强(PostGIS 地理数据、TimescaleDB 时序数据)

MySQL

"最流行的 Web 应用数据库"

-- MySQL 特点

-- 1. 多种存储引擎
CREATE TABLE logs (
    id INT,
    message TEXT
) ENGINE=InnoDB;   -- 支持事务、行级锁

-- 2. 复制配置成熟(主从复制、组复制)
-- 3. 大量采用的产品(WordPress、Magento 等)

选择建议

场景 推荐 原因
学习 SQL PostgreSQL 标准兼容、文档优秀
简单 Web 应用 MySQL 生态成熟、托管服务多
复杂查询、数据分析 PostgreSQL 查询优化器更智能
地理信息系统(GIS) PostgreSQL + PostGIS 地理数据处理的第一选择
需要 JSON 灵活存储 PostgreSQL JSONB 支持索引
分布式、大规模 MySQL 分库分表方案更成熟

10.8 Redis 缓存入门

为什么需要缓存?

没有缓存:
  用户请求 → Web 服务器 → 数据库(每次都查,慢)
  响应时间:200ms

有缓存:
  用户请求 → Web 服务器 → Redis(查缓存,快)
                         ↓ 缓存未命中
                       数据库 → 写入 Redis → 返回
  响应时间:10ms(命中时)

Redis 是一个高性能的内存键值数据库,常用作缓存、消息队列、计数器。

基本操作

# 启动 Redis 服务
redis-server

# 连接 Redis(另一个终端)
redis-cli
# Python 中使用 Redis
import redis
import json

# 连接
r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# === 字符串(String)===
r.set('user:1:name', '张三')
r.setex('session:abc', 3600, 'active')  # 带过期时间(秒)
name = r.get('user:1:name')
print(name)  # 张三

# === 哈希(Hash)——适合存对象 ===
r.hset('user:1', mapping={
    'name': '张三',
    'age': 25,
    'email': 'zhangsan@example.com'
})
print(r.hget('user:1', 'name'))   # 张三
print(r.hgetall('user:1'))        # 全部字段

# === 列表(List)——适合队列 ===
r.lpush('tasks', '发邮件', '生成报表')  # 左侧添加
task = r.rpop('tasks')                    # 右侧取出
print(task)  # 发邮件

# === 集合(Set)——去重、交并集 ===
r.sadd('user:1:tags', 'python', '数据库', 'web')
r.sadd('user:2:tags', 'python', '前端', 'react')
common = r.sinter('user:1:tags', 'user:2:tags')
print(common)   # {'python'}

# === 有序集合(Sorted Set)——排行榜 ===
r.zadd('leaderboard', {'张三': 100, '李四': 85, '王五': 92})
top = r.zrevrange('leaderboard', 0, 2, withscores=True)
print(top)  # [('张三', 100.0), ('王五', 92.0), ('李四', 85.0)]

缓存策略实战

def get_user(user_id):
    """先从缓存取,取不到再查数据库"""
    # 1. 尝试从 Redis 获取
    cache_key = f"user:{user_id}"
    cached = r.get(cache_key)

    if cached:
        return json.loads(cached)

    # 2. 缓存未命中,查数据库
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")

    if user:
        # 3. 写入缓存,设置 30 分钟过期
        r.setex(cache_key, 1800, json.dumps(user))

    return user

def update_user(user_id, data):
    """更新用户数据时,同时失效缓存"""
    db.update("users", data, f"id = {user_id}")
    r.delete(f"user:{user_id}")  # 删除缓存

缓存三大问题

问题 描述 解决方案
缓存穿透 查询不存在的数据,每次都穿透到数据库 缓存空值、布隆过滤器
缓存击穿 热点 key 过期,瞬间大量请求打到数据库 互斥锁、永不过期 + 异步更新
缓存雪崩 大量 key 同时过期,数据库压力骤增 过期时间加随机值、多级缓存
# 解决缓存雪崩:过期时间加随机值
import random
random_ttl = 1800 + random.randint(0, 600)  # 1800~2400秒
r.setex(key, random_ttl, value)

本章小结


[⬅️ 上一章:09-数据结构与算法](./09-数据结构与算法.html) · [🏠 目录](./README.html) · [➡️ 下一章:11-Web开发基础](./11-Web开发基础.html)

Collaplex · 克拉普莱克斯 collaplex.me · 2026 浙ICP备2026080865号