Database

  • 最近,业余时间都放在《高性能MySQL 第四版》的翻译工作了,以至于这个行业动态已经拖了将近三个月没有更新了。那,今天,我们就一起来看看在2022年的第一个季度,各个厂商的云数据库都有什么新进展吧。

    重点更新

    • Azure Data Studio持续更新,发布了Table Designer、Query Plan Viewer等功能。虽然,SQL Server最权威的管理工具一直是SSMS,不过最近看到MS也在快速更新Azure Data Studio。相比,SSMS,ADS是一个跨平台的产品,可以同时支持Windows、MacOS、Linux,可以看到MS在以云为战略核心之后,开发者、开放、开源都是其核心策略。

    • 火山引擎,自去年12月发布之后,也在快速迭代,最新发布了PostgreSQL的支持,不过,即现在记住火山引擎官网的域名还是有点难度的,不打算改吗? www.volcengine.com 你们都记得住吗?

    • AWS发布自己的JDBC for MySQL,并推荐客户使用,在全力推Aurora的情况下,又推出自己的JDBC,是在准备随时和Oracle全面脱钩吗?

    • AWS RDS通过类似半同步复制的机制,也推出三节点形态,相比EBS复制,这种逻辑复制可以让Standby有更好的性能,同时可以直接提供读服务:参考

    更新详情

    • [AWS] RDS for PostgreSQL支持了tds_fdw/mysql_fdw
    • [AWS] RDS Multi-AZ Cluster新增更多区域支持 参考
    • [AWS] RDS开始支持 Oracle 21c
    • [AWS] Aurora PostgreSQL支持大版本升级,例如从9.6升级到11.X:参考
    • [AWS] AWS JDBC for MySQL正式GA:参考
    • [AWS] RDS SQL Server 2007标准版支持Always On AG: 参考
    • [AWS] RDS MariaDB开始支持延迟复制:参考
    • [AWS] MemoryDB for Redis开始支持 PrivateLink:参考
    • [AWS] RDS开始支持PostgreSQL 14:参考
    • [MariaDB] 10.9版本发布,将增强JSON支持、异步redo等:参考
    • [Azure] Microsoft Defender支持保护Cosmos DB:参考
    • [Azure] Cosmos DB开始支持MongoDB 4.2 API:参考
    • [Azure] Azure Data Studio发布了Table Designer、Query Plan Viewer:参考
    • [GCP] Memorystore for Redis发布Read Replicas、RDB Snapshots等功能:参考
    • [GCP] Spanner发布了Optimizer v4,提升了二级索引、哈希JOIN等相关功能:参考
    • [GCP] Cloud SQL for MySQL支持了8.0.26,并作为默认版本:参考
    • [GCP] Cloud SQL for SQL Server支持了跨区域的副本:参考
    • [GCP] Cloud SQL for SQL Server 2019成为默认的SQL Server版本:参考
    • [阿里云] AnalyticDB PostgreSQL发布跨实例数据共享:参考
    • [阿里云] AnalyticDB PostgreSQL发布Serverless实例类型:参考
    • [阿里云] RDS MySQL只读实例支持开启binlog:参考
    • [阿里云] RDS PostgreSQL 14大版本发布:参考
    • [阿里云] Tair(Redis企业版)现已经开放TairTS时序数据结构、TairCpc数据结构:参考
    • [腾讯] TDB for MySQL支持了连接池功能:参考
    • [腾讯] TDB for SQL Server支持了数据库维度多任务并行、备份数据开始商业化计费:参考
    • [腾讯] TDB for PostgreSQL支持了跨可用区容灾、克隆实例、跨可用区创建只读实例等功能
    • [腾讯] TDB for Redis支持了全球复制功能:参考
    • [腾讯] TDSQL-C MySQL 8.0版本增加了只读节点等功能:参考
    • [腾讯] TDSQL PostgreSQL推出Oracle 兼容版的集中式版:参考
    • [腾讯] DTS支持了跨账号实例间数据同步,支持了更多的源/目标的组合:参考
    • [华为云] GaussDB(for Mongo)提供了多种数据迁移方案:参考
    • [华为云] GaussDB(for Redis)包周期(类似于包年包月)实例支持规格变更:参考
    • [华为云] 数据复制服务 DRS 实时同步支持DB2 for LUW 10.5、11.5、PostgreSQL->Kafka等
    • [阿里云] RDS PostgreSQL 支持机器学习MADlib插件:参考
    • [阿里云] 图数据库GDB自动机器学习组件发布:参考
    • [火山引擎] 云数据库 PostgreSQL 版正式发布上线:参考
  • 概述

    使用的Amazon Linux 2,相当于是CentOS 7,于是使用了官方的yum repo来进行安装。

    官方文档的参考:Linux downloads (Red Hat family)@postgresql.org

    添加yum仓库

    /etc/yum.repos.d/pgdg.repo
    [pgdg13]
    name=PostgreSQL 13 for RHEL/CentOS 7 - x86_64
    baseurl=https://download.postgresql.org/pub/repos/yum/13/redhat/rhel-7-x86_64
    enabled=1
    gpgcheck=0

    注意,上述文件中的url需要根据实际情况调整,需要根据主机的发行版本和需要安装的PostgreSQL版本,在仓库中找到对应的目录:目录列表

    更新yum仓库配置信息,并安装postgresql-server

    sudo yum update

    sudo yum install postgresql13-server

    添加执行文件到PATH路径

    export PATH="${PATH}:/usr/pgsql-13/bin"

    准备数据文件(database cluster)

    参考:Creating a Database Cluster

    root# mkdir /usr/local/pgsql
    root# adduser postgres
    root# chown postgres /usr/local/pgsql
    root# su postgres
    
    postgres$ export PATH="${PATH}:/usr/pgsql-13/bin"
    
    postgres$ pg_ctl -D /usr/local/pgsql/data initdb

    启动/关闭postgresql

    pg_ctl start -l logfile -D/usr/local/pgsql/data
    pg_ctl stop -D /usr/local/pgsql/data

    修改配置文件

    vim /usr/local/pgsql/data/postgresql.conf  # 例如修改 shared_buffers = 64MB

    连接数据库

    psql

  • 最近,Amazon RDS Custom开始支持了SQL Server。RDS Custom形态一方面提供托管数据库的安装、管理、弹性等能力,另一方面又提供类似自建数据库的OS访问与配置、驱动程序安装等能力。

    这种形态与阿里云数据库提供的MyBase有一些类似,那是不是同类产品呢?我们从一下几方面来看看Amazon RDS Custom。

    面向的场景:Amazon RDS Custom主要是面向一些比较封闭、传统的应用系统,需要对数据库控制、配置都有非常高要求的应用系统,让系统人员可以接触、控制RDS所运行的主机OS,从而完成这类“封闭、传统”的应用系统配置工作。所以,从这个逻辑出发,RDS Custom优先支持的是Oracle,现在又支持了SQL Server,而不是当下最流行的MySQL或者PostgreSQL。

    提供的能力:RDS Custom向用户提供了底层OS的访问权限,可以让用户一定程度上配置和管理数据库的运行环境。普通的RDS是一种全托管的数据库,用户不用关心数据库的安装配置,更不用关心底层的OS运行情况;如果基于EC2/ECS等构建数据库,则需要用户对OS、数据库做完整地管理与配置。可以这样理解,RDS Custom是一种介于这两种形态之间的一种中间形态,一方面RDS Custom提供了托管数据库地安装、管理、弹性等能力,另一方面又提供类似自建数据库地OS访问与配置、驱动程序安装等能力。下图,比较好的概括了相关能力,并给出了对比:

    一些常见的场景:

    • 在安装数据库时需要安装特定的数据库和OS补丁
    • 需要对数据库做一些特殊的配置
    • 应用系统和数据库需要通过文件的方式传输、共享数据

    RDS Custom的一些优势:

    • 安装、备份/恢复、监控/告警等,依旧可以全托管自动化完成
    • 可以在主机上运行自己的软件,例如某些第三方应用程序等
    • 可以按需的自己安装数据库补丁和OS补丁
    • 可以作为从本地环境迁移到全托管环境的一个过渡
    • 可以运行自己的系统脚本,例如监控、诊断、调度等

    与MyBase的异同:

    • 都提供了主机级别的权限,一方面向用户提供了更大自由度定制数据库和运行OS环境,另外也可以在主机上运行一些额外的软件(例如监控agent等)
    • MyBase比较重要的一点是,提供在主机级别超卖率的配置,可以让用户根据自己应用的实际情况去配置,这就可以在一些非性能关键的场景下,获得非常高的性价比。同时,MyBase也基本是全托管的(自动化安装、备份、监控等),使用起来依旧很建档,让客户更加专注于自己的业务系统。
    • 整体上,定位是不同的。RDS Custom核心是解决用户的部分传统应用部署时候对数据库有一些特殊要求的场景,所以,支持的数据库也是Oracle和SQL Server;而MyBase是提供给用户一个更加自主可控的环境,另外,MyBase是以主机为单位购买,也向用户提供更加高性价比的实例选择,基于此,希望通过这种产品形态,让用户放下一些“顾忌”,选择云数据库上云。

    所以,RDS Custom和MyBase这两个形态看起来有些像,但是出发点、形态、使用上差异也都非常大。不过有一点是一样的,都是在一些较为垂直的场景上,帮助用户更加便利、平滑的完成数据库上云。

    参考:

  • Azure数据库的Flexible Server

    ·

    一直对云数据库比较关注,在去年9月份的微软“Ignite”大会宣布推出的托管数据库“Flexible Server”(后面简称”FS”),虽然一直处于Preview状态,但是依据看到在过去一年中,该版本一直在非常快速的更新,猜测该版本应该会是未来开源托管数据库的主要形态(如有微软朋友可以帮回复确认一下),这里对比之前的”Single Server”(后面简称”SS”),对“Flexible Server”做一个概要性的介绍,详细的介绍可以直接阅读本文结尾处链接中Azure的官方文档。

    关于”Flexible Server”的”TLDR”版

    • Flexible Server就是Azure上使用了新一代底层架构的托管MySQL、PostgreSQL服务
    • 早期Azure上开源数据库托管是基于Windows(参考),称作”Single Server”,新版本托管平台基于Linux,称作Flexible Server
    • 该版本是Azure OSS开发者组2019年左右开始开发,2020年对外宣布,当前处于Preview状态
    • 该版本让开发者在管理实例时,具备更大的灵活性,包括:更多的参数管理、维护窗口控制等
    • 支持了多可用区的高可用,对于企业的核心应用来说,这应该是必须的能力
    • 是未来Azure上开源托管产品的主要形态(这是一个猜测)
    • 版本选择上的建议:
      • 当前,连续要求不高的业务,建议选择FS,因为这将是未来的主打形态
      • 如果稳定的、重要的业务,当前还是建议选择SS,毕竟是经过很长时间验证的产品形态

    继续阅读,可以了解更多关于Flexible Server的详细说明

    (more…)
  • 在前面两篇文章,较为系统介绍了B-TreeB+-Tree的基础。本文则以InnoDB为例,来看看B+-Tree内部排序的实现。

    简单回顾B-Tree存储

    在前面B-Tree基础文章中,我们了解到在一个B-TreeB+-Tree的“节点”通常可以存放很多的“索引入口”。在数据库实践中,通常一个“节点”大小为16KB8KB。一个索引入口的结构一般是:\( (k_i,p_i,\alpha_i) \) 、 \( (k_i,p_i) \)、\( (k_i,\alpha_i) \),具体的,在叶子节点上通常为\( (k_i,\alpha_i) \),而非叶子节点则为: \( (k_i,p_i) \)。

    那么,在实际的设计中,一个“节点”可以存储多个“索引入口”,在非叶子节点,主要存储键值和到子节点的指针,即 \( (k_i,p_i) \);叶子节点中,则存储键值和相关数据,即\( (k_i,\alpha_i) \)。

    问题描述

    在很多的B+-Tree实现中,一个节点可以存储几十、甚至数百个的“索引入口”。而我们在前面看到的示意图中,看起来节点内部的数据似乎是按照键值排好序的,但在实际实现中,这里以InnoDBB+-Tree实现为例,问题要更复杂一些,主要考虑如下:

    • B+-Tree中的“数据”\( \alpha_i \)可能是变长的,并且内容很大,例如200~300个Bytes。例如,在InnoDB的Clustered Index中,表结构是 id int, nick varchar(32),realname varchar(32),birthday date,password char(32),email varchar(60),字符集是utf8mb4
    • 查询搜索是一方面,还需要更快的写入速度,从而提升数据库的事务吞吐量
    • 依旧需要节点内部非常便利的顺序查找,以满足搜索、范围扫描等需要

    以“堆”的形式存储

    为了满足变长、快速存储的要求,InnoDB页面内部使用了典型的“堆”的形式组织数据,这也是数据库数据存储实现的一般模式。即,每次数据写入时,将数据存储在堆的“顶端”(或者中间的空洞),如右图:

    • 对于最后写入的新记录\( (31,\alpha_5) \),不考虑排序,直接放在“堆顶端”
    • 每条记录的大小不同,按“堆”遍历时,可以根据每条记录的元信息,到一条记录的头部(即本记录的尾部)

    但在B+-Tree中遍历时,更多的是需要顺序遍历,例如键值搜索、范围查找等场景。那么,按“堆”存储的记录,就需要一种顺序遍历的方式。

    B+-Tree页内搜索实现概述

    逻辑实现说明

    一种直观的思维是这样,在页面的某个特殊区域,新建一个“数组”(记录指针或偏移),顺序排列所有的记录。每次需要搜索时,则在该数据中进行二分或插值搜索。这种存储模式的优势是,简单而直接,且易于维护。但对于关系型数据库来说,如果数据写入或更新频繁,那么这个数组的维护会有非常频繁的数据拷贝。

    InnoDB的实现(我后面要去看看Jim Gray的那本书,看看是否有描述这些细节)并没有使用这种直接的实现,而是做了一些变化:

    • 在每条被存储的记录Header部分,都存储了按排序顺序的下一条记录的位置(页内偏移量),所以InnoDB的页内记录可以看做是一个按键值大小排序的链表
    • 页面开辟了一片区域(Page Directory)存储“部分”页内记录的偏移,这些记录的选取与Skip List实现类似。即按排序顺序,并按一定的间隔取部分记录放到Page Directory中,并将Page Directory间隔两个记录之间在页内的所有记录称为一个SlotPage Directory中的这条记录则可以称为一个SlotSlot Header

    上右图是在GitHub上搜索到的由SmartKeyerror绘制的关于Page Directory的示意图[6],是比较准确且直观的。

    所以,在进行搜索时(InnoDB的实现):

    • 首先,在Page Directory中的各个Slot Header中做二分查找
    • 然后,再在Slot中进行线性搜索,即逐个向前比较

    InnoDB中,一个Slot有4~8个记录,所以,可以认为一个500条记录的页面,需要做的比较次数约为(考虑平均每个slot中6条记录):

    $$
    log_2(\frac{500}{6}) + \frac{6}{2} \approx 9.38
    $$

    在实际中,可能会有多次CPUCache Miss发生,所以实际的成本比看起来的9.38次要略微高一些,但作为粗略预估,这个计算也是合理的。

    InnoDB 物理存储实现说明

    厘清物理层的实现,还是稍微花了一些时间的。在Jeremy Cole 2013 Efficiently traversing InnoDB B+Trees with the page directory 中对于Page Directory实现页内搜索做了比较详细的说明,并且在讨论中也一些有意思的讨论。但,对于其中的几个问题做了一些验证,还是有一些更细致的补充说明。

    上面,SmartKeyerror绘制的关于Page Directory图在逻辑上是没有太大问题的,但就物理实现,则容易引起一些误解,故绘制了新的示意图如右下,以更准确的反映InnoDB物理存储的结构:

    • Page Directory就是一个存储页内偏移的“数组”,就是右图的深红色部分
    • 每个slot使用2 Bytes,存储的是对应记录的页内偏移

    相比于逻辑结构,物理结构上,需要注意的是:

    • “记录”在页内物理位置,并不是按键值大小排列,而通常是按照插入时间向下生长的
    • Page Directory存储在页面的底部,从高位地址向低位地址扩展;此外,SlotInnoDB中的逻辑概念

    更多关于Page Directory实现细节

    一般的了解了Page Directory的实现逻辑以及概述,基本上就可以了,也算是清楚了InnoDB页内搜索的实现。本小结为一个扩展阅读部分,描述更多关于slot结构、搜索、查找的细节。

    InnoDB的代码参考

    在了解Page Directory的过程中,还是阅读了部分InnoDB的代码,主要是innobase/page/page0page.c以及相关的代码。在该文件中,也有很多的注释可以阅读。这里列举部分函数,算是验证一下上述的部分理解。这里的示例代码来自版本:MySQL-4.1.7

    InnoDB 代码中的Page Directory

    page0page.c 中的注释

    这段注释在page0page.c文件中,是InnoDB最早、也是最为原始的关于page directoryslot的说明与介绍,非常建议阅读。

    另外,这段注释中最后:50 x 4 bytes = 200 bytes这部分估算应该是有错误的,应该是50 x 2 bytes = 100 bytes。在16KB的页面中,2 bytes存储偏移量是够了的,而实际实现中也是2 bytes

    #define PAGE_DIR_SLOT_SIZE  2

    当然,如果一定要“找补”,那么也可以把每条记录中用于存放slot中记录数量的4-bit也算进来的话,那么平均一个slot需要:

    $$ 2+\frac{6*4}{8} \approx 5 $$

    /*          THE INDEX PAGE
                ==============
    
    The index page consists of a page header which contains the page's
    id and other information. On top of it are the the index records
    in a heap linked into a one way linear list according to alphabetic order.
    
    Just below page end is an array of pointers which we call page directory,
    to about every sixth record in the list. The pointers are placed in
    the directory in the alphabetical order of the records pointed to,
    enabling us to make binary search using the array. Each slot n:o I
    in the directory points to a record, where a 4-bit field contains a count
    of those records which are in the linear list between pointer I and
    the pointer I - 1 in the directory, including the record
    pointed to by pointer I and not including the record pointed to by I - 1.
    We say that the record pointed to by slot I, or that slot I, owns
    these records. The count is always kept in the range 4 to 8, with
    the exception that it is 1 for the first slot, and 1--8 for the second slot.
    
    An essentially binary search can be performed in the list of index
    records, like we could do if we had pointer to every record in the
    page directory. The data structure is, however, more efficient when
    we are doing inserts, because most inserts are just pushed on a heap.
    Only every 8th insert requires block move in the directory pointer
    table, which itself is quite small. A record is deleted from the page
    by just taking it off the linear list and updating the number of owned
    records-field of the record which owns it, and updating the page directory,
    if necessary. A special case is the one when the record owns itself.
    Because the overhead of inserts is so small, we may also increase the
    page size from the projected default of 8 kB to 64 kB without too
    much loss of efficiency in inserts. Bigger page becomes actual
    when the disk transfer rate compared to seek and latency time rises.
    On the present system, the page size is set so that the page transfer
    time (3 ms) is 20 % of the disk random access time (15 ms).
    
    When the page is split, merged, or becomes full but contains deleted
    records, we have to reorganize the page.
    
    Assuming a page size of 8 kB, a typical index page of a secondary
    index contains 300 index entries, and the size of the page directory
    is 50 x 4 bytes = 200 bytes. */

    从这里也可以看出来,因为InnoDB的页内地址很多地方是用的2 Bytes的存储偏移量,所以InnoDB单个页面大小也受此限制,最大为 \( 2^{16} \text{Bytes} = 64 \text{KB} \)。(参考:innodb_page_size

    page_dir_get_nth_slot

    该函数输入是一个页面指针,返回该页面的第\( n \)个slot的指针。下右为InnoDB中的源码,下左则是代码部分对应的示意图:

    Gets pointer to nth directory slot. */
    UNIV_INLINE
    page_dir_slot_t* page_dir_get_nth_slot(
                /* out: pointer to dir slot */
        page_t* page,   /* in: index page */
        ulint   n)  /* in: position */
    {
        ut_ad(page_header_get_field(page, PAGE_N_DIR_SLOTS) > n);
        return(page + UNIV_PAGE_SIZE - PAGE_DIR
                        - (n + 1) * PAGE_DIR_SLOT_SIZE);
    }

    其他的优化

    在 G. Graefe 2010 Modern B-Tree Techniques 中概述了B-Tree中使用Interpolation Search优化Binary Search的选择问题。关于这个问题,G. Graefe在另一篇Survey性质的论文中,较为系统的描述使用插值优化的一些相关技术:B-tree indexes, interpolation search, and skew.[2]

    这篇论文[2]介绍了很多使用插值优化二分查找的考虑,但对于InnoDB似乎并不适用,主要原因是,在InnoDB中很多的Key都不是整数,且Key的类型会比较复杂,例如字符串、多字段组合等,并没有很简单的插值计算。此外,在当前的InnoDB实现中,各个Slot并没有存储实际的Key值,而是存储Key所在的页内偏移,即如果要进行Key比较,则一定要跳转到页面的其他位置,而这与论文中的描述想避免CPUCache Miss的理念是冲突的。

    一些遗留的问题:

    • 一个slotPAGE_DIR_SLOT_MAX_N_OWNED就是8,那么如果b-tree结构的相同key就超过8个,如何处理? (可能就直接截断就可以了,在搜索slot的时候需要注意处理这种情况)

    B-tree indexes, interpolation search, and skew.

    这是一篇研究B-Tree内部搜索性能的文章。B-Tree的最为主要的访问成本是从存储到内存,但作者认为,在某些场景下,需要关注CPU和内存处理的成本。这里的“某些场景”作者认为在变得更为重要,具体的,“某些场景”包括:

    • 现在的内存变得很大,页面命中率变得非常高,只有很少的页面需要从存储读取
    • 由于B-Tree的特性,页面读取的效率已经非常优秀了,已经是对数级的了

    此外,作者还注意到:硬件技术的发展上,更多的是带宽的提升,而不是延迟的提升,这包括两个方面(磁盘到内存、内存到CPU)。

    所以,作者把注意力放到了CPU处理效率的提升上。作者在论文中的预估是这样:在这个场景下,如果一个页面中有500个索引入口,那么页面内部的二分查找,就需要做:\( log_{2}500 \approx 18 \) 次比较操作以及对应的内存访问操作。

    这里做过了一些初步的测试,在当代的CPU中,通常都会有32KBL1 Cache ,故这里的部分事实已经不成立了。不过还是把这篇论文看完,看看作者的思考是什么。

    相比于二分查找,插值查找则每次从区间的某个相对位置开始进行搜索,这个相对位置则使用插值的方式获得,而不是简单的从中点开始(二分查找)。如果,键值模型恰好是一个线性模型的时候,这个时候线性插值的效率就会非常完美。而现实中,确实很多就是线性分布的。这篇文章的目的:The topic and purpose of this paper is to survey techniques that avoid the worst case of interpolation search. 看看有哪些技术,避免差值查找的糟糕情况发生。

    更细致的关于B-Tree和CPU Cache相关的讨论:[GL 01] G Graefe, P Larson: B-tree indexes and CPU caches. ICDE 2001, 349-358.

    一些研究的方向:

    • 最为直接的:使用一个类似的B-Tree结构,可能是二叉树、红黑树等
    • 为了让这部分索引能够更快速,能够让其存放到CPU缓存中,会考虑删除前缀或后缀
    • 此外,考虑利用B-Tree的多层索引,与CPU的多级缓存进行适配

    阅读这篇论文的收获是了解大家考虑问题的方法,大家的思考方式,以及对各种技术边界的拓展。例如,

    • 了解到大家对于程序实现的考虑,如何去思考处理器技术对于程序效率的影响,L1 Cache、L2 Cache以及指令Cache等如何影响CPU的效率。这些技术远看,其实并不算特别复杂,但是有了这些思考,对于计算机性能、架构的理解是不一样的。包括这里提到的一个论文中的观点:L1 Cache for Instruction 和 L2 Cache for Data 对于事务处理技术更为重要。这个观点就很有意思,当然,在当下,当然未必适用,但是这个思考方式可能是很重要的。如果对于这些技术非常了解,才有可能写出,诸如Redis、ClickHouse、DuckDB等这些性能非常极致的产品(大概是这样吧)。

    再比如这两篇论文中的一些计算,可以看到如何考虑程序的效率,包括CPU时间、存储时间等。

    在这里的一些考虑,看看他的计算和实现吧:在当时(这篇论文时候),一次\( \text{Page Access} \) 计为 \( 8ms \) ,传输时间为 \( 0.1ms \),如果页面中有 256 个索引入口。

    考虑一个计算指标( “utility” of a disk page )即:

    $$
    \frac{\text{counter of comparison}}{\text{page access latency} + \text{page transfer time}}
    $$

    那么,256个入口,一个典型的二分查找需要约:\( \log_2{256} \) 次比较,则:\( utility = \frac{8}{8+0.1} = 0.987 \);

    那么,如果页面大小增加到当前的八倍,那么需要的比较次数:\( \log_2{256*8} = 11 \),延迟依旧是 \( 8ms \) ,传输时间则为:\( 8*0.1 ms \),则:\( utility = \frac{11}{8+0.8} = 1.25 \)

    根据这里的计算,最优的页面大小,应该远远大于各个数据库系统中实际使用值。对于现代CPU来说,相比于处理指令,处理性能需要优先考虑cache miss以及pipeline stalls。在一些论文中对相关问题做了一些量化分析,有一些可以参考的结论,例如在事务处理时,最为重要的是L1的指令缓存和L2的数据缓存([ADH 99].)。所以实现的主要考虑包括:代码量(具体的某个功能模块)的多少、复杂度,以及L2 Data Cache Miss。

    页大小

    概述:对于非叶子节点(尤其是靠近根节点的),使用非常大的页面大小是有一定道理的,但叶子节点则不应该页面太大。非叶子节点的使用率是非常高的,并且数量相比于叶子节点要少很多;但叶子节点,则不同。当需要读取某个叶子节点时,通常只是为了读取某一条记录(典型的OLTP场景),为了一条记录而读取过大的页面,会导致内存利用率的降低,反而会导致整体的性能降低。

    在实践场景中,如果这个问题很关键或者有人对这个问题感兴趣,是可以做一些量化的分析的。把整个流程的时间、命中率、数据大小等相关的因素都考虑上,然后做一个最优化的分析。只是呢,B-Tree是一个比较古老的话题,大家对于这个数据结构的关注可能没有那么高了。

    分裂和合并的选择

    分裂与合并,不同的实现会有一些不同选择,包括:

    • 提前分裂,例如页面的占用率超过了一定的比率就进行分裂;结合插值算法考虑,如果页面里面的值,可以非常好的分裂为两个非常适合插值的区间,也可以考虑优先进行分裂。
    • 在某些实现中,删除的时候,从不触发合并,而是在某种“碎片整理”的时候才进行合并或分裂处理

    回退到二分查找

    在数据分布非常倾斜的情况,插值查找的效率可能会非常糟糕。需要考虑回退到二分查找。一些常见的策略是这样:

    • 例如,进行了3、4次差值查找还没有找到目标,那么可以回退到二分查找
    • 可以考虑根据第一、二次查找的效率做一些预估,预估效果差,则回退到二分查找
    • 在差值查找中,考虑使用二分查找做一些修正,最终后再退回二分查找

    正规化处理

    • key是字符串的时候,通常需要考虑先进行一定的正规化处理,以可以更好的进行大小比较。
    • 前缀删除。和一般的索引/搜索技术一样,如果有公共前缀可能会大大降低效率。

    回归差值

    在进行了一些插值计算后,可以考虑使用这些值进行回归计算。

    相关性分析

    可以对一个节点内的值,先进行一次回归分析,并考虑定期进行回归分析,从而判断插值搜索的效果。

    一个感受:在SSD推出之初,有很多人认为,B-Tree可能不再适用于数据库的存储(或其他场景的存储),可能要有更有的存储结构适配新的硬件(SSD)。但实际的情况是,在现在的几乎所有的存储硬件中,B-Tree依旧非常成功。这是因为,B-Tree所解决的问题本质并没有改变:内存访问的速度/成本远高于“磁盘”(HDD或者SDD、甚至S3等块存储)存储设备。这时候,B-Tree这种多路、平衡树结构依旧非常有效。(注:LSM-Tree 在很多模型下,也是非常适用的,这是另一个问题)

    名词说明

    • B+TreeB+-Tree 均指B+-Tree
    • 一般的,B-Tree指狭义的、经典的B-Tree,该结构主要参考:了解 InnoDB 的索引结构:B-Tree 基础
    • 有时候,B-Tree也会指代B+-Tree或其他B-Tree变种;这种用法也是非常广泛的,例如,在MySQL/InnoDB的文档中,仅使用B-Tree,而更为准确的说是B+-Tree
    • “节点”(Node)、“页面”(Page)、“数据块”、“块”(Block)等均指存储B-TreeB+-Tree的节点,即树结构的节点,内部可以存储多个键值。在不同的语境下,大家会用不同的词,例如通常在算法课程/书籍中,会更多的使用“节点”(Node);而在数据库实现中,则更喜欢用“页面”(Page)或“块”(Block);更为细节的,如果在内存中,则更多的时候用“页面”(Page),在磁盘上,则更多的“块”(Block)。各种情况下混用的也很多,并没有什么问题。
    • “记录”(record)、“行”(row)、tuple都是表示表中的一行记录,通常包含多个字段数据。
    • 索引入口(Entry)等是指完整的“键值对”,在叶子节点中,通常包括\( (k_i, a_i) \);在非叶子节点中可能的形式是:\( (k_i,p_i, a_i) \) 或 \( (k_i, p_i) \)
    • TAOCP 是 “The Art of Computer Programming”的缩写

    参考

    本文主要参考内容为:

  • InnoDB 的“记录”存储

    ·

    了解InnoDB的物理存储可以为后续进一步了解其日志存储打下基础。在前述的内容,较为详细的介绍了InnoDB内部的B+-Tree存储,本文在此基础上,继续介绍,一个B+-Tree的节点(经常称作“页面”)中,如何存储单条记录的。本文,仅介绍其一般实现,并不对InnoDB记录格式的各种类型、场景做详细介绍。

    这部分倒没有什么复杂的理论的,更多的是一些实践经验以及实际应用场景中各种情况的考虑与适配。

    最为直接的,则是按照“数据表”各个列的元数据顺序排列即可。而事实上,这也是InnoDB实现的基本原则。但在实践中,还有很多的问题要去解决:

    • 总是要考虑存储与读取效率:存储空间尽量不要有浪费,这样可以保持单个页面存储更多的数据,则有更高的磁盘和内存的利用率。例如,需要考虑Default值如何存储?NULL值如何存储?变长字段如何存储?超大的字段如何存储等。
    • 如何较为便捷的实现各种类型的DDL,例如新增一个字段、删除一个字段等

    一条记录分为两个部分,Record Header

    1. 准备测试数据

    这里新建表,并写入两条新的数据:

    DROP TABLE IF EXISTS t_r; 
    CREATE TABLE t_r (
      id int auto_increment primary key ,
      nick varchar(32),
      birthday date,
      password_hash char(32),
      email varchar(32)
    );
    
    INSERT INTO t_r VALUES 
      (1,"zzx","1989-02-11","a76e5fc27899ada11b4eafd0b1a7a4fc",'zzx@example.com'),
      (2,"yhq","2012-05-18","b6bdc8dcddeacece09d5697145d373d9",'yhq@example.com')
    ;

    (注:最好不要这样存储hash值)

    2. 找到数据页

    这需要一些背景知识,但并不是本文介绍的重点。在独立表空间(当前MySQL的默认行为)的情况下,:

    • 在数据目录下找到表数据文件:db.t_r.ibd
    • 查看该页面的第四个Page即为第一个数据页面,因为这里写入的数据较少,所以,一般就存储在该页面

    2.1 为什么是第四个页面

    如果对于这个问题感兴趣的话,可以参考 The basics of InnoDB space file layout 中关于“Per-table space files”的描述。可以看到,第四个页面即为INDEX: Root page of first index

    使用vim:%!xxd)查看时,每行16 Bytes,一个页面就是1024行,第四个页面,即从3073行开始,到4096行结束。

    2.2 数据页二进制数据

    $ view -b t_r.ibd
    3073 0000c000: 646e 9e73 0000 0003 ffff ffff ffff ffff  dn.s............
    3074 0000c010: 0000 0007 de81 961e 45bf 0000 0000 0000  ........E.......
    3075 0000c020: 0000 0000 01d0 0002 0114 8004 0000 0000  ................
    3076 0000c030: 00ce 0002 0001 0002 0000 0000 0000 0000  ................
    3077 0000c040: 0000 0000 0000 0000 030c 0000 01d0 0000  ................
    3078 0000c050: 0002 00f2 0000 01d0 0000 0002 0032 0100  .............2..
    3079 0000c060: 0200 1d69 6e66 696d 756d 0003 000b 0000  ...infimum......
    3080 0000c070: 7375 7072 656d 756d 0f03 0000 0010 004e  supremum.......N
    3081 0000c080: 8000 0001 0000 0090 f05b bb00 0001 3101  .........[....1.
    3082 0000c090: 107a 7a78 8f8a 4b61 3736 6535 6663 3237  .zzx..Ka76e5fc27
    3083 0000c0a0: 3839 3961 6461 3131 6234 6561 6664 3062  899ada11b4eafd0b
    3084 0000c0b0: 3161 3761 3466 637a 7a78 4065 7861 6d70  1a7a4fczzx@examp
    3085 0000c0c0: 6c65 2e63 6f6d 0f03 0000 0018 ffa2 8000  le.com..........
    3086 0000c0d0: 0002 0000 0090 f05b bb00 0001 3101 1d79  .......[....1..y
    3087 0000c0e0: 6871 8fb8 b262 3662 6463 3864 6364 6465  hq...b6bdc8dcdde
    3088 0000c0f0: 6163 6563 6530 3964 3536 3937 3134 3564  acece09d5697145d
    3089 0000c100: 3337 3364 3979 6871 4065 7861 6d70 6c65  373d9yhq@example
    3090 0000c110: 2e63 6f6d 0000 0000 0000 0000 0000 0000  .com............
    3091 0000c120: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    3092 0000c130: 0000 0000 0000 0000 0000 0000 0000 0000  ................
         ......
    4094 0000ffd0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    4095 0000ffe0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
    4096 0000fff0: 0000 0000 0070 0063 646e 9e73 de81 961e  .....p.cdn.s....

    3. InnoDB 用户“记录”格式概述

    关于该格式的描述,可以参考innobase/rem/rem0rec.c中的注释说明部分,有比较详细的说明。

    /*
    ...
    | length of the last non-null variable-length field of data:
      if the maximum length is 255, one byte; otherwise,
      0xxxxxxx (one byte, length=0..127), or 1exxxxxxxxxxxxxx (two bytes,
      length=128..16383, extern storage flag) |
    ...
    | length of first variable-length field of data |
    | SQL-null flags (1 bit per nullable field), padded to full bytes |
    | 4 bits used to delete mark a record, and mark a predefined
      minimum record in alphabetical order |
    | 4 bits giving the number of records owned by this record
      (this term is explained in page0page.h) |
    | 13 bits giving the order number of this record in the
      heap of the index page |
    | 3 bits record type: 000=conventional, 001=node pointer (inside B-tree),
      010=infimum, 011=supremum, 1xx=reserved |
    | two bytes giving a relative pointer to the next record in the page |
    ORIGIN of the record
    | first field of data |
    ...
    | last field of data |
    ...
    */

    4. InnoDB 页内的记录

    在一个页面内,InnoDB存储每条物理记录时,都会在记录中存储一个“指针”,该“指针”指向下一条记录的物理位置,并且在实现时,该“指针”是以当前记录偏移的方式存储的。

    所以,如果我们能够找到页面内的第一条记录,那么就可以依此顺序的遍历整个页面内的记录了。找到第一条记录大概有很多的方法,这里说说InnoDB的实现:infimumsupremum。简单来说,InnoDB会在页面中存储一个infimum记录,该记录可以认为是该页面的极小值,该记录中的“Next Record”即为实际的该页面最小值。

    4.1 系统记录(system record):infimum 和 supremum

    infimumsupremum是每个InnoDB页面内存储两条物理记录,分别代表“起始最小记录”和“结束最大记录”。所以如果使用上述的下一条记录“指针”遍历页内所有记录,则总是从infimum开始,以supremum结束,中间就是所有的用户记录。

    在页面找到infimumsupremum记录的方法有两种:

    • infimumsupremum在页面的偏移,总是99112
    • Page Directoryslot中查找:第一个slot总是指向infimum;最后一个slot总是指向supremum

    4.2 从Page Directory找到infimum

    在前文“B+-Tree的内部搜索:InnoDB的实现”,已经概述了slot 0中总是存储的infimum记录,则可以通过页面底部向前偏移PAGE_DIR(InnoDB中PAGE_DIR的值是8),然后找到slot 0对应的记录的偏移,即infimum的偏移,而因为一个slot存储的偏移地址都是2 bytes 。所以,在页面内的倒数第10、9个字节,即为infimum在页内的偏移地址,即0x0063

    4.3 “记录”指针

    这里有两点需要注意:

    1. 一般“记录”指针通常是指向记录的“the origin of the record”,即实际数据开始的地方。不包含“记录头”部分
    2. 一般“记录”指针会使用两种方式表示:(a) 页内偏移;(b) 相当于当前记录的偏移。不同的地方会不同,例如“记录头”中存储next record的指针则是“相对偏移”,而slot内存储的则是页内绝对偏移。

    理解上述两点,对于理解后续内容是很重要的,要不然,可能会算错地址。

    例如,上述的slot 0中记录的infimum的偏移0x0063就是绝对偏移;而infimum中存储的next record偏移,则是相对于infimum的偏移地址。例如,next record偏移是00 1d,那么意味着这条记录的偏移是0x0063+0x001d,即绝对偏移是:0x0080

    4.4 记录头 record header

    按照上述方法,这里已经找到第一条(按大小顺序)记录的“记录指针”,即0x0080。把记录的头部(record header)和记录本身数据从上述二进制数据单独取出来,如下:

                                       |<-record header->|
    3080 0000c070: ................... 0f03 0000 0010 004e  supremum.......N
                   |<---------- field data   ----------->|
    3081 0000c080: 8000 0001 0000 0090 f05b bb00 0001 3101  .........[....1.
                   ^
                   |--> record pointer/offset (the origin of the record)
    3082 0000c090: 107a 7a78 8f8a 4b61 3736 6535 6663 3237  .zzx..Ka76e5fc27
    3083 0000c0a0: 3839 3961 6461 3131 6234 6561 6664 3062  899ada11b4eafd0b
    3084 0000c0b0: 3161 3761 3466 637a 7a78 4065 7861 6d70  1a7a4fczzx@examp
    3085 0000c0c0: 6c65 2e63 6f6d ........................  le.com..........
                  |<-field data->|

    注:

    • 在获得了 record pointer后,是无法简单的、直接获得record header长度的,也无法简单的获取记录本身的长度的。而是需要根据元数据库、以及record header中的部分数据,计算而来。
    • record header 的长度获取,是比较复杂的。这里做一定的简化,描述如下:
      • record pointer向前(record header方向)移动,5个bytes是相对固定的,包含了
        • 2 bytes: next record
        • 3 bits: record type
        • 13 bits: the order number of this record
        • 4 bits: the number of records owned by this record(slot)
        • 4 bits: delete mark and mark a predefined minimum record
      • record pointer再向前的 nbytes则用于存储 nullfield,该存储模式为按标记位存储,且按bytes补齐:
        • 例如,这里一共有5fields,则需要1 bytes,作为标记位;如果某个fieldnull则对应的bit即标记位1
        • 所以,这里的 n=1
      • record pointer再向前的mbytes则用于存储变成列的实际长度。本示例中,共有两个变成列,故这里的 m = 2

    4.5 记录的解析Record Header

    这里以页面中的第一条记录为示例,继续解析该记录的头部信息。详情如下:

    (<------------- record header / hex ----------------->) (<----- data/hex ----->)
    0f03 0000             0010             004e             8000 0001 0000 0090 f05b 
    
    (hex)(<---------------  binary   -------------------->) (<----- data/hex ----->)
    0f03 0000000000000000 0000000000010000 0000000001001110 8000 0001 0000 0090 f05b
    | |  |<----->|<->|<-->|<---------->|   |<------------>|
    | |  |       |   |    |            |   |
    | |  |       |   |    |            |   |-->(2 bytes) next record
    | |  |       |   |    |            |
    | |  |       |   |    |            |-->(3 bits) record type: 000=conventional
    | |  |       |   |    |
    | |  |       |   |    |-->(13 bits) the order number of this record
    | |  |       |   |
    | |  |       |   |-->(4 bits) the number of records owned by this record(slot)
    | |  |       |
    | |  |       |-->(4 bits) delete mark and mark a predefined  minimum record
    | |  |
    | |  |-->(8 bits) null-flag (here ,no null field at all, 5 field, so 1 bytes )
    | |
    | |-->(1 byte) length of 1st variable field, so length is 03
    |
    |-->(1 byte) length of 2nd variable field, so length is 0f(15)
    

    结合记录头部信息,再结合“元数据信息”,则就可以解析出记录本身的信息了。

    5. 其他

    有很多人对这个问题已经有了非常深入到说明或研究,如下给出主要的参考链接:

    延伸讨论:

    • 各种数据类型的序列与反序列化,以及对应的效率考虑
    • B+TreeB+-Tree 均指B+-Tree
    • 一般的,B-Tree指狭义的、经典的B-Tree,该结构主要参考:了解 InnoDB 的索引结构:B-Tree 基础
    • 有时候,B-Tree也会指代B+-Tree或其他B-Tree变种;这种用法也是非常广泛的,例如,在MySQL/InnoDB的文档中,仅使用B-Tree,而更为准确的说是B+-Tree
    • “节点”(Node)、“页面”(Page)、“数据块”、“块”(Block)等均指存储B-TreeB+-Tree的节点,即树结构的节点,内部可以存储多个键值。在不同的语境下,大家会用不同的词,例如通常在算法课程/书籍中,会更多的使用“节点”(Node);而在数据库实现中,则更喜欢用“页面”(Page)或“块”(Block);更为细节的,如果在内存中,则更多的时候用“页面”(Page),在磁盘上,则更多的“块”(Block)。各种情况下混用的也很多,并没有什么问题。
    • “记录”(record)、“行”(row)、tuple都是表示表中的一行记录,通常包含多个字段数据。
    • 索引入口(Entry)等是指完整的“键值对”,在叶子节点中,通常包括\( (k_i, a_i) \);在非叶子节点中可能的形式是:\( (k_i,p_i, a_i) \) 或 \( (k_i, p_i) \)
    • TAOCP 是 “The Art of Computer Programming”的缩写