MySQL、PostgreSQL、Oracle、SQL Server 核心差异与选型
很多同学学数据库会遇到一个困惑:MySQL 说 B+Tree,Oracle、PostgreSQL、SQL Server 说 B-tree;MySQL 说 redo、undo、binlog,PostgreSQL 说 WAL,Oracle 说 Undo、Redo、SCN,SQL Server 说 Transaction Log;看起来名词不同,但又都能支持事务、索引、执行计划和恢复。
这一页的目标不是让你背“数据库 A 比数据库 B 快”,而是建立一套能迁移的理解方式:
先理解关系型数据库共同要解决的问题,再理解每个数据库选择了什么实现方式,最后才能知道为什么排查方法不同。
学习目标
学完这一页,你要能回答:
- MySQL、PostgreSQL、Oracle、SQL Server 都在解决哪些共同问题。
- 为什么 MySQL 教材经常说 B+Tree,而其他数据库常说 B-tree。
- Oracle、PostgreSQL、SQL Server 说的 B-tree 和普通 B 树是不是一回事。
- 事务日志、redo、undo、WAL、binlog、SCN、Transaction Log 分别解决什么问题。
- MVCC 在不同数据库中为什么实现不同。
- 为什么同一条 SQL 在不同数据库上的执行计划和优化方式不同。
- 商业项目中如何按订单、资产、报表、搜索、审计、高可用选择数据库。
- 从 MySQL 迁移到 PostgreSQL / Oracle / SQL Server 时最容易踩哪些坑。
先抓住共同主线
四类数据库都要解决这些问题:
flowchart TD
A["业务 SQL"] --> B["解析和权限检查"]
B --> C["优化器选择执行计划"]
C --> D["执行器访问表和索引"]
D --> E["缓存池读取数据页"]
E --> F["锁和版本控制并发"]
F --> G["事务日志保证恢复"]
G --> H["复制、备份、高可用"]所以不管换成哪种数据库,学习主线都不是变成“重新学一门完全无关的技术”,而是看它在每一层具体怎么实现。
| 共同问题 | MySQL InnoDB | PostgreSQL | Oracle | SQL Server |
|---|---|---|---|---|
| 数据怎么组织 | 表空间、段、区、页、聚簇索引 | Database、schema、heap page、tuple | 表空间、段、区、block | database、file、extent、page |
| 索引怎么定位 | B+Tree 主键聚簇、二级索引保存主键 | B-tree 索引指向 heap tuple | B-tree 索引指向 rowid | 聚集索引叶子为数据,非聚集索引定位行 |
| SQL 怎么变计划 | 优化器基于统计信息估算成本 | 成本优化器,重视统计和扩展统计 | CBO,重视统计、直方图、执行计划 | 优化器、统计信息、计划缓存 |
| 并发怎么处理 | 锁 + undo 版本链 MVCC | tuple 多版本 + VACUUM | Undo + SCN 一致性读 | 锁,或 RCSI/Snapshot 行版本 |
| 提交怎么恢复 | redo log 保证崩溃恢复 | WAL 先写日志 | redo log + SCN | Transaction Log |
| 复制怎么做 | binlog 主从复制 | WAL 流复制 | Data Guard / redo 传输 | Always On / 日志传送 |
为什么同叫关系型数据库,差异这么大
因为“关系模型”定义的是数据和查询的逻辑模型,不规定内部必须怎么存。数据库内部有很多工程取舍:
| 取舍 | 选择 A | 选择 B | 影响 |
|---|---|---|---|
| 表数据组织 | 聚簇索引组织 | 堆表组织 | 主键查询、二级索引回表方式不同 |
| MVCC 存储 | undo 保存旧版本 | 行本身保存多版本 | 长事务、清理机制、空间膨胀不同 |
| 日志职责 | 恢复日志和复制日志分开 | WAL 同时服务恢复和复制 | 运维关注点不同 |
| 读一致性 | 默认快照读 | 默认语句级快照 | 阻塞体验和版本清理不同 |
| 执行计划缓存 | 强调计划复用 | 更重视每次规划场景 | 参数嗅探、计划稳定性不同 |
换句话说:它们面向的业务目标相似,但内部路线不同。
B+Tree、B-tree、B 树到底怎么理解
这是面试里最容易混乱的点。
教科书里的 B 树
教科书里的 B 树可以在内部节点和叶子节点都保存 key 和数据。查找某个 key 时,命中内部节点可能就能拿到数据。
flowchart TD
A["根节点<br/>保存 key 和部分数据"] --> B["中间节点<br/>保存 key 和部分数据"]
A --> C["中间节点<br/>保存 key 和部分数据"]
B --> D["叶子节点"]
B --> E["叶子节点"]它适合说明多路平衡查找树思想,但不等于所有商业数据库索引的完整实现。
MySQL 常说 B+Tree 的原因
MySQL InnoDB 中,B+Tree 更常被称为 B+Tree/B+树,核心特点是:
- 非叶子节点主要保存索引键和子节点指针。
- 叶子节点保存完整数据或主键。
- 叶子节点之间有链表,适合范围扫描。
- 主键索引的叶子节点保存整行数据,所以叫聚簇索引。
- 二级索引叶子节点保存二级索引键和主键值,再通过主键回表。
flowchart TD
A["根页<br/>索引键 + 子页指针"] --> B["非叶子页"]
A --> C["非叶子页"]
B --> D["叶子页<br/>key + 行数据或主键"]
B --> E["叶子页<br/>key + 行数据或主键"]
D --> E为什么这样设计?
| 设计 | 好处 |
|---|---|
| 非叶子节点不放整行数据 | 一个页能放更多 key,树更矮,IO 更少 |
| 叶子节点有序链表 | 范围查询、排序、分页更友好 |
| 主键聚簇 | 主键查询直接拿到整行 |
| 二级索引保存主键 | 避免保存物理地址失效,但可能需要回表 |
如果不用这种设计,范围查询可能要到处跳,树高可能变大,缓存命中率和 IO 都会变差。
Oracle、PostgreSQL、SQL Server 为什么说 B-tree
Oracle、PostgreSQL、SQL Server 官方语境里常用 B-tree 泛指多路平衡树索引家族。这里的 B-tree 并不等于“内部节点保存完整行数据的普通 B 树”。
可以这样理解:
MySQL 教学里强调 B+Tree,是为了突出叶子层和范围扫描;Oracle、PostgreSQL、SQL Server 说 B-tree,更多是官方术语习惯和索引家族命名。不要把它们误解成简单二叉树,也不要机械认为“只有 MySQL 是 B+Tree,其他数据库就是低级 B 树”。
不同数据库的关键区别不是名字,而是叶子层如何定位数据:
| 数据库 | 常见 B-tree 叶子层保存什么 | 查询缺列时怎么取完整行 |
|---|---|---|
| MySQL InnoDB 主键索引 | 完整行数据 | 不需要回表 |
| MySQL InnoDB 二级索引 | 二级索引键 + 主键 | 通过主键回聚簇索引 |
| PostgreSQL B-tree | 索引键 + TID | 回 heap 表取 tuple,并做可见性检查 |
| Oracle B-tree | 索引键 + ROWID | 通过 ROWID 定位表 block 和行 |
| SQL Server 聚集索引 | 叶子层就是数据行 | 不需要 Lookup |
| SQL Server 非聚集索引 | 索引键 + 行定位器 | Key Lookup / RID Lookup |
四种数据库的一条查询怎么走
MySQL InnoDB
flowchart TD
A["SQL"] --> B["Parser"]
B --> C["Optimizer"]
C --> D["Executor"]
D --> E["InnoDB Buffer Pool"]
E --> F["B+Tree 索引页"]
F --> G["数据页"]
G --> H["undo 可见性判断"]高频关注点:
- 是否走了合适联合索引。
- 是否回表过多。
rows估算和真实扫描是否偏差。- 是否
Using temporary、Using filesort。 - 是否被锁等待、长事务、undo 堆积影响。
PostgreSQL
flowchart TD
A["SQL"] --> B["Parser / Rewriter"]
B --> C["Planner"]
C --> D["Executor"]
D --> E["Shared Buffers"]
E --> F["B-tree / GIN / GiST 索引"]
F --> G["Heap Tuple"]
G --> H["xmin/xmax 可见性判断"]高频关注点:
EXPLAIN ANALYZE中估算行数和实际行数差异。- heap tuple 多版本是否导致表膨胀。
- VACUUM 是否及时清理死元组。
- GIN、GiST、BRIN 等索引是否适合查询类型。
- 复杂 SQL、CTE、窗口函数、JSONB 是否被合理优化。
Oracle
flowchart TD
A["SQL"] --> B["Shared Pool 解析和优化"]
B --> C["CBO 选择计划"]
C --> D["执行器"]
D --> E["Database Buffer Cache"]
E --> F["Index Block"]
F --> G["Table Block"]
G --> H["Undo + SCN 一致性读"]高频关注点:
- 执行计划、统计信息、直方图是否准确。
- 通过 ROWID 回表是否过多。
- Undo 是否足够,是否出现快照过旧问题。
- Redo 生成量、日志切换、归档是否影响写入。
- AWR、ASH、等待事件如何定位瓶颈。
SQL Server
flowchart TD
A["T-SQL"] --> B["Parser / Algebrizer"]
B --> C["Optimizer"]
C --> D["执行计划缓存"]
D --> E["执行器"]
E --> F["Buffer Pool"]
F --> G["聚集/非聚集索引页"]
G --> H["锁或行版本判断"]高频关注点:
- 聚集索引和非聚集索引设计是否合理。
- Key Lookup 是否过多。
- 参数嗅探是否导致同一 SQL 忽快忽慢。
- 等待类型、阻塞链、死锁图。
- TempDB 是否被排序、Hash、版本存储拖垮。
事务日志体系对比
事务日志的核心价值是:数据库不能只把最终结果写到数据文件,还要记录“怎么改的”,这样才能回滚、恢复和复制。
flowchart TD
A["事务修改数据"] --> B["生成事务日志"]
B --> C["日志先持久化"]
C --> D["修改缓存中的数据页"]
D --> E["事务提交"]
E --> F["后台刷脏页到数据文件"]
C --> G["宕机恢复可重做"]不同数据库叫法不同:
| 数据库 | 日志/版本组件 | 主要解决什么 |
|---|---|---|
| MySQL InnoDB redo log | 崩溃恢复,保证提交事务可重做 | |
| MySQL undo log | 回滚和 MVCC 旧版本读取 | |
| MySQL binlog | Server 层逻辑日志,主从复制和点位恢复 | |
| PostgreSQL WAL | 崩溃恢复、流复制、归档恢复 | |
| Oracle Redo | 崩溃恢复、Data Guard 传输 | |
| Oracle Undo | 回滚、一致性读、Flashback 基础 | |
| Oracle SCN | 数据库全局逻辑时间,判断一致性版本 | |
| SQL Server Transaction Log | 回滚、崩溃恢复、日志备份、Always On |
为什么 MySQL 既有 redo 又有 binlog
MySQL 有 Server 层和存储引擎层。InnoDB 的 redo log 属于存储引擎,记录页级物理修改,用于崩溃恢复;binlog 属于 Server 层,记录逻辑变更,用于主从复制、审计和时间点恢复。
如果只有 redo,没有 binlog:
- InnoDB 能崩溃恢复。
- 但主从复制、CDC、基于 binlog 的数据同步会缺少统一日志。
如果只有 binlog,没有 redo:
- 可以知道执行过哪些 SQL 或行变更。
- 但宕机时不能高效恢复 InnoDB 缓存页中的已提交修改。
所以 MySQL 提交事务时需要协调 redo 和 binlog,避免一个成功一个失败造成主库和从库不一致。
为什么 PostgreSQL 一个 WAL 能承担更多角色
PostgreSQL 的 WAL 是预写日志,同时用于崩溃恢复、流复制和归档恢复。它的核心原则也是先写日志,再刷数据页。
但这不代表 PostgreSQL 没有版本管理。PostgreSQL 的旧版本主要保存在表的 heap tuple 中,通过 xmin、xmax 等事务信息判断可见性,后续由 VACUUM 清理死元组。
所以 PostgreSQL 运维特别关注:
- 长事务会阻止旧版本清理。
- VACUUM 不及时会表膨胀。
- WAL 生成过快会影响复制和归档。
MVCC 实现差异
MVCC 的目标是:读操作尽量不要被写操作阻塞,同时读到一个一致的历史版本。
flowchart TD
A["事务开始"] --> B["确定可见性规则"]
B --> C["读取行"]
C --> D{"当前版本是否可见"}
D -- "可见" --> E["返回当前版本"]
D -- "不可见" --> F["寻找旧版本"]
F --> G["根据事务状态判断"]| 数据库 | 旧版本在哪里 | 清理压力来自哪里 |
|---|---|---|
| MySQL InnoDB | undo log 版本链 | 长事务导致 undo 不能清 |
| PostgreSQL | heap 中的旧 tuple | VACUUM 不能清死元组导致膨胀 |
| Oracle | Undo 表空间 | Undo 不足可能快照过旧 |
| SQL Server | TempDB 版本存储,取决于隔离设置 | RCSI/Snapshot 增加 TempDB 压力 |
为什么长事务可怕
长事务不是“慢一点”这么简单。它会让数据库保留更多历史版本:
| 数据库 | 长事务影响 |
|---|---|
| MySQL | undo 版本链变长,purge 不能推进,快照读变慢 |
| PostgreSQL | dead tuple 不能被 VACUUM 清掉,表和索引膨胀 |
| Oracle | Undo 被长查询占用,可能影响一致性读 |
| SQL Server | 行版本在 TempDB 中保留更久,TempDB 压力增加 |
这就是为什么线上排查慢 SQL 和空间暴涨时,要看长事务,而不是只看索引。
执行计划为什么会选错
优化器不是神,它依赖统计信息估算。
flowchart TD
A["SQL 条件"] --> B["统计信息"]
B --> C["估算过滤后行数"]
C --> D["估算 IO / CPU / 内存成本"]
D --> E["选择成本最低计划"]
E --> F["实际执行"]
F --> G{"估算和实际是否接近"}
G -- "接近" --> H["计划通常稳定"]
G -- "偏差大" --> I["可能慢 SQL"]常见选错原因:
| 原因 | 表现 | 处理方向 |
|---|---|---|
| 统计信息过期 | 估算行数和实际差很多 | 更新统计信息 |
| 数据倾斜 | 热值查询计划不适合冷值 | 直方图、改写 SQL、分离热值 |
| 字段相关性 | 多条件独立估算错误 | 扩展统计、联合索引、改写查询 |
| 参数嗅探 | SQL Server 同 SQL 不同参数忽快忽慢 | Query Store、重新编译、计划治理 |
| 函数包列 | 索引不能正常利用 | 函数索引、生成列、改写条件 |
| 返回列太多 | 本来可覆盖,结果大量回表 | 覆盖索引或减少返回列 |
商业场景选型
订单、支付、库存
优先考虑 MySQL、Oracle、SQL Server 这类成熟关系库。关键不是“单条 SQL 多快”,而是事务、唯一约束、状态流转、审计、备份恢复能不能稳。
| 场景 | 推荐思路 |
|---|---|
| 互联网订单 | MySQL 常见,配合分库分表、缓存、MQ |
| 金融核心交易 | Oracle 常见,强调稳定性、高可用、审计 |
| 微软生态企业系统 | SQL Server 常见,配合 .NET、报表、BI |
医疗资产、采集任务、设备台账
这类系统通常不是极致高并发秒杀,但要求数据准确、可追溯、可查询。
常见设计:
- 关系库保存设备、医院、科室、采集任务、采集结果。
- Redis 缓存热设备状态和采集进度。
- Elasticsearch 承担复杂搜索和日志检索。
- MQ 解耦采集结果异步入库和索引同步。
- 审计表记录关键状态变化。
报表和分析
不要把在线交易库当无限报表库。复杂统计会大量扫描、排序、聚合,影响交易请求。
可选方案:
- 从库承接轻量报表。
- 定时 ETL 到数仓或 OLAP。
- PostgreSQL 可承担一部分复杂 SQL 和分析需求。
- SQL Server + BI 工具适合微软生态报表。
- Oracle 在大型政企环境中常配合专门报表库和物化视图。
迁移和多数据库开发常见坑
| 坑 | 原因 | 处理 |
|---|---|---|
| SQL 方言不同 | 分页、函数、日期、递归查询语法不同 | 使用数据库专属适配层 |
| 自增主键不同 | auto_increment、sequence、identity 差异 | 明确 ID 生成策略 |
| 隔离级别体验不同 | 默认隔离和 MVCC 实现不同 | 重新验证并发场景 |
| 索引语法不同 | INCLUDE、函数索引、部分索引支持不同 | 按目标库重建索引策略 |
| 执行计划差异 | 优化器成本模型不同 | 用真实数据重新压测 |
| 大小写和编码 | 标识符、排序规则、字符集不同 | 统一命名和 collation |
| 时间类型 | timestamp、datetime、timezone 行为不同 | 明确时区和精度 |
| 事务日志运维不同 | binlog、WAL、redo、LDF 管理方式不同 | 建立目标库备份恢复手册 |
可运行 Demo:同一个业务在不同数据库的建表差异
MySQL
create table asset_device (
id bigint primary key auto_increment,
hospital_id varchar(32) not null,
asset_code varchar(64) not null,
name varchar(128) not null,
status tinyint not null,
updated_at datetime not null,
unique key uk_hospital_asset (hospital_id, asset_code),
key idx_hospital_status_updated (hospital_id, status, updated_at)
) engine = InnoDB default charset = utf8mb4;PostgreSQL
create table asset_device (
id bigserial primary key,
hospital_id varchar(32) not null,
asset_code varchar(64) not null,
name varchar(128) not null,
status smallint not null,
updated_at timestamp not null,
constraint uk_hospital_asset unique (hospital_id, asset_code)
);
create index idx_hospital_status_updated
on asset_device (hospital_id, status, updated_at);Oracle
create table asset_device (
id number(19) primary key,
hospital_id varchar2(32) not null,
asset_code varchar2(64) not null,
name varchar2(128) not null,
status number(3) not null,
updated_at timestamp not null,
constraint uk_hospital_asset unique (hospital_id, asset_code)
);
create sequence seq_asset_device start with 1 increment by 1;
create index idx_hospital_status_updated
on asset_device (hospital_id, status, updated_at);SQL Server
create table asset_device (
id bigint identity(1,1) primary key,
hospital_id varchar(32) not null,
asset_code varchar(64) not null,
name varchar(128) not null,
status tinyint not null,
updated_at datetime2 not null,
constraint uk_hospital_asset unique (hospital_id, asset_code)
);
create index idx_hospital_status_updated
on asset_device (hospital_id, status, updated_at);你会发现,业务模型相同,但自增、字符串类型、时间类型、索引语法都不同。商业项目不能靠“复制 MySQL SQL 到 Oracle 运行”完成迁移。
同一条列表查询在四种数据库里为什么表现不同
很多人觉得“SQL 一样,索引也差不多,性能应该差不多”。实际不是这样。数据库真正执行时,不只看 SQL 文本,还看表组织方式、索引叶子层、统计信息、可见性判断、回表方式和缓存命中。
以医疗资产列表为例:
select id, hospital_id, asset_code, name, status, updated_at
from asset_device
where hospital_id = ?
and status = ?
order by updated_at desc
fetch first 20 rows only;这条 SQL 的目标很明确:先按医院过滤,再按状态过滤,最后取更新时间最新的 20 条。它适合建立联合索引:
(hospital_id, status, updated_at)但四种数据库的“命中索引以后怎么拿到行”不一样。
flowchart TD
A["列表查询<br/>hospital_id + status + order by updated_at"] --> B["优化器估算行数和成本"]
B --> C["选择索引或全表扫描"]
C --> D["索引定位候选行"]
D --> E["读取完整列和判断可见性"]
E --> F["排序 / Top-N / 返回 20 行"]MySQL InnoDB:二级索引命中后可能回聚簇索引
MySQL InnoDB 的二级索引叶子层保存的是索引列和主键值。如果查询列不都在二级索引里,就要通过主键回到聚簇索引取整行。
flowchart TD
A["二级索引<br/>hospital_id,status,updated_at"] --> B["找到主键 id"]
B --> C["回聚簇索引"]
C --> D["读取整行 name、asset_code 等列"]如果只返回 20 行,回表通常可接受;如果条件范围很大、深分页、返回很多行,回表会变成大量随机访问。覆盖索引能减少回表,但覆盖索引很宽也会增加写入和缓存成本。
PostgreSQL:Index Only Scan 也要看 Visibility Map
PostgreSQL B-tree 索引叶子项通常保存 key 和 TID,TID 指向 heap tuple。即使查询列都在索引里,数据库还要确认这个 tuple 对当前事务快照是否可见。
flowchart TD
A["B-tree 索引命中"] --> B["得到 TID"]
B --> C{"heap page 是否 all-visible"}
C -- "是" --> D["Index Only Scan 可少访问 heap"]
C -- "否" --> E["访问 heap 检查 xmin/xmax 可见性"]所以 PostgreSQL 的“覆盖索引”不能照搬 MySQL 逻辑。它还受 VACUUM、Visibility Map、长事务和表膨胀影响。Autovacuum 跟不上时,Index Only Scan 可能退化为大量 heap fetch。
Oracle:索引叶子保存 ROWID,回表看 ROWID 成本
Oracle B-tree leaf block 保存 key 和 ROWID。ROWID 可以快速定位表 block 和行位置。
flowchart TD
A["B-tree Leaf<br/>key + ROWID"] --> B["TABLE ACCESS BY INDEX ROWID"]
B --> C["读取表块"]
C --> D["通过 Undo + SCN 做一致性读"]返回少量行时,ROWID 回表非常高效;返回大量行时,可能造成大量随机读。此时优化器可能认为全表扫描更便宜。Oracle 排查时要特别看 TABLE ACCESS BY INDEX ROWID 次数、逻辑读、物理读、E-Rows/A-Rows 差异和等待事件。
SQL Server:非聚集索引缺列会 Key Lookup
SQL Server 聚集索引叶子层就是数据行,非聚集索引叶子层保存索引键和行定位器。如果查询字段不在非聚集索引里,会出现 Key Lookup。
flowchart TD
A["Nonclustered Index Seek"] --> B["得到聚集键"]
B --> C["Key Lookup"]
C --> D["回聚集索引叶子层取完整行"]少量 Key Lookup 很正常;大量 Key Lookup 会拖慢查询。可以用 include 做覆盖索引,但 include 列越多,索引越宽,写入、备份、Buffer Pool 压力越大。
为什么“走了索引还是慢”
面试和生产排查里,最危险的回答是:“已经走索引了,应该没问题”。走索引只是说明数据库选择了某个访问路径,不代表扫描范围小、回表少、排序少、锁等待少。
| 慢的原因 | MySQL 表现 | PostgreSQL 表现 | Oracle 表现 | SQL Server 表现 |
|---|---|---|---|---|
| 扫描范围大 | rows 大,覆盖索引仍扫很多 | actual rows 大,Buffers 高 | A-Rows 大,逻辑读高 | Actual Rows 大,logical reads 高 |
| 回表/Lookup 多 | 二级索引回聚簇索引 | heap fetch 多 | ROWID 回表多 | Key Lookup 多 |
| 统计信息不准 | 选错索引或 Join 顺序 | estimated/actual 差距大 | E-Rows/A-Rows 差距大 | Estimated/Actual 差距大 |
| 排序或临时空间 | Using filesort、临时表 | Sort 节点、磁盘临时文件 | SORT、TEMP 使用高 | Sort/Hash spill 到 TempDB |
| 版本/锁问题 | 长事务、锁等待、undo 压力 | VACUUM 被阻塞、dead tuple 多 | Undo 不足、snapshot too old | 阻塞、RCSI 版本存储、TempDB |
排查顺序应该是:
flowchart TD
A["SQL 慢"] --> B["看真实执行计划"]
B --> C["比较估算行数和实际行数"]
C --> D["看逻辑读、物理读、回表或 Lookup"]
D --> E["看排序、临时空间、Join 算法"]
E --> F["看锁等待、版本清理、日志和 IO"]
F --> G["再决定改 SQL、索引、统计信息、表设计或架构"]从零到精通必须会的迁移思维
如果你从 MySQL 学到 Oracle、PostgreSQL、SQL Server,不能只替换语法,要替换思维模型。
| MySQL 经验 | 迁移时要重新理解 |
|---|---|
| 覆盖索引减少回表 | PostgreSQL 还要看 Visibility Map,SQL Server 要看 include,Oracle 要看 ROWID 回表成本 |
EXPLAIN rows | PostgreSQL/Oracle/SQL Server 也有估算行数,但真实验证方式不同 |
| undo 版本链 | PostgreSQL 旧版本在 heap tuple 中,Oracle 用 Undo + SCN,SQL Server 可用 TempDB 行版本 |
| redo + binlog | PostgreSQL WAL、Oracle Redo、SQL Server Transaction Log 的职责边界不同 |
| 深分页慢 | 四种数据库都会慢,但语法和优化手段不同,核心都是避免跳过大量行 |
| 自增主键 | Oracle sequence、PostgreSQL sequence/identity、SQL Server identity 都不保证业务连续 |
真正的“从零到精通”不是背完每个数据库的名词,而是看到同一类业务问题时,能说清楚:
- 事实数据存在哪里。
- SQL 经过哪些层。
- 优化器为什么选这个计划。
- 索引命中后如何定位完整行。
- 并发下如何判断可见性。
- 提交后靠什么日志恢复。
- 慢的时候用什么证据排查。
面试标准回答
MySQL 为什么常说 B+Tree,Oracle、PostgreSQL、SQL Server 为什么常说 B-tree
标准回答:
MySQL InnoDB 教学中常说 B+Tree,是为了强调非叶子节点主要保存键和指针、叶子节点保存数据或主键,并且叶子节点有序链表适合范围扫描。Oracle、PostgreSQL、SQL Server 官方文档里常说 B-tree,更多是索引家族和术语习惯,不代表它们使用简单二叉树,也不代表它们就是教科书里内部节点保存完整数据的普通 B 树。真正要比较的是叶子层保存什么以及如何定位数据:InnoDB 主键索引叶子层是整行,二级索引保存主键;PostgreSQL B-tree 保存 TID 回 heap;Oracle 保存 ROWID;SQL Server 聚集索引叶子层是数据行,非聚集索引通过行定位器 Lookup。追问点:
- 为什么 B+Tree 适合范围查询?
- 为什么非叶子节点不放整行能降低树高?
- 为什么二级索引可能回表?
- PostgreSQL 的 heap tuple 可见性检查是什么?
为什么同一条 SQL 在 MySQL、PostgreSQL、Oracle、SQL Server 上性能不同
标准回答:
因为 SQL 文本只是逻辑请求,真正性能取决于存储组织、索引叶子层、统计信息、执行计划、可见性判断、回表方式和缓存命中。MySQL InnoDB 二级索引命中后可能通过主键回聚簇索引;PostgreSQL B-tree 通过 TID 回 heap,还要做 MVCC 可见性判断,Index Only Scan 也受 Visibility Map 影响;Oracle B-tree 通过 ROWID 回表,并基于 Undo + SCN 做一致性读;SQL Server 非聚集索引缺列时会 Key Lookup。即使都叫走索引,扫描范围、回表次数、排序、锁等待和版本清理成本都可能完全不同。追问点:
- 为什么 Index Seek 不一定快?
- PostgreSQL Index Only Scan 为什么还可能访问 heap?
- Oracle ROWID 回表什么时候不如全表扫描?
- SQL Server Key Lookup 多了怎么优化?
- MySQL 覆盖索引为什么大表场景仍可能慢?
四种数据库事务日志有什么区别
标准回答:
事务日志的共同目标是保证提交、回滚、崩溃恢复和复制。MySQL InnoDB redo log 用于崩溃恢复,undo log 用于回滚和 MVCC,binlog 用于复制和时间点恢复;PostgreSQL WAL 同时服务崩溃恢复、流复制和归档恢复;Oracle 通过 Redo 保证恢复和 Data Guard,通过 Undo 做回滚和一致性读,SCN 作为一致性时间点;SQL Server Transaction Log 用于回滚、恢复、日志备份和 Always On。名字不同,本质都是围绕“先写日志,再让数据页异步落盘”来保证故障后可恢复。追问点:
- MySQL 为什么 redo 和 binlog 要两阶段提交?
- PostgreSQL 为什么需要 VACUUM?
- Oracle 快照过旧和 Undo 有什么关系?
- SQL Server 日志暴涨可能是什么原因?
关联学习
| 知识点 | 说明 |
|---|---|
| 关系型数据库底层原理 | 从 SQL 层、优化器、页、索引、事务、日志串起来 |
| MySQL 从零到生产级掌握 | InnoDB、B+Tree、redo/undo/binlog、主从复制 |
| PostgreSQL 从零到生产级掌握 | heap tuple、WAL、VACUUM、扩展索引 |
| Oracle 从零到生产级掌握 | SGA/PGA、Undo、Redo、SCN、Data Guard |
| SQL Server 从零到生产级掌握 | 聚集索引、事务日志、锁、RCSI、参数嗅探 |
| MySQL B+Tree 与索引家族 | 深入理解 B+Tree、B-tree、Hash、Bitmap、GIN、GiST |
| MySQL EXPLAIN 执行计划 | 理解 rows、key、type、Extra 等字段来源 |
