你每天都在和数据库打交道——写 SQL 查订单、用 Redis 缓存热点数据、往 Elasticsearch 灌日志。但你有没有想过:为什么 MySQL 用表存数据,MongoDB 用 JSON,Neo4j 用节点和边?为什么分析型查询在列存数据库上快几个数量级?为什么同一个业务有时需要两三种数据库?
本章是整个系列的”地图”。不会深入任何一种数据库的内部实现(那是后续章节的任务),而是从宏观视角俯瞰数据库的完整分类体系:数据怎么建模、怎么查询、怎么存储、怎么分类、怎么选型。理解了这幅全景图,后续每一章的学习就有了锚点。
数据库的历史可以追溯到 1960 年代。1966 年,IBM 的 IMS(Information Management System)成为第一个商用数据库,采用层次模型存储数据——像一棵树,每个节点只有一个父节点。1970 年,Edgar F. Codd 在 IBM 发表了划时代的论文”A Relational Model of Data for Large Shared Data Banks”,提出了关系模型——数据用表格表示,操作用集合论描述。这篇论文催生了 System R(1974,IBM)和 Ingres(1973,UC Berkeley)两个原型系统,分别演化为今天的 DB2 和 PostgreSQL。1995 年,Michael Widenius 发布了 MySQL,将关系数据库带入了互联网时代。2000 年代,Google 的 Bigtable(2006)和 Amazon 的 Dynamo(2007)论文催生了 NoSQL 运动。2012 年,Google 的 Spanner 论文开启了 NewSQL 时代——分布式数据库可以同时提供 SQL 接口和水平扩展。理解这段演进脉络,你会发现每一种数据库都是对特定时代需求的回应——没有”最好的数据库”,只有”最适合当前需求的数据库”。
前置知识
- SQL 基础:
SELECT/INSERT/UPDATE/DELETE、JOIN、GROUP BY等基本语法 - 基本的数据结构知识:哈希表、B 树、排序
- 操作系统基础:文件系统、内存管理、进程模型
本章是全系列的起点,不需要前置章节。如果你已有数据库使用经验,可以快速浏览本章,重点关注数据模型分类和存储模型部分。
一、为什么需要数据库
1.1 从文件系统到数据库
假设你要做一个用户系统,最朴素的做法是直接写文件:
# 用纯文件存储用户数据def save_user(user_id, name, email): with open(f"users/{user_id}.txt", "w") as f: f.write(f"{name}\n{email}\n")
def read_user(user_id): with open(f"users/{user_id}.txt", "r") as f: lines = f.readlines() return {"name": lines[0].strip(), "email": lines[1].strip()}这段代码在单线程、小数据量时能跑。但一旦面临真实场景,问题接踵而至:
- 并发写入:两个进程同时写同一个用户文件,数据互相覆盖
- 原子性:转账操作需要同时修改两个账户,中间崩溃怎么办?
- 查询能力:想查”年龄大于 30 且城市为北京的用户”,你得遍历所有文件
- 数据完整性:邮箱格式不对、年龄为负数,谁来校验?
- 可靠性:磁盘坏了、进程崩溃,数据能恢复吗?
数据库的出现,正是为了系统性地解决这些问题。
1.2 数据密集型应用的挑战
现代应用的核心矛盾是:数据量增长的速度远超单机处理能力的增长。Martin Kleppmann 在 Designing Data-Intensive Applications 中指出,数据密集型应用面临三大核心挑战:
| 挑战 | 含义 | 数据库的应对 |
|---|---|---|
| 可靠性(Reliability) | 系统在故障面前仍能正确工作 | WAL、复制、备份恢复 |
| 可扩展性(Scalability) | 系统在负载增长时仍能保持性能 | 分区、分片、读写分离 |
| 可维护性(Maintainability) | 系统在演化过程中仍可理解和修改 | Schema 演化、数据迁移工具 |
这三大挑战并非数据库独有,而是所有数据密集型系统的共性。数据库只是将应对这些挑战的机制”内置”了——你不需要自己实现 WAL 或 MVCC,数据库替你做了。但理解这些机制的原理,才能在出问题时知道从哪里排查。
二、数据模型
数据模型是数据库的”世界观”——它决定了你如何组织、理解和操作数据。不同的数据模型适合不同的问题域,没有万能模型。
2.1 数据模型关系图
2.2 关系模型
关系模型由 E.F. Codd 于 1970 年提出,是数据库领域最成功的数据模型。核心思想:数据组织为关系(relation),即二维表,每行是一个元组(tuple),每列是一个属性(attribute)。
-- 关系模型:用表表示实体和关系CREATE TABLE users ( id BIGINT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE);
CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), amount DECIMAL(10,2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- 多对多关系通过中间表实现SELECT u.name, COUNT(o.id) AS order_countFROM users uJOIN orders o ON u.id = o.user_idGROUP BY u.id;优势:强 Schema 约束保证数据一致性;SQL 声明式查询,优化器自动选择执行路径;ACID 事务支持;成熟的生态和工具链。
劣势:Schema 不灵活,加列需要 ALTER TABLE;多对多关系需要 JOIN,数据量大时性能堪忧;对象-关系阻抗失配(ORM 的根源)。
2.3 文档模型
文档模型将数据组织为自包含的文档(通常是 JSON 或 BSON),天然支持嵌套结构,减少了 JOIN 的需求。
// 文档模型:一个订单包含所有相关信息{ "_id": "order_001", "user": { "id": "user_001", "name": "张三", "email": "zhangsan@example.com" }, "items": [ {"product": "数据库系统概念", "price": 89.00, "qty": 1}, {"product": "DDIA", "price": 128.00, "qty": 1} ], "total": 217.00, "status": "shipped", "created_at": "2026-04-22T10:00:00Z"}优势:Schema 灵活(Schema-on-Read),适合数据结构频繁变化的场景;嵌套文档减少 JOIN;与前端 JSON 天然契合。
劣势:多对多关系仍需引用 + 应用层 JOIN;嵌套过深导致更新困难;缺乏强一致性约束。
文档模型最常见的陷阱是”把所有东西都塞进一个文档”。当文档大小增长到 MB 级别时,写入放大、查询性能、内存占用都会急剧恶化。文档模型适合”一对多”关系(如订单-商品项),不适合”多对多”关系(如用户-角色-权限)。
2.4 图模型
图模型用节点(Node) 和边(Edge) 表示实体和关系,天生适合多对多关系密集的场景。
// Neo4j Cypher:查询张三的朋友的朋友(二度关系)MATCH (me:User {name: '张三'})-[:KNOWS]->(friend)-[:KNOWS]->(fof)WHERE NOT (me)-[:KNOWS]->(fof) AND me <> fofRETURN DISTINCT fof.name AS recommendation, COUNT(friend) AS mutual_countORDER BY mutual_count DESCLIMIT 10优势:多对多关系查询极快(无需 JOIN,沿边遍历);关系作为一等公民,语义清晰;社交网络、知识图谱、欺诈检测等场景的天然选择。
劣势:无法用标准 SQL 查询(需学习 Cypher/SPARQL);分布式图查询困难;生态不如关系数据库成熟。
2.5 键值模型
键值模型最简单:一个键对应一个值,值可以是任意二进制数据。
# Redis 键值操作SET user:001:name "张三"GET user:001:name# → "张三"
# 哈希:模拟对象HSET user:001 name "张三" email "zhangsan@example.com" age 28HGET user:001 email# → "zhangsan@example.com"
# 有序集合:排行榜ZADD leaderboard 9500 "player_A"ZADD leaderboard 8700 "player_B"ZREVRANGE leaderboard 0 9 WITHSCORES优势:极简的数据模型带来极高的性能(单机百万级 QPS);天然支持分布式(一致性哈希);丰富的数据结构(Redis 的 List/Set/Sorted Set 等)。
劣势:无 Schema,数据一致性靠应用保证;不支持复杂查询(无 WHERE/JOIN);数据关系需要应用层维护。
2.6 时序模型
时序模型针对带时间戳的度量数据优化:每条记录是一个时间点上的观测值。
-- InfluxDB:写入时序数据INSERT cpu,host=server01,region=us-west value=0.64 1422568543702900257
-- 查询最近 1 小时的平均 CPU 使用率SELECT mean(value) FROM cpuWHERE time > now() - 1h AND host = 'server01'GROUP BY time(5m)优势:针对时间范围查询和聚合极度优化;自动数据压缩和降采样;海量写入友好(追加为主)。
劣势:只适合时序数据,通用查询能力弱;数据通常只按时间维度组织;更新和删除历史数据的代价高。
2.7 数据模型对比
| 维度 | 关系模型 | 文档模型 | 图模型 | 键值模型 | 时序模型 |
|---|---|---|---|---|---|
| 数据组织 | 二维表 | JSON 文档 | 节点 + 边 | Key → Value | 时间戳 + 度量 |
| 关系表达 | 外键 + JOIN | 嵌套 + 引用 | 边(一等公民) | 应用层维护 | 通常无关系 |
| Schema | 强约束 | 灵活 | 灵活 | 无 | 半约束 |
| 查询能力 | 最强(SQL) | 中等 | 图遍历 | 最弱 | 时间聚合 |
| 典型场景 | 通用业务 | 内容管理、配置 | 社交、知识图谱 | 缓存、计数器 | 监控、IoT |
| 代表产品 | MySQL, PG | MongoDB | Neo4j | Redis, DynamoDB | InfluxDB |
三、查询语言
数据模型决定了数据如何组织,查询语言决定了数据如何被操作。查询语言的选择深刻影响着数据库的优化空间和使用体验。
3.1 声明式 vs 命令式
声明式查询(Declarative)告诉数据库”我要什么”,不指定”怎么做”:
-- 声明式:只描述目标,不描述过程SELECT name, age FROM users WHERE age > 30 AND city = '北京' ORDER BY age DESC;命令式查询(Imperative)告诉数据库”一步步怎么做”:
# 命令式:精确描述执行过程result = []for user in users: if user.age > 30 and user.city == '北京': result.append(user)result.sort(key=lambda u: u.age, reverse=True)为什么声明式更优?
| 维度 | 声明式(SQL) | 命令式(代码) |
|---|---|---|
| 优化空间 | 优化器可自由选择执行路径 | 执行路径由代码固定 |
| 并行化 | 优化器可自动并行 | 需手动实现并行逻辑 |
| 硬件适配 | 新硬件只需优化器适配 | 需修改所有查询代码 |
| 可读性 | 意图清晰 | 淹没在实现细节中 |
声明式查询的核心优势在于关注点分离:你只关心”要什么”,数据库负责”怎么最快地给你”。这也是为什么 SQL 历经 50 年仍未被取代——它把优化空间留给了数据库,而不是锁死在应用代码里。将在索引原理中看到,优化器如何在声明式查询的基础上选择最优执行路径。
3.2 MapReduce 的局限
MapReduce 曾被视为 SQL 的替代方案,但它在表达力和性能上都有局限:
# MapReduce:统计各城市用户数def mapper(user): yield (user.city, 1)
def reducer(city, counts): yield (city, sum(counts))
# 等价 SQL(简洁得多)# SELECT city, COUNT(*) FROM users GROUP BY city;MapReduce 的问题:
- 表达力弱:多步 MapReduce 链式调用极其繁琐,SQL 一句
GROUP BY搞定 - 每次操作全表扫描:无法利用索引,无法做查询优化
- 编程模型笨重:写 Mapper/Reducer 比写 SQL 复杂得多
- 延迟高:面向批处理设计,不适合交互式查询
现代数据系统(如 Spark SQL、Presto)已经回归声明式查询,MapReduce 作为编程模型已基本退出历史舞台。
3.3 图查询语言
图数据库有专门的查询语言,最流行的是 Cypher(Neo4j)和 SPARQL(RDF 数据库):
// Cypher:模式匹配,直觉表达图遍历MATCH (u:User {name: '张三'})-[:PURCHASED]->(p:Product)<-[:PURCHASED]-(other:User)RETURN other.name, collect(p.name) AS shared_productsORDER BY size(shared_products) DESC图查询语言的独特之处在于模式匹配:你用类似 ASCII Art 的语法画出”图的形状”,数据库负责在数据中找到匹配的子图。这比用 SQL 的多重 JOIN 表达图遍历要直观得多。
3.4 为什么声明式语言更适合优化
声明式查询语言为数据库优化器留出了巨大的操作空间:
- 逻辑重写:
WHERE a=1 AND b=2可以重写为WHERE b=2 AND a=1,让选择性更高的条件先过滤 - 物理优化:同一个 JOIN 可以选择 Nested Loop、Hash Join 或 Merge Join
- 索引选择:优化器根据统计信息选择最优索引
- 并行执行:将查询拆分为可并行的子任务
这些优化在命令式查询中都需要开发者手动实现——而人的直觉往往不如优化器的代价模型准确。
四、存储模型
数据模型决定”数据怎么组织”,存储模型决定”数据怎么存在磁盘上”。同一个逻辑表,行存储和列存储的物理布局完全不同,性能差异可达数量级。
4.1 行存储 vs 列存储
行存储将一行的所有列连续存放,列存储将同一列的所有值连续存放。这个看似简单的差异,导致了截然不同的性能特征。
4.2 为什么分析型查询在列存上更快
假设执行 SELECT AVG(age) FROM users:
行存储需要:
- 读取每一行的完整数据(包括不需要的 name、city 等列)
- 从中提取 age 列
- 计算平均值
列存储只需要:
- 读取 age 列的数据(其他列完全不碰)
- 计算平均值
# 行存储:读取整行,浪费 I/O# 假设每行 100 字节,age 列占 4 字节# 100 万行需要读取 100 MB,但只用到 4 MB
# 列存储:只读 age 列# 100 万行只需读取 4 MB# I/O 减少约 25 倍!列存储对分析型查询的优势不止于减少 I/O:
| 优化手段 | 行存储 | 列存储 |
|---|---|---|
| 只读需要的列 | 不可能,整行读取 | 天然支持 |
| 列内数据压缩 | 困难(类型混合) | 极高压缩比(同类型连续存储) |
| 向量化执行 | 困难 | 天然适配(列数据连续,SIMD 友好) |
| 聚合计算 | 逐行提取列值 | 直接在列上操作 |
列存储并非万能。对于点查询(SELECT * FROM users WHERE id = 42)和单行写入,行存储远优于列存储——因为行存储一次 I/O 就能拿到完整的一行,而列存储需要从多个列文件中分别读取再重组。这就是为什么 OLTP 用行存、OLAP 用列存。
4.3 OLTP vs OLAP vs HTAP
| 维度 | OLTP | OLAP | HTAP |
|---|---|---|---|
| 全称 | 联机事务处理 | 联机分析处理 | 混合事务分析处理 |
| 典型操作 | INSERT/UPDATE/DELETE | GROUP BY/JOIN/聚合 | 两者兼有 |
| 读写模式 | 读少写多,点查询 | 读多写少,范围扫描 | 混合 |
| 存储模型 | 行存储 | 列存储 | 行存 + 列存 |
| 延迟要求 | 毫秒级 | 秒级到分钟级 | 事务毫秒级,分析秒级 |
| 数据量 | GB ~ TB | TB ~ PB | TB 级 |
| 代表产品 | MySQL, PostgreSQL | ClickHouse, Doris | TiDB, OceanBase |
传统架构中,OLTP 和 OLAP 是分离的:业务库(MySQL)通过 ETL 管道将数据同步到分析库(ClickHouse)。HTAP 的目标是消除这条 ETL 管道,在同一个数据库中同时支持事务和分析。关于存储引擎如何实现行存和列存,将在存储引擎中深入分析。
五、数据库分类体系
将数据模型、存储模型、查询语言组合起来,就形成了数据库的完整分类体系。
5.1 关系型数据库(RDBMS)
关系型数据库是 50 年来数据管理的主流方案,以 SQL 为查询语言、以表为数据组织形式、以 ACID 事务为一致性保证。
| 产品 | 特点 | 适用场景 |
|---|---|---|
| MySQL | 生态最成熟、InnoDB 事务支持完善 | Web 应用、电商、内容管理 |
| PostgreSQL | 功能最丰富、扩展性最强 | 地理空间、复杂查询、JSON 支持 |
| Oracle | 企业级功能最全、贵 | 金融、电信、大型企业 |
5.2 NoSQL 数据库
NoSQL(Not Only SQL)是对关系型数据库的补充,针对特定场景做了极致优化。
| 类型 | 代表产品 | 核心优化 | 典型场景 |
|---|---|---|---|
| 文档型 | MongoDB | JSON 嵌套、灵活 Schema | 内容管理、配置中心 |
| 键值型 | Redis, DynamoDB | 极低延迟、高吞吐 | 缓存、会话、计数器 |
| 图型 | Neo4j | 关系遍历优化 | 社交网络、推荐系统 |
| 时序型 | InfluxDB, TimescaleDB | 时间聚合、自动降采样 | 监控、IoT、金融行情 |
| 宽列型 | Cassandra, HBase | 高写入吞吐、线性扩展 | 日志存储、消息历史 |
5.3 搜索引擎
搜索引擎本质上是倒排索引驱动的文档数据库,专门优化全文检索:
# Elasticsearch:全文检索 + 聚合分析curl -X POST "localhost:9200/articles/_search" -H 'Content-Type: application/json' -d '{ "query": { "multi_match": { "query": "数据库性能优化", "fields": ["title^3", "content"] } }, "aggs": { "by_category": { "terms": { "field": "category.keyword" } } }}'5.4 NewSQL
NewSQL 试图兼顾关系型数据库的 ACID 和 NoSQL 的水平扩展能力:
| 产品 | 架构特点 | 一致性模型 |
|---|---|---|
| TiDB | Raft 共识 + MVCC + 行列双存 | 强一致性(Raft) |
| CockroachDB | Range 分片 + Raft | 强一致性(Raft) |
| OceanBase | Paxos + 分区 + 多租户 | 强一致性(Paxos) |
六、数据库选型初探
6.1 选型决策树
选型决策树只是起点,不是终点。真实的选型还需要考虑:团队技术栈、运维能力、社区生态、成本预算、数据迁移代价。一个”技术上最优”但团队无法驾驭的数据库,比一个”够用”但团队熟悉的数据库更危险。更详细的选型框架参见数据库选型与实践。
6.2 CAP 定理简介
CAP 定理指出,分布式数据系统最多只能同时满足以下三者中的两者:
| 属性 | 含义 | 示例 |
|---|---|---|
| C(Consistency) | 所有节点看到相同的数据 | 线性一致性读 |
| A(Availability) | 每个请求都能得到响应(不保证最新) | 节点故障时仍可读写 |
| P(Partition tolerance) | 网络分区时系统仍能运行 | 网络断开时不宕机 |
在分布式系统中,网络分区(P)是不可避免的现实,因此实际选择是 CP 还是 AP:
- CP 系统(如 ZooKeeper、etcd):分区时牺牲可用性,保证一致性
- AP 系统(如 Cassandra、DynamoDB):分区时牺牲一致性,保证可用性
CAP 定理常被过度简化。实际上,C 和 A 并非非此即彼的二选一——在正常情况下(无网络分区),系统可以同时提供一致性和可用性;只有在分区发生时才需要做出取舍。现代分布式数据库(如 CockroachDB、TiDB)通过 Raft 共识协议,在大多数场景下同时提供 C 和 A,只在少数节点故障时短暂牺牲 A。
6.3 多语言持久化(Polyglot Persistence)
现代系统很少只用一种数据库。多语言持久化是指在同一系统中使用多种数据库,每种负责自己最擅长的领域:
# 一个电商系统的多语言持久化架构class OrderService: def __init__(self): self.mysql = MySQLClient() # 核心业务数据(订单、用户、商品) self.redis = RedisClient() # 缓存、会话、库存预扣 self.es = ElasticsearchClient() # 商品搜索、日志分析 self.mongo = MongoClient() # 商品详情(Schema 灵活) self.influx = InfluxDBClient() # 业务监控指标
async def create_order(self, order): # 1. Redis 预扣库存 await self.redis.decr(f"stock:{order.product_id}") # 2. MySQL 写入订单(事务保证) await self.mysql.insert("orders", order) # 3. ES 更新搜索索引 await self.es.index("orders", order) # 4. InfluxDB 记录指标 await self.influx.write("order_created", tags={"product": order.product_id})多语言持久化的代价是运维复杂度:每种数据库都需要监控、备份、升级、排障。在数据库选型与实践中,将讨论如何在”够用”和”最优”之间找到平衡。
七、总结
| 维度 | 关系型 | 文档型 | 图型 | 键值型 | 时序型 | 搜索引擎 | NewSQL |
|---|---|---|---|---|---|---|---|
| 数据模型 | 表 | JSON 文档 | 节点+边 | Key-Value | 时间+度量 | 倒排索引 | 表 |
| 查询语言 | SQL | MongoDB Query | Cypher | Redis CMD | InfluxQL | DSL | SQL |
| 存储模型 | 行存 | 行存/列存 | 行存 | 内存/磁盘 | 列存 | 倒排索引 | 行存+列存 |
| 事务支持 | ACID | 单文档 | ACID | 有限 | 有限 | 无 | ACID |
| 扩展方式 | 垂直为主 | 水平分片 | 垂直 | 水平 | 水平 | 水平分片 | 水平 |
| 一致性 | 强一致 | 最终/强一致 | 强一致 | 最终一致 | 最终一致 | 最终一致 | 强一致 |
| 代表产品 | MySQL, PG | MongoDB | Neo4j | Redis | InfluxDB | ES | TiDB |
本章建立了理解所有数据库的概念框架:
- 数据模型决定数据如何组织——关系、文档、图、键值、时序各有适用域
- 查询语言决定数据如何操作——声明式比命令式留给优化器更大空间
- 存储模型决定数据如何落盘——行存适合 OLTP,列存适合 OLAP
- 分类体系将数据模型、存储模型、查询语言组合为完整的数据库图谱
- 选型决策需要平衡技术需求与团队能力,CAP 定理是分布式选型的理论基石
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






