数据库必知必会系列:存储引擎与数据存储结构

发布于:2023-09-27 ⋅ 阅读:(70) ⋅ 点赞:(0)

作者:禅与计算机程序设计艺术

1.简介

互联网技术的蓬勃发展,数据量的飞速增长,使得海量数据的存储和分析成为系统工程中不可替代的重要组成部分。而数据库作为关系型数据库管理系统(RDBMS)的一种,其独特的存储结构和存储机制,同样十分重要。在本系列文章中,将从底层数据库的存储结构以及各类存储引擎,以及数据库存储的数据类型,入手,带领大家对数据库的存储结构有全面的认识。

首先,为什么需要数据库?数据库的主要作用就是用来存储、组织、检索和管理海量的数据。目前,无论是传统的企业级数据库还是云计算时代的分布式数据库,都已经成为当今信息化建设中的关键工具。无论从规模、性能、安全性、可用性等方面衡量,数据库都是综合应用和支撑业务运营的基础设施,因此,掌握好数据库存储结构及相关存储引擎对于相关人员日后的职业生涯至关重要。

其次,数据库的存储结构到底是什么?从最简单的记录型数据库到关系型数据库再到 NoSQL,数据库的存储结构一直伴随着变化,本文将介绍一些常见的数据库存储结构及其演进过程。

最后,数据库存储的数据类型又有哪些?不同类型的数据用不同的存储方法才能有效地利用硬件资源。因此,了解数据库存储的数据类型,可以更好地理解数据库设计及优化策略。通过阅读本文,读者可以全面、深入地了解数据库存储结构及相关知识。

2.基本概念术语说明

2.1 数据模型

数据模型(Data Model)是指定义和描述数据及其结构、特征、关系和操作的方法和规则。数据模型用于对现实世界中现存的数据进行抽象、整合和描述。数据模型一般包括实体-联系模型、面向对象模型、层次模型、网状模型、关系模型、星型模型等。在数据库中,数据模型通常对应于表、视图、触发器、函数等。

2.2 数据仓库

数据仓库(Data Warehouse)是为了支持业务决策而建立起来的一种集成、汇总、报告的多维数据集合,它是一个按照主题组织、结构清晰、数据一致性强的大型仓库。数据仓库拥有完整的事务处理、分析和数据挖掘功能,能够提供海量、高质量的信息,为企业提供决策支持和制定重点项目提供了数据支持。数据仓库通常作为企业内部或外部的共享资源存在,并由 IT、业务、科研部门共同管理。

2.3 存储引擎

存储引擎(Storage Engine)也称为数据库引擎,是存储和提取数据的软件模块。存储引擎实现了数据库技术中关于数据库的核心服务功能,包括数据持久性、索引和查询等。数据库系统中负责管理底层存储结构的模块统称为存储引擎。

数据库中的各种存储引擎,如 B+ 树索引、B- 树索引、hash 索引、聚集索引、堆文件、LSM 树、页压缩、随机访问日志(WAL),以及位图索引等。其中,B+ 树索引和 hash 索引较为成熟,支持范围查询,B- 树索引支持快速排序;聚集索引按照数据物理位置排列,非聚集索引按照索引键值排列;堆文件是基于磁盘文件的顺序存放方式,随机访问日志(WAL)采用预写日志的方式支持事务,位图索引以位图的形式存储索引数据。

2.4 数据类型

数据类型(Data Type)是指用来表示和存储数据的值、值的集合、值之间的关系的属性。数据类型决定了数据的取值范围、存储大小、操作精度等,不同的类型的数据所占用的空间也不同。常见的数据类型包括整数、浮点数、字符串、日期时间、布尔值、枚举类型、数组、JSON 对象等。

3.核心算法原理和具体操作步骤以及数学公式讲解

3.1 B+ 树索引

B+ 树索引(Balanced Tree Indexing)是一种索引数据结构,它可以高效地找到一个给定的索引值对应的磁盘块。它与其他数据结构如哈希索引、二叉搜索树索引等相比,有以下优点:

  1. 局部性原理:B+ 树的索引节点只包含索引值、指向子节点指针和指向数据块的指针,而不是像二叉查找树一样包含两棵子树。因此,相对于其他数据结构,它的访问速度要快很多。
  2. 分裂平衡:由于每个节点中只包含索引值和数据指针,因此索引结构中不包含类似于二叉树那种指向兄弟节点的指针,从而使得索引结构具有更好的局部性,减少磁盘 I/O 的次数。而且,插入删除数据时还能保持 B+ 树索引的高度平衡,即使在节点上有多个数据项,也不会影响索引结构的高度。

操作步骤

创建索引

创建索引的过程与普通的索引类似,只是在创建索引时,需要指定唯一性约束,并且创建一个唯一的索引名。例如,如果要在 "orders" 表中创建索引 “order_id”,则可执行如下 SQL 命令:

CREATE UNIQUE INDEX order_id ON orders(order_id);
查询数据

查询数据时,先搜索索引,然后根据索引中的地址直接访问磁盘中的数据。如下图所示,假设用户输入的订单编号为 “007”。搜索索引的过程与普通索引搜索过程相同,先找到“order_id”字段对应的索引块,然后从该索引块中找到第一个索引值等于“007”的条目,并得到相应的数据块。然后再从该数据块中读取数据。

插入数据

插入数据时,先检查是否存在重复值。如果没有重复值,则在相应的数据块末尾添加新的数据项。如果存在重复值,则先删除旧的数据项,再添加新的数据项。这样,可以保证数据的唯一性。

删除数据

删除数据时,先找到要删除的数据项所在的数据块。然后修改相应的索引值,使之指向下一个数据项。如果数据块内只有一条数据,则直接删除该数据块。如果索引节点上的数据项删完后,索引节点上的指针数量小于某个阈值,则删除该索引节点,同时将索引节点上的数据项分配给兄弟节点。

索引文件

索引文件由 B+ 树索引的索引节点、数据块组成。索引节点存放的是索引键值和指向子节点的指针,数据块存放的是真正的数据。

3.2 B- 树索引

B- 树索引(Balanced Tree Indexing)也是一种索引数据结构。它比 B+ 树索引更加简单,并且与其他数据结构如哈希索引、二叉搜索树索引等相比,缺少一些特性,但是它的索引构建速度快、查询速度稳定。

操作步骤

创建索引

创建索引的过程与普通的索引类似,只是在创建索引时,需要指定唯一性约束,并且创建一个唯一的索引名。例如,如果要在 "orders" 表中创建索引 “order_id”,则可执行如下 SQL 命令:

CREATE UNIQUE INDEX order_id ON orders(order_id);
查询数据

查询数据时,先搜索索引,然后根据索引中的地址直接访问磁盘中的数据。如下图所示,假设用户输入的订单编号为 “007”。搜索索引的过程与普通索引搜索过程相同,先找到“order_id”字段对应的索引块,然后从该索引块中找到第一个索引值等于“007”的条目,并得到相应的数据块。然后再从该数据块中读取数据。

插入数据

插入数据时,先检查是否存在重复值。如果没有重复值,则在相应的数据块末尾添加新的数据项。如果存在重复值,则先删除旧的数据项,再添加新的数据项。这样,可以保证数据的唯一性。

删除数据

删除数据时,先找到要删除的数据项所在的数据块。然后修改相应的索引值,使之指向下一个数据项。如果数据块内只有一条数据,则直接删除该数据块。如果索引节点上的数据项删完后,索引节点上的指针数量小于某个阈值,则删除该索引节点,同时将索引节点上的数据项分配给兄弟节点。

索引文件

索引文件由 B- 树索引的索引节点、数据块组成。索引节点存放的是索引键值和指向子节点的指针,数据块存放的是真正的数据。

3.3 Hash 索引

Hash 索引(Hash Indexing)是一种索引数据结构,它通过哈希算法来映射数据值到磁盘块。

操作步骤

创建索引

创建索引的过程与普通的索引类似,只是在创建索引时,需要指定唯一性约束,并且创建一个唯一的索引名。例如,如果要在 "orders" 表中创建索引 “order_id”,则可执行如下 SQL 命令:

CREATE UNIQUE INDEX order_id ON orders(order_id);
查询数据

查询数据时,先计算索引键值对应的哈希值,然后从哈希表中获取对应的数据。如下图所示,假设用户输入的订单编号为 “007”。计算哈希值之后,查找到的哈希表里保存了所有的索引键值和相应的数据块地址。再根据订单编号“007”的哈希值来查找到数据块。最后从数据块中读取数据。

插入数据

插入数据时,先计算索引键值对应的哈希值,然后把键值和相应的数据块地址添加到哈希表中。

删除数据

删除数据时,先计算索引键值对应的哈希值,然后从哈希表中移除相应的数据。

索引文件

索引文件由哈希表组成。哈希表中的每一个元素都保存了一个索引键值和相应的数据块地址。

3.4 聚集索引

聚集索引(Clustered Indexing)是一种索引数据结构,它将数据按照索引的排列顺序存储在磁盘上。

操作步骤

创建索引

创建索引的过程与普通的索引类似,只是在创建索引时,需要指定唯一性约束,并且创建一个唯一的索引名。例如,如果要在 "orders" 表中创建索引 “order_id”,则可执行如下 SQL 命令:

CREATE UNIQUE CLUSTERED INDEX order_id ON orders(order_id);
查询数据

查询数据时,首先在磁盘上搜索索引,然后根据索引中的地址直接访问磁盘中的数据。如下图所示,假设用户输入的订单编号为 “007”。搜索索引的过程与普通索引搜索过程相同,先找到“order_id”字段对应的索引块,然后从该索引块中找到第一个索引值等于“007”的条目,并得到相应的数据块。然后再从该数据块中读取数据。

插入数据

插入数据时,先检查是否存在重复值。如果没有重复值,则追加数据到已有的磁盘数据块,并更新索引文件。如果存在重复值,则先删除旧的数据项,再添加新的数据项。这样,可以保证数据的唯一性。

删除数据

删除数据时,先找到要删除的数据项所在的数据块。然后修改相应的索引值,使之指向下一个数据项。如果数据块内只有一条数据,则直接删除该数据块。如果索引节点上的数据项删完后,索引节点上的指针数量小于某个阈值,则删除该索引节点,同时将索引节点上的数据项分配给兄弟节点。

索引文件

索引文件由索引块、数据块组成。索引块和数据块中的数据项排列顺序与表中实际存储的顺序相同。

3.5 非聚集索引

非聚集索引(Nonclustered Indexing)是一种索引数据结构,它将数据按照索引键值的顺序存储在磁盘上。

操作步骤

创建索引

创建索引的过程与普通的索引类似,只是在创建索引时,需要指定唯一性约束,并且创建一个唯一的索引名。例如,如果要在 "orders" 表中创建索引 “order_id”,则可执行如下 SQL 命令:

CREATE NONCLUSTERED INDEX order_id ON orders(order_id);
查询数据

查询数据时,首先在磁盘上搜索索引,然后根据索引中的地址直接访问磁盘中的数据。如下图所示,假设用户输入的订单编号为 “007”。搜索索引的过程与普通索引搜索过程相同,先找到“order_id”字段对应的索引块,然后从该索引块中找到第一个索引值等于“007”的条目,并得到相应的数据块。然后再从该数据块中读取数据。

插入数据

插入数据时,先检查是否存在重复值。如果没有重复值,则追加数据到已有的磁盘数据块,并更新索引文件。如果存在重复值,则先删除旧的数据项,再添加新的数据项。这样,可以保证数据的唯一性。

删除数据

删除数据时,先找到要删除的数据项所在的数据块。然后修改相应的索引值,使之指向下一个数据项。如果数据块内只有一条数据,则直接删除该数据块。如果索引节点上的数据项删完后,索引节点上的指针数量小于某个阈值,则删除该索引节点,同时将索引节点上的数据项分配给兄弟节点。

索引文件

索引文件由索引块、数据块组成。索引块中的数据项排列顺序与表中实际存储的顺序相同,数据块中则按索引键值的顺序存储。

4.具体代码实例和解释说明

4.1 堆文件

堆文件(Heap File)是基于磁盘文件的顺序存放方式,随机访问日志(WAL)采用预写日志的方式支持事务。堆文件只支持插入和删除操作。

操作步骤

创建堆文件

创建一个新的堆文件,其目录由数据库管理系统自动生成。文件名为 heapfile.dat,可以通过调用 fopen 函数打开该文件。

FILE *heapFile = fopen("heapfile.dat", "wb"); // 创建堆文件
写入数据

向堆文件中写入数据,每次写入之前需要调用 fseek 函数定位到文件尾部。

int id = 1;
fwrite(&id, sizeof(int), 1, heapFile);    // 写入 int 类型数据
读取数据

读取数据时,首先需要调用 ftell 函数获取当前偏移量,然后读取数据。

int currentPos = ftell(heapFile);        // 获取当前偏移量
rewind(heapFile);                        // 重新设置文件读取位置
int value;
if (currentPos % sizeof(int) == 0) {
    if ((currentPos / sizeof(int)) >= 1) {
        fread(&value, sizeof(int), 1, heapFile);   // 从头开始读取
    } else {
        printf("There is no data to read.\n");
    }
} else {
    printf("The data size must be a multiple of the int type size.");
}
删除数据

删除数据时,首先找到要删除的数据项所在的数据块,然后修改相应的索引值,使之指向下一个数据项。如果数据块内只有一条数据,则直接删除该数据块。如果索引节点上的数据项删完后,索引节点上的指针数量小于某个阈值,则删除该索引节点,同时将索引节点上的数据项分配给兄弟节点。

// To delete an item from the heap file:
void HeapFileDeleteItem(FILE* heapFile, unsigned long offset, void* buffer)
{
    // find the block that contains this record and update its pointer fields appropriately
    int blockSize = 4096;      // assume all blocks are this size for now
    char* ptrToBlockStart = NULL;

    // calculate the start address of the containing block
    ptrToBlockStart = static_cast<char*>(offset - (offset % blockSize));

    // lock the block into memory so we can access it safely
    FILE* tempFile = tmpfile();
    fseek(tempFile, ptrToBlockStart, SEEK_SET);
    fwrite(ptrToBlockStart, blockSize, 1, tempFile);
    rewind(tempFile);

    // now we can safely modify the pointers in the block as needed...
    //...by reading them out of place first
    int previousOffset = 0;
    fread(&previousOffset, sizeof(int), 1, tempFile);
    fread(buffer, blockSize, 1, tempFile);
    fclose(tempFile);

    // update the pointers in the original block to skip over this one
    // note: leave room at the beginning of the block for the new header
    int nextOffset = *(static_cast<int*>(static_cast<char*>(buffer) + blockSize - 4));
    *(static_cast<int*>(static_cast<char*>(ptrToBlockStart))) = nextOffset;     // update the prev offset in curr block
    *(static_cast<int*>(static_cast<char*>(nextOffset) + 4)) = blockSize;       // update the size in next block
    *(static_cast<int*>(static_cast<char*>(offset))) = 0xFFFFFFFF;            // mark this slot deleted by setting to -1

}

4.2 随机访问日志

随机访问日志(Write Ahead Log,WAL)是一种提交日志的机制,它可以帮助确保数据库操作的原子性,即要么全部完成,要么完全不执行。它可以防止系统崩溃或电源故障导致的数据丢失。

操作步骤

概念

WAL 是一种提交日志的机制,它可以帮助确保数据库操作的原子性。提交日志是一种特殊的文件,它包含待提交的事务的所有变更。事务提交前,先将其变更记录在提交日志中。当事务提交后,才会将变更应用到真实的数据中。

优点
  1. 提升性能:WAL 可以提升性能,因为它可以在一定程度上减少磁盘写操作的数量,因此可以提高写入吞吐率。
  2. 提升容错能力:WAL 可以帮助降低因系统崩溃或其他错误导致的数据丢失的概率。
使用步骤
  1. 初始化:启动数据库时,初始化一个日志文件。
  2. 事务准备:将所有准备提交的事务的变更记录在日志文件中。
  3. 提交事务:将事务的状态标记为成功,确保事务的原子性。
  4. 事务恢复:当发生系统崩溃、错误或其他意外事件时,可以从日志文件中恢复事务。

文件格式

日志文件由事务日志记录块(TLRBs)组成。每个 TLRB 中包含一条事务的变更。每个 TLRB 的大小为 8KB。

5.未来发展趋势与挑战

数据库存储结构是数据库发展的一个重要方向,也是数据库工程师需要关注的内容。由于数据库的核心服务是处理海量数据,因此数据库的存储结构始终处于一个动态的状态,充满了挑战与机遇。

未来,数据库存储结构可能会进一步发展。在此过程中,数据库引擎也可能发生巨大的变化。为了应对这种变化,数据库工程师必须保持敏锐的嗅觉,保持跟踪最新技术的步伐,学习存储技术的最新进展。

另外,由于数据量的不断增长,数据库系统的效率也越来越重要。因此,数据库的存储结构也需要在性能、成本、可用性和可扩展性之间取得平衡。数据库的存储结构还需要考虑到许多复杂的因素,如数据库、硬件环境、操作系统等。

6.附录常见问题与解答


网站公告

今日签到

点亮在社区的每一天
去签到