数据库基础篇:从存储、索引到事务与查询
数据库不是一个单一产品,而是一组围绕数据持久化、查询效率、并发控制和故障恢复展开的系统设计。
数据库基础篇:从存储、索引到事务与查询
很多人第一次学习数据库时,会从 SQL 语法开始:SELECT、INSERT、UPDATE、DELETE。这当然重要,但如果只停留在语法层面,很难理解为什么同样一条查询在不同数据库上性能差异巨大,也很难理解为什么 MySQL 适合业务系统,ClickHouse 更适合分析系统,Redis 又经常被放在缓存层。
数据库本质上是在回答几个基础问题:数据怎么存,怎么快速找到,多个请求同时修改时怎么保证正确,机器宕机后怎么恢复,以及数据量变大后怎么扩展。
数据库解决的核心问题
数据库首先解决的是持久化。程序进程退出后,内存里的对象会消失,但业务数据不能消失。订单、用户、支付记录、日志、指标,都需要被稳定地保存下来。
其次是高效访问。磁盘和网络都很慢,如果每次查询都全量扫描数据,系统很快就不可用了。索引、缓存、分区、列式存储、压缩、执行计划,本质上都是为了减少无效的数据读取和计算。
第三是并发控制。真实业务里,多个用户会同时下单、支付、修改资料、扣减库存。数据库必须在并发环境下保证数据不会互相踩踏。
第四是故障恢复。机器会宕机,磁盘会损坏,网络会抖动。数据库需要通过日志、复制、快照、备份等机制,把故障影响控制在可接受范围内。
数据库的几种常见类型
关系型数据库以表、行、列和 SQL 为核心,典型代表是 MySQL、PostgreSQL、Oracle、SQL Server。它们适合处理结构化数据和复杂关系。
KV 数据库以 Key 到 Value 的映射为核心,典型代表是 Redis、RocksDB、FoundationDB。它们通常追求简单访问模式下的极致性能。
文档数据库以 JSON 或类 JSON 文档为核心,典型代表是 MongoDB。它适合结构变化频繁、对象嵌套明显的业务。
搜索引擎以倒排索引为核心,典型代表是 Elasticsearch 和 OpenSearch。它们擅长全文检索、日志检索和复杂过滤。
OLTP 数据库服务在线交易,强调事务、低延迟和强一致。OLAP 数据库服务在线分析,强调大规模扫描、聚合和吞吐。
行存、列存与 KV
行存数据库把一行数据的多个字段放在一起。比如订单表里,一条订单的 order_id、user_id、amount、status 通常会相邻存储。这样读取或修改一整条订单非常方便,所以行存天然适合 OLTP。
列存数据库把同一列的数据连续存储。比如一亿条订单的 amount 会集中放在一起。分析系统经常只关心少数几列,例如按日期统计 GMV,只需要读 date 和 amount,不必把整行订单都读出来。列存因此更适合 OLAP。
KV 存储则把访问模式进一步简化成 get(key) 和 put(key, value)。它不擅长复杂 SQL,但在缓存、元数据、状态存储、嵌入式存储引擎里非常常见。
索引为什么重要
没有索引时,数据库只能扫描大量数据。索引的价值是把“从一堆数据里找目标”变成“沿着某种结构快速定位目标”。
B+Tree 是 OLTP 数据库里最常见的索引结构。它适合等值查询、范围查询和排序,MySQL InnoDB 的主键索引就是典型代表。
LSM Tree 常见于写入密集系统。它先把数据写入内存,再顺序刷盘,后台进行合并。很多 KV 存储和分布式数据库使用这种思路。
倒排索引适合搜索场景。它不是从文档找词,而是从词找到包含这个词的文档,所以 Elasticsearch 能高效做全文检索。
稀疏索引和数据跳过索引常见于 OLAP。它们不一定精确定位每一行,而是帮助系统跳过明显不相关的数据块。
索引不是越多越好。索引会占用空间,也会拖慢写入。每次插入、更新、删除数据时,相关索引也要维护。
一个最小的索引例子如下:
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL,
amount DECIMAL(12, 2) NOT NULL
);
CREATE INDEX idx_orders_user_time
ON orders (user_id, created_at);
-- 这个查询可以利用 user_id + created_at 组合索引
SELECT id, status, amount
FROM orders
WHERE user_id = 10001
AND created_at >= '2026-05-01'
ORDER BY created_at DESC;
如果查询条件经常是 user_id + created_at,这个组合索引有意义。如果只是为了“可能以后会查”而给每个字段都建索引,写入成本和维护成本会一起上升。
事务与 ACID
事务是一组要么全部成功、要么全部失败的操作。比如支付成功后要修改订单状态、写支付流水、扣减库存,这些操作不能只成功一半。
ACID 是事务系统的经典目标:
- Atomicity:原子性,一组操作不可分割。
- Consistency:一致性,事务前后数据满足约束。
- Isolation:隔离性,并发事务之间不能随意互相影响。
- Durability:持久性,提交后的数据不能因为宕机随便丢失。
真实数据库会在性能和隔离性之间做权衡。隔离级别越强,异常越少,但并发成本也越高。
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
INSERT INTO transfer_logs (from_account, to_account, amount)
VALUES (1, 2, 100);
COMMIT;
这段 SQL 里的三步操作应该作为一个整体提交。只扣款不加款,或者只写流水不改余额,都会破坏业务一致性。
MVCC、锁与 WAL
锁是一种直接的并发控制方式。要修改一行数据,先锁住它,其他事务等待。但如果所有读写都互相阻塞,系统并发能力会很差。
MVCC 的思路是给数据保留多个版本。读请求看到某个时间点的快照,写请求创建新版本。这样读和写可以在很多场景下并行。
WAL 是 Write-Ahead Logging,也就是先写日志,再改数据页。它让数据库在宕机后可以通过日志恢复到一致状态。MySQL 的 redo log、PostgreSQL 的 WAL 都属于这个范畴。
查询是怎么执行的
一条 SQL 通常会经历解析、语义分析、优化、执行几个阶段。
优化器负责选择执行计划。比如先过滤哪张表,使用哪个索引,Join 用哪种算法,是否需要排序或聚合。复杂 SQL 的性能问题,很多时候不是 SQL “写得长”,而是执行计划不合适。
OLAP 数据库还会强调向量化执行。它不是一行一行处理数据,而是一批一批处理列数据,更容易利用 CPU 缓存和 SIMD 能力。
从单机到分布式
单机数据库最简单,也最容易维护。但数据量、并发量和可用性要求上来后,就需要复制、分片和分布式一致性。
主从复制可以提升读能力和可用性。分片可以把数据拆到多台机器上。分区可以在单表内按时间或业务字段组织数据,减少扫描范围。
分布式系统真正复杂的地方在一致性。Raft、Paxos、两阶段提交、分布式事务,都是为了在网络不可靠、节点可能失败的环境下维护正确性。
小结
理解数据库,不要从产品名开始,而要从几个底层问题开始:
- 数据按行存、按列存,还是按 Key 存。
- 查询靠 B+Tree、LSM、倒排索引,还是列式扫描。
- 并发靠锁、MVCC,还是更弱的一致性模型。
- 故障靠 WAL、复制、快照,还是外部系统兜底。
- 扩展靠主从、分片,还是原生分布式架构。
掌握这些基础后,再看 MySQL、PostgreSQL、ClickHouse、Doris、Redis、Elasticsearch,就不再是记产品特性,而是在理解每个系统为什么长成现在这个样子。