MySQL存储引擎之:`InnoDB`的`Clustered Index`:其在主键索引和数据页中的物理存储。

好的,我们开始今天的讲座,主题是 MySQL InnoDB 存储引擎中的聚集索引(Clustered Index),重点讲解它在主键索引和数据页中的物理存储方式。

InnoDB 聚集索引原理与物理存储

InnoDB 是 MySQL 中最常用的存储引擎,它是一种索引组织表(Index Organized Table),这意味着表的数据存储方式是按照主键的顺序进行组织的。这种组织方式的核心就是聚集索引。

1. 聚集索引的定义

聚集索引不是一种单独的索引类型,而是一种数据存储方式。在 InnoDB 中,表的数据实际上存储在索引的叶子节点上。更具体地说,聚集索引决定了表中数据的物理存储顺序。

  • InnoDB 的聚集索引: 实际上就是表本身。
  • 叶子节点存储: 叶子节点存储的是完整的行数据。

2. 聚集索引的特性

  • 每张表只有一个聚集索引: 因为数据只能以一种物理顺序存储。
  • 主键是聚集索引的依据: 如果表中定义了主键,InnoDB 会使用主键作为聚集索引。
  • 没有主键时的处理: 如果表中没有定义主键,InnoDB 会选择一个唯一的非空索引作为聚集索引。
  • 没有合适索引时的处理: 如果既没有主键,也没有合适的唯一非空索引,InnoDB 会隐式定义一个 rowid 作为聚集索引。

3. 聚集索引的物理存储:数据页

InnoDB 将数据存储在逻辑上连续的数据页(Data Page)中。默认情况下,每个数据页的大小是 16KB。这些数据页通过双向链表连接,形成一个有序的 B+ 树结构。

  • 数据页结构: 数据页包含行数据、页头、页尾等信息。
  • B+ 树结构: 聚集索引通过 B+ 树实现,叶子节点存储实际的行数据。

4. 主键索引与聚集索引的关系

在 InnoDB 中,如果表定义了主键,那么主键就是聚集索引。主键索引和聚集索引是同一个概念。

代码示例:创建一个带主键的表

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(255),
    email VARCHAR(255),
    created_at TIMESTAMP
);

在这个例子中,id 字段被定义为主键。InnoDB 会使用 id 字段作为聚集索引,按照 id 的顺序存储数据。

5. 数据插入过程

当插入新数据时,InnoDB 会尝试将数据插入到聚集索引的合适位置,以保持数据的有序性。

  • 顺序插入: 如果按照主键顺序插入数据,InnoDB 可以直接将数据追加到最后一个数据页。
  • 随机插入: 如果插入的数据的主键值不是顺序的,InnoDB 需要在 B+ 树中查找合适的位置,并将数据插入到相应的数据页中。
  • 页分裂: 如果数据页已满,插入新数据会导致页分裂。InnoDB 会将数据页拆分成两个或多个数据页,并将部分数据移动到新的数据页中。这会影响性能。

代码示例:插入数据

INSERT INTO users (id, username, email, created_at) VALUES
(1, 'john.doe', '[email protected]', NOW()),
(2, 'jane.doe', '[email protected]', NOW()),
(3, 'peter.pan', '[email protected]', NOW());

6. 数据查询过程

当使用主键进行查询时,InnoDB 可以通过聚集索引快速定位到目标数据。

  • B+ 树搜索: InnoDB 从 B+ 树的根节点开始,沿着索引路径向下搜索,直到找到包含目标主键值的数据页。
  • 数据页读取: 读取包含目标数据的数据页,并返回结果。

代码示例:通过主键查询数据

SELECT * FROM users WHERE id = 2;

7. 二级索引(辅助索引)

除了聚集索引,InnoDB 还支持二级索引(Secondary Index),也称为辅助索引。二级索引不是聚集索引,它不会影响数据的物理存储顺序。

  • 二级索引存储: 二级索引的叶子节点存储的是索引列的值和对应行的主键值。
  • 回表: 当通过二级索引查询数据时,InnoDB 首先通过二级索引找到对应的主键值,然后通过聚集索引找到完整的行数据。这个过程称为回表(Lookup)。

代码示例:创建一个二级索引

CREATE INDEX idx_username ON users (username);

8. 覆盖索引

如果查询只需要使用索引列的值,而不需要访问完整的行数据,那么可以使用覆盖索引(Covering Index)来避免回表操作。

  • 覆盖索引的优势: 覆盖索引可以提高查询性能,减少 IO 操作。

代码示例:覆盖索引查询

SELECT username FROM users WHERE username = 'john.doe';

如果 username 列上存在索引,并且查询只需要 username 列的值,那么可以使用覆盖索引,避免回表操作。

9. 聚集索引的优势与劣势

  • 优势:
    • 查询速度快: 通过主键进行查询时,速度非常快。
    • 范围查询优化: 范围查询可以通过聚集索引进行优化。
    • 数据局部性: 相关的数据存储在相邻的数据页中,可以减少 IO 操作。
  • 劣势:
    • 插入速度慢: 随机插入数据可能导致页分裂,影响插入性能。
    • 二级索引需要回表: 通过二级索引查询数据需要回表,增加 IO 操作。
    • 占用空间大: 聚集索引会占用大量的存储空间。

10. 如何优化聚集索引的使用

  • 选择合适的主键: 选择自增的主键可以避免页分裂,提高插入性能。
  • 避免频繁的随机插入: 尽量按照主键顺序插入数据。
  • 使用覆盖索引: 尽量使用覆盖索引来避免回表操作。
  • 定期维护索引: 定期进行索引优化,可以提高查询性能。

11. 聚集索引的物理存储图示

为了更直观地理解聚集索引的物理存储方式,我们可以用一个简化的图示来表示:

+-----------------+     +-----------------+     +-----------------+
|   数据页 1      | --> |   数据页 2      | --> |   数据页 3      |
+-----------------+     +-----------------+     +-----------------+
|   [主键1, 数据] |     |   [主键4, 数据] |     |   [主键7, 数据] |
|   [主键2, 数据] |     |   [主键5, 数据] |     |   [主键8, 数据] |
|   [主键3, 数据] |     |   [主键6, 数据] |     |   [主键9, 数据] |
+-----------------+     +-----------------+     +-----------------+

(双向链表连接)

在这个图示中,每个数据页存储了多行数据,数据按照主键的顺序排列。数据页之间通过双向链表连接,形成一个有序的链表。

12. 聚集索引与非聚集索引的对比

为了更好地理解聚集索引,我们可以将其与非聚集索引进行对比:

特性 聚集索引 (InnoDB) 非聚集索引 (MyISAM)
数据存储方式 索引组织表 堆表
叶子节点存储内容 完整行数据 行指针
每表索引数量 1 多个
查询效率 主键查询快 回表可能影响效率

13. 一些实际场景的考虑

  • 自增主键 vs. UUID: 使用自增主键可以避免页分裂,提高插入性能。但是,在分布式系统中,可能需要使用 UUID 作为主键。在这种情况下,需要权衡性能和数据一致性。
  • 大表的分区: 对于大表,可以考虑使用分区表来提高查询性能。
  • 索引的维护: 定期进行索引优化,可以提高查询性能。可以使用 OPTIMIZE TABLE 命令来优化表。

14. 模拟聚集索引的创建和查询(Python)

虽然不能直接模拟 InnoDB 内部的存储机制,我们可以使用 Python 模拟一个简单的聚集索引的创建和查询过程。

class DataPage:
    def __init__(self, page_size=3):
        self.data = []
        self.page_size = page_size

    def is_full(self):
        return len(self.data) >= self.page_size

    def insert(self, key, value):
        if self.is_full():
            return False
        self.data.append((key, value))
        self.data.sort(key=lambda x: x[0]) # 保持顺序
        return True

    def get(self, key):
        for k, v in self.data:
            if k == key:
                return v
        return None

    def __repr__(self):
        return str(self.data)

class ClusteredIndex:
    def __init__(self):
        self.pages = [DataPage()]

    def insert(self, key, value):
        for page in self.pages:
            if not page.is_full():
                if page.insert(key, value):
                    return True
        # 如果所有页都满了,创建新页
        new_page = DataPage()
        if new_page.insert(key, value):
            self.pages.append(new_page)
            self.pages.sort(key=lambda page: page.data[0][0] if page.data else float('inf')) # 页排序
            return True
        return False

    def get(self, key):
        for page in self.pages:
            value = page.get(key)
            if value:
                return value
        return None

    def __repr__(self):
        return str(self.pages)

# 示例使用
index = ClusteredIndex()
index.insert(1, "Value1")
index.insert(3, "Value3")
index.insert(2, "Value2")
index.insert(4, "Value4")
index.insert(5, "Value5")
index.insert(6, "Value6")

print(index)
print(index.get(3))
print(index.get(7)) # 不存在

这个 Python 代码模拟了一个简单的聚集索引,包括数据页的创建、数据的插入和查询。它不涉及真实的 B+ 树结构,但可以帮助理解聚集索引的基本原理。

InnoDB 聚集索引的物理存储方式

总结:InnoDB 的聚集索引决定了数据的物理存储顺序,主键是聚集索引的主要依据,数据存储在数据页中,并通过 B+ 树结构进行组织。

聚集索引的优缺点与优化

总结:聚集索引的查询速度快,范围查询优化,但插入速度慢,二级索引需要回表。选择合适的主键,避免频繁的随机插入,使用覆盖索引等方法可以优化聚集索引的使用。

希望今天的讲座能够帮助大家更好地理解 InnoDB 存储引擎中的聚集索引。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注