几乎每一位软件开发者都是从编写 SELECT * FROM users 开始接触数据库的。在入门阶段,我们关注的是如何让数据“存进去、取出来”。但当系统从几百用户扩展到几百万用户,从单机部署到分布式集群,原本顺滑的数据库突然变成了整个系统的瓶颈——查询变慢、写入卡顿、数据不一致、甚至死锁崩溃。
这背后的根本原因在于:数据库是一门关于权衡的科学。它需要在性能、一致性、可用性、可扩展性之间做出精妙的选择。进阶的数据库开发者,不是背下几个优化口诀,而是深刻理解数据库内部的工作机制:索引的数据结构、事务的隔离实现、锁的粒度、查询优化器的决策过程、以及分布式环境下的数据分布策略。
本篇文章将系统地讲解数据库进阶的核心知识点。我们会以关系型数据库(主要是 PostgreSQL 和 MySQL/InnoDB)为主线,同时穿插 NoSQL 和 NewSQL 的对比,涵盖索引优化、查询分析与调优、事务隔离与锁、MVCC、分区与分片、复制与高可用、数据建模(范式与反范式)、以及数据库监控与运维等内容。每个知识点都配有详细的 SQL 示例、执行计划分析、以及能直接运行的代码片段。
预备知识:你已经熟悉 SQL 的基本写法(增删改查、多表连接、分组聚合),有至少几个月使用关系型数据库的经验。
第一部分:索引 —— 查询提速的核心武器
索引是数据库性能优化中最重要也最容易被滥用的工具。一个合适的索引可以让查询速度提升千百倍,而一个错误的索引不仅浪费空间,还会拖慢写入操作。
1.1 索引的底层数据结构:B-Tree vs Hash vs LSM Tree
1.1.1 B-Tree 与 B+Tree
绝大多数关系型数据库的默认索引类型是 B-Tree(或变种 B+Tree)。B-Tree 是一种平衡多路搜索树,其核心特点:
所有叶子节点在同一深度(平衡)。
内部节点存储键值和指向子节点的指针;B+Tree 中数据只存在于叶子节点,叶子节点之间用链表连接,非常适合范围查询和顺序扫描。
为什么选择 B-Tree 而不是二叉树?
因为数据库索引通常存储在磁盘上,磁盘 I/O 是主要开销。B-Tree 每个节点可以存储大量键值(一个节点对应一个磁盘页,通常 4KB-16KB),树的高度很低(通常 3-4 层),查找一个值只需要很少的磁盘读取次数。
1.1.2 Hash 索引
Hash 索引基于哈希表实现,等值查询极快(O(1)),但不支持范围查询和排序。Memory 引擎和某些 NoSQL(如 Redis)大量使用 Hash 索引。InnoDB 有一个自适应哈希索引(AHI)特性,当系统检测到某些索引值被频繁等值查询时,会自动在内存中建立哈希索引加速。
1.1.3 LSM Tree(Log-Structured Merge-Tree)
LSM Tree 被 Cassandra、RocksDB、LevelDB 等 NoSQL 数据库采用。它的设计思路是:写入先写到内存中的 MemTable,达到阈值后刷入磁盘形成不可变的 SSTable,后台异步合并压缩。LSM Tree 写入性能极高(顺序写),但读取可能需要合并多个 SSTable 的数据,且空间放大问题存在。适合写多读少的场景,如时间序列数据、日志数据。
1.2 聚簇索引与二级索引
聚簇索引(Clustered Index)
InnoDB 中,表数据本身就是按照主键组织的 B+Tree,叶子节点存储完整的行数据。这意味着:
主键查询非常快,直接到达叶子节点拿到数据。
如果没有显式定义主键,InnoDB 会选择第一个非空的唯一索引作为聚簇索引,否则隐式生成一个 6 字节的 row id。
为什么推荐使用自增整数主键?
因为插入时新记录的主键值总是大于已有值,B+Tree 只需要在右边缘追加,页分裂频率低;而使用 UUID 作为主键会导致大量随机插入,页分裂频繁,碎片化严重,写入性能下降。
二级索引(Secondary Index)
二级索引的叶子节点存储的是主键值(而不是行数据的指针)。这意味着通过二级索引查询时,需要先找到主键,再回表到聚簇索引获取完整数据(称为“回表查询”)。
覆盖索引(Covering Index):如果二级索引的叶子节点已经包含了查询所需的所有字段(例如 SELECT id, name FROM user WHERE age=20,在 (age, name) 上建立联合索引),则不需要回表,性能显著提升。
1.3 联合索引与最左前缀原则
联合索引是多个列组成的索引,例如 INDEX (last_name, first_name, dob)。其排序规则:先按 last_name 排序,last_name 相同再按 first_name,以此类推。
最左前缀原则:查询条件必须从索引的最左列开始,才能使用该索引。缺失某一列,则其右边的列无法用于索引过滤。
-- 假设有索引 (a, b, c)
-- 可以使用索引的情况:
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3 -- 只用到了 a 列,c 无法用索引(因为缺失 b)
WHERE b = 2 AND a = 1 -- 优化器会调整顺序,依然可用
-- 无法使用索引的情况:
WHERE b = 2
WHERE c = 3
WHERE a > 1 AND b = 2 -- a 用了范围查询,b 无法使用索引(范围后的列失效)
深度说明:MySQL 8.0 引入了“跳跃扫描”(Skip Scan)优化,在某些条件下可以跳过前导列使用后续列索引,但仍有较多限制,不能完全依赖。
实战:如何选择合适的联合索引顺序?
经验法则:
区分度高的列放在左边(等值查询)。
经常用于范围查询的列放在右边(因为范围之后的列索引失效)。
考虑查询模式:如果 WHERE a=? AND b=? 和 WHERE b=? 两种查询都很频繁,可能需要两个索引 (a,b) 和 (b),或者利用覆盖索引兼顾。
1.4 索引失效的常见场景与分析
即使建了索引,SQL 语句写法不当也会导致索引无法使用。以下是一些典型情况:
示例:隐式类型转换导致索引失效
-- 假设 user_id 是 VARCHAR 类型,且有索引
EXPLAIN SELECT * FROM users WHERE user_id = 123;
-- 结果 type=ALL (全表扫描)
-- 因为优化器会执行 CAST(user_id AS SIGNED) = 123,函数使索引失效
-- 正确写法
SELECT * FROM users WHERE user_id = '123';
1.5 索引的维护与代价
索引不是越多越好。每增加一个索引:
插入、更新、删除时都需要维护所有索引,降低写性能。
占用额外的磁盘空间(有时索引比数据还大)。
如何评估冗余索引?例如索引 (a) 和 (a, b) 冗余,因为 (a, b) 可以覆盖 (a) 的查询。使用工具如 pt-duplicate-key-checker 查找重复/冗余索引。
何时重建索引?随着数据更新,B-Tree 可能出现页碎片,导致空间浪费和扫描效率降低。可以使用 OPTIMIZE TABLE(MySQL)或 REINDEX(PostgreSQL)重建。
来源:
https://hllft.cn/