数据库基础篇:从存储、索引到事务与查询

数据库不是一个单一产品,而是一组围绕数据持久化、查询效率、并发控制和故障恢复展开的系统设计。

数据库基础篇:从存储、索引到事务与查询

很多人第一次学习数据库时,会从 SQL 语法开始:SELECTINSERTUPDATEDELETE。这当然重要,但如果只停留在语法层面,很难理解为什么同样一条查询在不同数据库上性能差异巨大,也很难理解为什么 MySQL 适合业务系统,ClickHouse 更适合分析系统,Redis 又经常被放在缓存层。

数据库本质上是在回答几个基础问题:数据怎么存,怎么快速找到,多个请求同时修改时怎么保证正确,机器宕机后怎么恢复,以及数据量变大后怎么扩展。

数据库解决的核心问题

数据库首先解决的是持久化。程序进程退出后,内存里的对象会消失,但业务数据不能消失。订单、用户、支付记录、日志、指标,都需要被稳定地保存下来。

其次是高效访问。磁盘和网络都很慢,如果每次查询都全量扫描数据,系统很快就不可用了。索引、缓存、分区、列式存储、压缩、执行计划,本质上都是为了减少无效的数据读取和计算。

第三是并发控制。真实业务里,多个用户会同时下单、支付、修改资料、扣减库存。数据库必须在并发环境下保证数据不会互相踩踏。

第四是故障恢复。机器会宕机,磁盘会损坏,网络会抖动。数据库需要通过日志、复制、快照、备份等机制,把故障影响控制在可接受范围内。

数据库的几种常见类型

Rendering diagram...

关系型数据库以表、行、列和 SQL 为核心,典型代表是 MySQL、PostgreSQL、Oracle、SQL Server。它们适合处理结构化数据和复杂关系。

KV 数据库以 Key 到 Value 的映射为核心,典型代表是 Redis、RocksDB、FoundationDB。它们通常追求简单访问模式下的极致性能。

文档数据库以 JSON 或类 JSON 文档为核心,典型代表是 MongoDB。它适合结构变化频繁、对象嵌套明显的业务。

搜索引擎以倒排索引为核心,典型代表是 Elasticsearch 和 OpenSearch。它们擅长全文检索、日志检索和复杂过滤。

OLTP 数据库服务在线交易,强调事务、低延迟和强一致。OLAP 数据库服务在线分析,强调大规模扫描、聚合和吞吐。

行存、列存与 KV

行存数据库把一行数据的多个字段放在一起。比如订单表里,一条订单的 order_iduser_idamountstatus 通常会相邻存储。这样读取或修改一整条订单非常方便,所以行存天然适合 OLTP。

列存数据库把同一列的数据连续存储。比如一亿条订单的 amount 会集中放在一起。分析系统经常只关心少数几列,例如按日期统计 GMV,只需要读 dateamount,不必把整行订单都读出来。列存因此更适合 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,就不再是记产品特性,而是在理解每个系统为什么长成现在这个样子。