第 1 章 SQL 索引剖析

Posted 不剪发的Tony老师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第 1 章 SQL 索引剖析相关的知识,希望对你有一定的参考价值。

关于索引,最常见的解释就是:索引可以提高查询的速度。虽然这种说法指出了索引最重要的一点,但是它没有解释索引的本质。本章将会进一步描述索引的结构但是不会太深陷细节之中,相关知识点足够我们理解本书中涉及的 SQL 性能问题。

索引是数据库中的一个独立的数据结构,使用 create index 语句创建。索引需要单独的磁盘空间,并且存储了被索引数据的一个副本。这就意味着索引完全是冗余的结构。创建索引不会影响表中的数据,仅仅是创建了一个指向表的数据结构。总之,数据库索引和图书中的索引非常类似,它占用单独的空间、高度冗余并且指向了其他位置中的实际内容。

📝SQL Server 和 mysql(InnoDB)将基于索引结构存储的表称为聚集索引(clustered index);在 Oracle 数据库中,这种类型的表被称为索引组织表(Index-Organized Table、IOT)。第 5 章将会详细描述这种表,包括它的优点和缺点。

搜索数据库索引和查找电话簿类型,关键在于所有的元素都已经按照指定的顺序排列,在一个已经排好序的数据集中查找数据更快更容易,因为排序顺序决定了每个元素的位置。

不过,数据库索引比电话簿更复杂,因为它在不停的变化。我们不可能每次发生变化时更新电话簿,原因很简单,已有的条目之间没有空间增加新的元素。电话簿解决问题的方式是累积一些更新,下次打印新的电话簿时一起处理。数据库不可能等待那么长的时间再进行更新,它必须立即处理插入、更新和删除语句,同时在不需要移动大量数据的前提下维护索引的顺序。

为了解决这一问题,数据库结合使用了两种数据结构:双向链表和搜索树。这两种数据结构可以解释大部分的数据库性能特征。

叶子节点:双向链表

索引的主要目的就是按照顺序存储被索引的数据。不过,我们无法按照顺序存储数据,因为插入语句可能导致元素的移动,从而为新的数据让出空间。移动大量数据是一个非常耗时的操作,因此插入语句可能会非常缓慢。解决问题的方法就是在内存中建立一个独立于物理顺序的逻辑顺序

逻辑顺序使用双向链表存储,每个节点包含指向前后两个相邻元素的链接,就像一个链条一样。如果需要在两个相邻节点之间插入新节点,只需要更新它们的链接指针,指向这个新的节点。新节点的物理位置不再重要,因为双向链表保存了逻辑顺序。

这种数据结构被称为双向链表(doubly linked list),因为每个节点都存储了指向前一个节点和后一个节点的指针。双向链表使得数据库可以随意向前或者向后读取索引数据。插入新节点不再需要移动大量数据,而仅仅是修改一些指针。

在许多编程语言中,双向链表也被用于集合(容易)结构。

编程语言名称
Javajava.util.LinkedList
.NET FrameworkSystem.Collections.Generic.LinkedList
C++std::list

数据库使用双向链表连接索引的叶子节点。每个叶子节点存储在一个数据库块(block )或页(page)中,块或页是数据库的最小存储单元。所有的索引块(页)大小都相同,通常是几个 KB(Oracle、SQL Server、PostgreSQL 默认 8 KB,MySQL 默认 16 KB)。数据库尽可能使用每个块中的空间,并在每个块中存储尽可能多的索引项。这意味着索引的顺序通过两个级别进行维护:每个叶子节点内部的索引项,以及使用双向链表连接的叶子节点。

图 1.1 索引叶子节点以及相应的表数据

图 1.1 演示了索引叶子节点以及它们和表数据的连接。每个索引项包含了被索引的字段(索引键、column 2)和指向相应数据行的指针(ROWID或者RID)。表数据和索引不同,它们存储在堆结构中,完全没有排序。数据块(页)中的数据行之间完全没有任何关系,多个数据块之间也没有相互连接。

B-树:平衡树

索引叶子节点使用任意顺序进行存储,它们在磁盘中的位置和索引逻辑顺序之间没有对应关系。它们就像是一个被打乱顺序的电话目录,如果你想要查找“Smith”,但是打开了“Robinson”,并不意味着 Smith 位于 Robinson 的后面。数据库需要使用另一个数据结构,确保能够快速找到这些被打乱数据页之间的某个元素:平衡搜索树,也就是 B-树。

图 1.2 B-树结构

图 1.2 显示了一个包含 30 个索引项的索引。双向链表维护了叶子节点之间的逻辑顺序。根节点和分支节点用于支持叶子节点之间的快速查找。

上图左侧是一个分支节点和它指向的多个叶子节点。每个分支节点中的元素对应了相应叶子节点中的最大索引值。以第一个叶子节点为例,最大的索引值是 46,因此在相应分支节点元素中存储了该数值。其他叶子节点也是如此,所以最终该分支节点存储了 46、53、57 以及 83。基于这种方式,当所有叶子节点都被分支节点覆盖时,就构建了一个分支层。

下一层的构建也类似,只不过它是基于第一次构建的分支节点。以上过程会不断重复,知道所有的索引键都可以存储到一个节点中,也就是根节点中。这是一种平衡搜索树,因为树中每个位置的深度都相同,根节点到所有叶子节点的距离都相等。

⚠️B-树是指平衡树(balanced tree),而不是二叉树(binary tree)。

一旦被创建,数据库会自动维护索引。无论是插入、更新还是删除操作,都会保持树的平衡,因此也会给写入操作带来额外的维护开销。第 8 章将会进一步讨论相关细节。

图1.3 B-树遍历

图1.3 显示了一个索引片段,演示了如何搜索索引键 57。树的遍历从左侧的根节点开始,按照升序的顺序依次处理每个索引项,直到找到一个大于等于(>=)搜索词(57)的索引值,在上图中该值为 83。然后数据库沿着相应的分支节点重复以上查找过程,直到遍历过程到达某个叶子节点。

⚠️B-树结构使得数据库能够快速找到叶子节点。

树的遍历是一种非常高效的操作,即使数据量很大,也可以快速找到相应的数据。主要的原因在于树的平衡,访问所有的元素所需的操作都相同;另外一个原因是树的深度具有对数增长特性,这意味着随着叶子节点的增长(数据量的增长),树的深度增长得非常缓慢。实际情况是,对于数百万条记录,树的深度只有 4 或 5 层。很少有树的深度达到了 6 层。

在数学中,对数是对求幂的逆运算。10 的 2(指数)次方为 100,以 10 为基数的 100 的对数为 2。在搜索树中,基数对应每个分支节点的索引项数量,指数对应树的深度。图 1.2 中的索引示例每个节点包含 4 个索引项,树的深度为 3。这意味着该索引最多可以保存 64(43)个索引值。如果树的深度加一层,可以保存 256(44)个索引值。树的深度每增加一层,最大的索引项增长 4 倍。

对数执行相反的操作,树的深度等于 log4(索引项数量)。

树的深度索引项数量
364
4256
51024
64096
716384
865536
9262144
101048576

对数增长使得示例中的索引可以通过 10 层搜索树查找一百万条记录,真实的索引更加高效。影响树的深度和查找性能的主要因素是每个树节点能够存储的索引项数量。从数学上将,这个数量就是对数函数的基数。基数越大,树的层次越浅,遍历速度越快。

数据库充分利用了这个数学概念,在每个分支节点中尽可能多的存储索引项,通常可以存储数百个索引项。这意味增加一层索引结构可以增加上百倍的索引项。

📝B+树的结构演示可以参考这个网站

慢索引:第一部分

尽管树遍历的效率很高,但仍有一些情况下索引查找的速度不如预期。这种矛盾长期以来导致了“索引退化”的缪论。该缪论认为重建索引可以产生奇迹般的效果。附件 B 讨论了关于该缪论和其他缪论的细节。目前,你可以认为从长远来看,重建索引不会提高性能。查询执行缓慢(即使使用了索引)的原因可以使用前面章节的基础上进行解释。

索引查找缓慢的第一个原因是叶节点链。考虑图 1.3 中的示例,显然索引中存在两个满足条件(57)的记录。至少有两个索引记录相同,更准确地说,下一个叶子节点可能包含更多的 57。数据库必须读取下一个叶子节点才能确认是否存在更多满足条件的记录。这意味着索引查找不仅仅需要执行树的遍历,还需要遍历叶节点链。

索引查找缓慢的第二个原因是访问表。即使是单个叶子节点也可能包含很多记录,通常是数百个。相应的表数据通常会分散在很多表数据块中(参见图 1.1)。这就意味着每个满足条件的索引项都需要一次额外的表访问。

索引查找需要执行三个步骤:(1)树的遍历;(2)遍历叶节点链;(3)访问表中的数据。树的遍历是唯一个存在访问次数上限的步骤,上限就是索引树的深度。另外两个步骤可能需要访问大量数据块,从而导致索引查找缓慢。

“慢索引”缪论的根源在于错误的认为索引查找仅仅需要遍历索引树,因此认为慢索引一定是由于索引树的“断裂”或者“不平衡”导致的。事实是你可以询问大多数数据库它们是如何使用索引的。Oracle 数据库在这方面提供了非常详细的信息,对于基本索引查找提供了三种不同的操作描述:

  • INDEX UNIQUE SCAN,INDEX UNIQUE SCAN 只需要执行树的遍历。Oracle 数据库中,如果唯一约束能够确保查询条件最多返回一条记录,数据库会选择该操作。
  • INDEX RANGE SCAN,INDEX RANGE SCAN 首先执行树的遍历,然后沿着叶子节点链接查找所有满足条件的记录。如果可能存在多个满足条件的记录,将会执行这种后备操作。
  • TABLE ACCESS BY INDEX ROWID,TABLE ACCESS BY INDEX ROWID 操作从表中返回数据行。这种操作通常基于前面执行的索引扫描返回的索引记录执行表的访问。

重点在于,INDEX RANGE SCAN 可能会扫描索引中的大部分节点。如果针对索引扫描返回的每个记录还需要执行一次表的访问,即使通过索引返回数据也可能会很慢。

以上是关于第 1 章 SQL 索引剖析的主要内容,如果未能解决你的问题,请参考以下文章

Mysql高级调优篇——第四章:Sql实战调优场景剖析(下)

第 2 章 查询条件优化之等值查找

MySQL之SQL优化详解

干货篇:深入剖析 MySQL 索引和 SQL 调优实战

SQL 撤销索引撤销表以及撤销数据库方法剖析

第 0 章 为什么说设计索引是开发人员的职责?