跳到主要内容

MySQL 的数据页结构

数据页概述

InnoDB 存储引擎以页为单位管理数据,默认页大小为 16KB。数据页是 MySQL 存储和读取数据的基本单元,理解其结构对于数据库性能优化至关重要。

数据页内部结构

记录组织方式

页面内的记录通过单向链表连接,按主键顺序排列:

这种设计带来的好处:

  • 顺序访问高效:主键范围查询时,相邻记录在物理上也相邻
  • 插入有序:使用自增主键时,新记录总是追加到末尾
  • 页目录加速:通过稀疏索引快速定位记录组

页目录结构

页目录将记录分组,每组4-8条记录,每个槽位指向组内最大记录:

// 伪代码:页目录查找过程
func FindRecordInPage(page *Page, targetKey int) *Record {
// 1. 在页目录中二分查找确定记录组
slotIndex := binarySearchSlots(page.directory, targetKey)

// 2. 在确定的组内线性查找
startRecord := page.directory[slotIndex-1].getRecord()
for current := startRecord; current != nil; current = current.next {
if current.key == targetKey {
return current
}
if current.key > targetKey {
break
}
}
return nil
}

空间管理

当页面空闲空间不足时,InnoDB会执行页分裂操作,这是一个较为昂贵的操作。

数据页之间的组织结构

页面链表连接

同一层级的数据页通过双向链表连接,支持高效的范围查询:

B+树多层结构

数据页组织成B+树结构,内部节点存储索引,叶子节点存储数据:

这种结构的优势:

  • 查询效率:O(log n)的查找复杂度
  • 范围查询:叶子节点链表支持高效顺序扫描
  • 插入性能:新数据总是插入到叶子节点

页面分裂与合并

页面分裂发生在空间不足时:

// 伪代码:页面分裂过程
func SplitPage(fullPage *Page, newRecord *Record) {
// 1. 创建新页面
newPage := createNewPage()

// 2. 选择分裂点(通常是中间位置)
splitPoint := findSplitPoint(fullPage)

// 3. 移动后半部分记录到新页面
moveRecords(fullPage, newPage, splitPoint)

// 4. 更新页面链表
newPage.nextPage = fullPage.nextPage
fullPage.nextPage = newPage

// 5. 在父节点插入新的索引项
insertIndexEntry(parentPage, newPage.minKey, newPage.pageNumber)
}

页面合并发生在页面利用率过低时,避免空间浪费。

性能优化要点

主键设计

// 不推荐:UUID主键导致页面分裂
type OrderBad struct {
ID string `gorm:"primary_key"` // UUID随机插入
Amount decimal.Decimal
CreateAt time.Time
}

// 推荐:自增主键保证顺序插入
type OrderGood struct {
ID int64 `gorm:"primary_key;auto_increment"` // 顺序插入
Amount decimal.Decimal
CreateAt time.Time
}

查询优化

利用页面局部性

-- 高效:连续的主键范围,减少页面访问
SELECT * FROM orders WHERE id BETWEEN 1000 AND 1100;

-- 低效:分散的主键值,增加随机I/O
SELECT * FROM orders WHERE id IN (1, 500, 1000, 1500, 2000);

批量操作优化

func BatchInsert(orders []Order) error {
// 按主键排序,充分利用页面缓存
sort.Slice(orders, func(i, j int) bool {
return orders[i].ID < orders[j].ID
})

// 批量插入减少页面分裂
return db.CreateInBatches(orders, 1000).Error
}

监控关键指标

-- 查看页面分裂情况
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_split';

-- 查看页面合并情况
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_pages_merge%';

-- 查看缓冲池使用情况
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';

总结

MySQL数据页的设计体现了在存储效率、查询性能和维护成本之间的精妙平衡:

  • 页内结构:通过记录链表 + 页目录的组合,实现了高效的记录定位
  • 页间结构:通过B+树 + 双向链表,支持了高效的点查询和范围查询
  • 空间管理:通过页分裂和合并机制,保持了良好的空间利用率

理解这些原理有助于我们:

  1. 设计更合理的数据库schema
  2. 编写更高效的SQL查询
  3. 进行有针对性的性能调优
  4. 理解各种数据库配置参数的作用机制

数据页作为MySQL存储的基石,其设计思想值得每个数据库开发者深入理解。