暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL中的索引介绍及主要扫描技术

翻译:尚凯

审核:魏波

Egor Rogov
俄罗斯Postgres Professional公司数据库专家

目录

1 介绍

2 索引

3 索引引擎

4 主要扫描技术

     4.1 索引扫描

     4.2 位图扫描

     4.3 顺序扫描

     4.4 仅索引扫描

介绍

       本文主要关注PostgreSQL中的索引。

      任何主题都可以从不同的角度考虑。我们将讨论应用程序开发人员使用DBMS感兴趣的点:可用的索引类型,为什么有这么多不同类型的索引,以及如何使用它们来加快查询。

       这个主题也许可以用更少的篇幅来描述,但是我们希望作为一个好奇的开发人员,也可以对内部细节感兴趣,特别是了解这些细节不仅可以让您遵从他人的判断,还可以得出自己的结论。

      新类型索引的开发超出了讨论范围。这需要了解C语言编程,并且属于系统程序员而不是应用程序开发人员的专业知识。出于同样的原因,我们不讨论编程接口,只关注使用索引相关的内容。

      接下来,我们将讨论与DBMS核心相关的通用索引引擎和各个索引访问方法之间的职责分配,PostgreSQL允许我们将这些方法添加为扩展。在下一篇文章中,我们将讨论访问方法接口和关键概念,如类和运算符。之后,将考虑不同类型索引的结构和应用的细节:HashB-treeGiSTSP-GiSTGINRUMBRINBloom


索引

       在PostgreSQL中,索引是一种特殊的数据库对象,主要用于加速数据访问。索引是辅助结构:可以从表中的信息中删除和重新创建每个索引。索引还可用于强制执行某些完整性约束。

       目前,PostgreSQL 9.6内置了六种不同类型的索引,并且由于版本9.6的重大更改,还提供了一个索引作为扩展。因此,预计不久的将来会出现新的索引类型。

       尽管索引类型(也称为访问方法)之间存在差异,但它们最终都将一个键(例如,索引列的值)与包含该键的表的行关联起来。每一行由TID(元组id)标识,它由文件中的块数和块内行的位置组成。也就是说,使用已知键或有关它的一些信息,我们可以快速读取那些可能包含我们感兴趣的信息的行,而无需扫描整个表。

       重要的是要理解索引以一定的维护成本加速数据访问。对于索引数据上的每个操作,无论是表行的插入,删除还是更新,都需要在同一个事务中更新该表的索引。请注意,更新尚未构建索引的表字段不会导致索引更新; 这种技术叫做HOT(Heap-Only Tuples)。

       可扩展性带来一些影响。为了方便向系统添加新的访问方法,实现了通用索引引擎接口。它的主要任务是从访问方法中获取TID并使用它们:

• 从相应版本的表行中读取数据。

• 通过TID获取行版本TID,或使用预构建的位图批量获取行版本。

• 检查当前事务的行版本的可见性,同时考虑其隔离级别。

      

       索引引擎参与执行查询。它是根据优化阶段创建的计划调用的。优化器对执行查询的不同方法进行排序和评估,应该了解所有可能适用的访问方法功能。该方法是否能够按所需的顺序返回数据,或者我们是否应该预期排序?我们可以使用此方法搜索NULL吗?这些是优化器经常要解决的问题。

       不仅优化器需要有关访问方法的信息。在构建索引时,系统必须决定是否可以在多个列上构建索引,以及此索引是否确保唯一性。

       因此,每种访问方法都应提供有关自身的所有必要信息。低于9.6的版本使用了pg_am表,而从版本9.6开始,数据移动到更深层的特殊函数中。我们将进一步了解这个接口。

        剩下的就是访问方法的任务:

• 实现用于构建索引的算法,并将数据映射到页面中(以便缓冲区缓存管理器统一处理每个索引)。

• 通过“索引字段操作符表达式”形式的谓词在索引中搜索信息。

• 评估索引使用成本。

• 操纵正确并行处理所需的锁。

• 生成预写日志(WAL)记录。

       

       我们将首先考虑通用索引引擎的功能,然后再考虑不同的访问方法。


索引引擎

       索引引擎使PostgreSQL能够统一地处理各种访问方法,但要考虑它们的特性。

主要扫描技术

  1.索引扫描

       我们可以用不同的方法处理由索引提供的TID。我们来看一个例子:

    postgres=# create table t(a integer, b text, c boolean);
    postgres=# insert into t(a,b,c)
    select s.id, chr((32+random()*94)::integer), random() < 0.01
    from generate_series(1,100000) as s(id)
    order by random();
    postgres=# create index on t(a);
    postgres=# analyze t;

           我们创建了一个包含三个字段的表。第一个字段包含1到100000之间的数字,并且在此字段上创建索引(无论何种类型)。第二个字段包含除不可打印字符之外的各种ASCII字符。最后,第三个字段包含一个逻辑值,约1%的行是true,其余的是false。行以随机顺序插入表中。

           让我们尝试通过条件a = 1来选择一个值。请注意,条件看起来像“ 索引字段操作符表达式 ”,其中操作符为“相等”,表达式(搜索键)为“1”。在大多数情况下,要使用的索引,条件必须如此。    

      postgres=# explain (costs off) select * from t where a = 1;
      QUERY PLAN
      -------------------------------
      Index Scan using t_a_idx on t
      Index Cond: (a = 1)
      (2 rows)

             在这种情况下,优化器决定使用索引扫描。通过索引扫描,访问方法逐个返回TID值,直到达到最后一个匹配的行。索引引擎依次访问由TID指示的表行,获取行版本,检查其对多版本并发规则的可见性,并返回获得的数据。


      2.位图扫描

             当我们只处理几个值时,索引扫描工作正常。但是,随着检索到的行数增加,很有可能多次返回同一个表页。因此,优化器切换到位图扫描。

        postgres=# explain (costs off) select * from t where a <= 100;
        QUERY PLAN
        ------------------------------------
        Bitmap Heap Scan on t
        Recheck Cond: (a <= 100)
        -> Bitmap Index Scan on t_a_idx
        Index Cond: (a <= 100)
        (4 rows)

                访问方法首先返回所有与条件匹配的TID(位图索引扫描节点),并从这些TID构建行版本的位图。然后从表中读取行版本(位图堆扫描),每个页面只读取一次。
               请注意,在第二步中,可能会重新检查条件(Recheck Cond)。对于行版本的位图来说,检索的行数可能太大,无法完全匹配RAM(受work_mem参数的限制)。在这种情况下,仅为包含至少一个匹配行版本的页面构建位图。  

                这个“有损”位图需要的空间更少,但在阅读页面时,我们需要重新检查其中包含的每一行的条件。请注意,即使对于少量检索到的行并因此“精确”位图(例如在我们的示例中),«RecheckCond»步骤仍然在计划中表示,尽管实际上并未执行。

               如果对多个表字段施加条件并对这些字段建立索引,位图扫描允许同时使用多个索引(如果优化程序认为这样有效)。对于每个索引,构建行版本的位图,然后执行按位布尔乘法(如果表达式由AND连接)或布尔加法(如果表达式由OR连接)。例如:

          postgres=# create index on t(b);
          postgres=# analyze t;
          postgres=# explain (costs off) select * from t where a <= 100 and b = 'a';
          QUERY PLAN
          --------------------------------------------------
          Bitmap Heap Scan on t
          Recheck Cond: ((a <= 100) AND (b = 'a'::text))
          -> BitmapAnd
          -> Bitmap Index Scan on t_a_idx
          Index Cond: (a <= 100)
          -> Bitmap Index Scan on t_b_idx
          Index Cond: (b = 'a'::text)
          (7 rows)

                这里BitmapAnd节点通过按位与操作连接两个位图。

                位图扫描使我们能够避免重复访问相同的数据页。但是如果表页面中数据的物理排序方式与索引记录完全相同怎么办?毫无疑问,我们不能完全依赖页面中数据的物理顺序。如果需要排序数据,我们必须在查询中显式指定ORDER BY子句。但实际情况可能是“几乎所有”数据都被排序的情况:例如,如果按所需顺序添加行,并且在此之后或执行CLUSTER命令之后不进行更改。在这种情况下,构建位图是一个过度的步骤,常规索引扫描也同样好(除非我们考虑连接多个索引的可能性)。因此,在选择访问方法时,计划器会查看一个特殊的统计数据,该统计数据显示了物理行排序和列值逻辑排序之间的相关性:

            postgres=# select attname, correlation from pg_stats where tablename = 't';
            attname | correlation
            ---------+-------------
            b | 0.533512
            c | 0.942365
            a | -0.00768816
            (3 rows)

                  接近1的绝对值表示高相关性(对于列c),而接近于0的值则相反,表示混沌分布(列a)。


            3.顺序扫描

                   在非选择性条件下,优化器将优先选择整个表的顺序扫描而不是使用索引:

              postgres=# explain (costs off) select * from t where a <= 40000;
              QUERY PLAN
              ------------------------
              Seq Scan on t
              Filter: (a <= 40000)
              (2 rows)

                     问题是条件选择性越高,索引就越好,也就是说,匹配它的行越少。检索行数的增长增加了读取索引页的开销成本。
                     顺序扫描比随机扫描更快,这使情况更加复杂。这特别适用于硬盘,在硬盘上,将磁头带到轨道的机械操作比读取数据本身花费更多的时间。SSD的效果不太明显。有两个参数可用于考虑访问成本的差异,seq_page_cost和random_page_cost,我们不仅可以在全局设置,还可以在表空间级别设置,这种方法可以调整不同磁盘子系统的特性。


              4.仅索引扫描

                     通常,访问方法的主要任务是返回匹配表行的标识符,以便索引引擎从这些行读取必要的数据。但是,如果索引已包含查询所需的所有数据呢?这样的索引称为覆盖,在这种情况下,优化器可以应用仅索引扫描:

                postgres=# vacuum t;
                postgres=# explain (costs off) select a from t where a < 100;
                QUERY PLAN
                ------------------------------------
                Index Only Scan using t_a_idx on t
                Index Cond: (a < 100)
                (2 rows)

                        此名称可能让人觉得索引引擎根本不访问该表,而是仅从访问方法获取所有必要信息。但事实并非如此,因为PostgreSQL中的索引并不存储使我们能够判断行可见性的信息。因此,访问方法返回与搜索条件匹配的行版本,而不考虑它们在当前事务中的可见性。
                       但是,如果索引引擎每次都需要查看表以获得可见性,那么这种扫描方法与常规索引扫描没有什么不同。

                      为解决这个问题,PostgreSQL为表维护了一个所谓的可见性映射,在这个映射中,不管起始时间和隔离级别如何,标记清理的页面中,如果数据更改的时间不够长,数据可被所有事务看到。如果索引返回的行的标识符与这样的页面相关,则可以避免可见性检查。
                       因此,定期vacuum可以提高覆盖指数的效率。此外,优化器会考虑死元组的数量,如果预测可见性检查的开销很高,则可以决定不使用仅索引扫描。

                       我们可以使用EXPLAIN ANALYZE命令了解对表的强制访问次数:

                  postgres=# explain (analyze, costs off) select a from t where a < 100;
                  QUERY PLAN
                  -------------------------------------------------------------------------------
                  Index Only Scan using t_a_idx on t (actual time=0.025..0.036 rows=99 loops=1)
                  Index Cond: (a < 100)
                  Heap Fetches: 0
                  Planning time: 0.092 ms
                  Execution time: 0.059 ms
                  (5 rows)

                         在这种情况下,不需要访问表(Heap Fetches:0),因为刚刚进行了vacuum操作。一般来说,这个数字越接近零越好。
                         并非所有索引都与行标识符一起存储索引值。如果访问方法无法返回数据,则不能将其用于仅索引扫描。


                     
                  下期预告:

                      上文介绍了索引、索引类型及主要的扫描技术,后续将继续介绍与索引相关的内容,主要包括空值与索引、索引类型示例、索引与排序、并行创建。


                  欢迎投稿



                          中国开源软件推进联盟PostgreSQL分会,欢迎大家积极投稿,向PGer分享自己的实践经验、心得体会,共建PG中国生态。

                  投稿邮箱:

                  press@postgresqlchina.com

                  原文请点击下方“阅读原文”获取,建议PC端阅读更佳!

                  最后修改时间:2019-11-06 10:43:49
                  文章转载自开源软件联盟PostgreSQL分会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论