09 — 数据库设计

核心规律:索引是速度的代价,事务是一致性的保障 一句话:好的表设计让查询自然快速,坏的表设计让优化举步维艰


一、核心规律

1.1 数据库设计三范式

1NF:原子性 — 每列不可再分
2NF:消除部分依赖 — 非主键字段完全依赖主键
3NF:消除传递依赖 — 非主键字段不依赖其他非主键字段

实际开发:适度反范式
├── 冗余字段提升查询性能
├── 宽表减少JOIN
└── 但要保持数据一致性

1.2 设计决策树

flowchart TD
    A["设计表"] --> B{"数据量级?"}
    B -->|<100万| C["单表即可"]
    B -->|100万-1000万| D["考虑索引优化"]
    B -->|>1000万| E["考虑分库分表"]
    
    F{"读写比例?"} -->|"读多写少"| G["加缓存 + 读写分离"]
    F -->|"写多读少"| H["优化写入 + 异步处理"]
    F -->|"均衡"| I["标准设计"]
    
    style C fill:#e8f5e9
    style E fill:#ffebee
    style G fill:#fff3e0

二、索引原理

2.1 B+树结构

flowchart LR
    subgraph 非叶子节点["📄 非叶子节点"]
        N1["Key: 1"]
        N2["Key: 5"]
        N3["Key: 10"]
    end
    
    subgraph 叶子节点["🌿 叶子节点(数据)"]
        L1["1 → 数据"]
        L2["3 → 数据"]
        L3["5 → 数据"]
        L4["7 → 数据"]
        L5["10 → 数据"]
        L6["15 → 数据"]
    end
    
    N1 & N2 & N3 --> L1 & L2 & L3 & L4 & L5 & L6
    L1 -.-> L2 -.-> L3 -.-> L4 -.-> L5 -.-> L6
    
    style L1 fill:#e8f5e9
    style L3 fill:#e8f5e9
    style L5 fill:#e8f5e9

关键特性:

  • 非叶子节点只存Key,不存数据 → 一页能存更多Key → 树更矮 → 查询更快
  • 叶子节点用链表连接 → 范围查询高效

2.2 索引类型对比

类型结构适用场景特点
聚簇索引B+树(主键)主键查询数据按主键有序存储
二级索引B+树(普通字段)范围查询/排序回表查询
全文索引Inverted Index文本搜索支持MATCH…AGAINST
哈希索引Hash Table等值查询O(1)但无法范围查询

2.3 索引设计原则

✅ 应该建索引:
├── WHERE条件频繁使用的字段
├── JOIN关联字段
├── ORDER BY排序字段
├── GROUP BY分组字段
└── 高区分度的字段(唯一值多)

❌ 不应该建索引:
├── 频繁更新的字段(维护成本高)
├── 低区分度字段(如性别)
├── LIKE '%keyword'(前导通配符)
└── 字段过短(如单字符)

三、事务与锁

3.1 ACID特性

A - 原子性:事务要么全部成功,要么全部失败
C - 一致性:事务前后数据保持一致
I - 隔离性:并发事务互不干扰
D - 持久性:提交后永久保存

3.2 隔离级别

quadrantChart
    title 事务隔离级别对比
    x-axis 并发性能:高 --> 并发性能:低
    y-axis 数据一致性:弱 --> 数据一致性:强
    quadrant-1 串行化
    quadrant-2 可重复读
    quadrant-3 读已提交
    quadrant-4 读未提交
    
    "读未提交": [0.9, 0.1]
    "读已提交": [0.7, 0.4]
    "可重复读": [0.4, 0.7]
    "串行化": [0.1, 0.9]
级别脏读不可重复读幻读MySQL默认
读未提交
读已提交PostgreSQL默认
可重复读❌(部分)MySQL默认
串行化最高级别

3.3 锁机制

行锁(InnoDB):
├── 自动加锁:WHERE条件匹配的行
├── 显式加锁:SELECT ... FOR UPDATE
└── 高并发友好

间隙锁:
├── 防止幻读
├── 锁定索引记录之间的间隙
└── Next-Key Lock = 记录锁 + 间隙锁

四、分库分表

4.1 拆分策略

flowchart LR
    subgraph 垂直拆分["📐 垂直拆分"]
        V1["按业务拆分<br/>订单库/用户库/商品库"]
        V2["按字段拆分<br/>热字段/冷字段"]
    end
    
    subgraph 水平拆分["📊 水平拆分"]
        H1["按范围拆分<br/>id 1-100万/100万-200万"]
        H2["按取模拆分<br/>id % 8 = 分片号"]
    end
    
    style V1 fill:#e3f2fd
    style H1 fill:#e8f5e9

4.2 核心挑战

挑战解决方案
跨分片查询避免跨库JOIN,应用层组装
全局ID生成雪花算法 / 号段模式
分布式事务Seata / TCC / 本地消息表
数据迁移双写 + 灰度切换
分页查询游标分页替代OFFSET

五、慢查询优化

5.1 EXPLAIN分析

EXPLAIN SELECT * FROM orders WHERE user_id = 123;
 
-- 关注字段:
-- type: ALL(全表扫描) → index → range → ref → const
-- key: 实际使用的索引
-- rows: 预估扫描行数
-- Extra: Using filesort(需优化), Using temporary(需优化)

5.2 优化 checklist

□ 避免SELECT *,指定需要的字段
□ 合理使用索引(最左前缀原则)
□ 避免在索引列上用函数
□ 大结果集分页用游标替代OFFSET
□ 批量操作用chunk()分批处理
□ 定期分析慢查询日志
□ 监控索引使用情况(废弃索引删除)

六、本章总结

核心规律:索引是速度的代价。每个索引都有写入和维护成本,要在查询性能和写入性能之间权衡。

关键记忆点

  • ✅ B+树是非叶子节点只存Key,叶子节点存数据+链表
  • ✅ MySQL默认可重复读隔离级别,部分解决幻读
  • ✅ 分库分表解决单表瓶颈,但引入分布式复杂性
  • ✅ EXPLAIN是查询优化的第一步

延伸阅读