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

PostgreSQL中的索引介绍(二)

翻译:尚凯

审核:魏波

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

     上期描述了索引、索引类型及主要的扫描技术(关注文末二维码,往期文章不迷路)。
     在接下来的内容中继续介绍与索引相关的内容,主要包括空值与索引、索引类型示例、索引与排序、并行创建。

目录

1 空值

2 多列索引

3 表达式索引

4 部分索引

5 排序

6 并发构建

空值

       NULL作为表示不存在或未知值的一种方便的方法,在关系数据库中发挥着重要的作用。

       但是特殊的值需要特殊的处理。常规布尔代数变为三元代数; 目前还不清楚NULL是小于还是大于常规值(这需要特殊的排序结构,NULLS FIRST和NULLS LAST); 聚合函数是否应该考虑NULL是不明显的; 规划者需要一个特殊的统计数据......

       从索引支持的角度来看,我们还不清楚是否需要对这些值进行索引。如果未对NULL进行索引,则索引可能更紧凑。但是如果对NULL进行索引,我们能够将索引用于像“ indexed-field IS[NOT] NULL“这样的条件,并且当没有为表指定条件时也可以作为覆盖索引(因为在这种情况下,索引必须返回所有表的数据行,包括那些使用NULL值的行)。

       对于每种访问方法,开发人员决定是否对NULL进行索引。但通常情况下,它们会被编入索引。

多列索引

       要支持多个字段的条件,可以使用多列索引。例如,我们可以在表的两个字段上构建索引:

    postgres=# create index on t(a,b);
    postgres=# analyze t;

           优化器很可能更喜欢这个索引,而不是加入位图,因为在这里我们很容易获得所需的TID而无需任何辅助操作:

      postgres=# explain (costs off) select * from t where a <= 100 and b = 'a';
      QUERY PLAN
      ------------------------------------------------
      Index Scan using t_a_b_idx on t
      Index Cond: ((a <= 100) AND (b = 'a'::text))
      (2 rows)

               多列索引还可以通过某些字段的条件(从第一个字段开始)加快数据检索速度:

        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_b_idx
        Index Cond: (a <= 100)
        (4 rows)

                通常,如果第一个字段没有附加条件,则不使用索引。但有时优化器可能会认为索引的使用比顺序扫描更有效。在考虑btree索引时,我们将扩展这个主题。

                并非所有访问方法都支持在多个列上构建索引。


        表达式索引

               我们已经提到搜索条件必须看起来像“ 索引字段操作符表达式 ”。在下面的示例中,将不使用索引,因为使用包含字段名称的表达式而不是字段名称本身:

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

                  重写此特定查询并不需要太多时间,只要字段名称写入操作符的左侧。如果这不允许,表达式索引(函数索引)将会有所帮助:

            postgres=# explain (costs off) select * from t where lower(b) = 'a';
            QUERY PLAN
            ----------------------------------------------------
            Bitmap Heap Scan on t
            Recheck Cond: (lower((b)::text) = 'a'::text)
            -> Bitmap Index Scan on t_lower_idx
            Index Cond: (lower((b)::text) = 'a'::text)
            (4 rows)

                    函数索引不是建立在表字段上,而是建立在任意表达式上。优化器会将此索引视为“索引表达式操作符表达式 ”之类的条件。如果要索引表达式的计算代价高昂,那么索引的更新也将需要大量的计算资源。

                    还请记住,为索引表达式收集了单独的统计信息。可以通过索引名称在pg_stats视图中了解这个统计信息:

              postgres=# create index on t(lower(b));
              postgres=# analyze t;
              postgres=# \d t
              Table "public.t"
              Column | Type | Modifiers
              --------+---------+-----------
              a | integer |
              b | text |
              c | boolean |
              Indexes:
              "t_a_b_idx" btree (a, b)
              "t_a_idx" btree (a)
              "t_b_idx" btree (b)
              "t_lower_idx" btree (lower(b))
              postgres=# select * from pg_stats where tablename = 't_lower_idx';

                     如果有必要,可以使用与常规数据字段相同的方式控制直方图的栏的数量(注意,根据索引表达式,列名可能不同):

                postgres=# \d t_lower_idx
                Index "public.t_lower_idx"
                Column | Type | Definition
                --------+------+------------
                lower | text | lower(b)
                btree, for table "public.t"
                postgres=# alter index t_lower_idx alter column "lower" set statistics 69;


                部分索引

                      有时需要仅索引表行的一部分。这通常与高度不均匀的分布有关:通过索引搜索不频繁的值是有意义的,但通过对表的完全扫描更容易找到频繁值。

                      我们当然可以在«c»列上建立一个常规索引,它将以我们期望的方式工作:

                  postgres=# create index on t(c);
                  postgres=# analyze t;
                  postgres=# explain (costs off) select * from t where c;
                  QUERY PLAN
                  -------------------------------
                  Index Scan using t_c_idx on t
                  Index Cond: (c = true)
                  Filter: c
                  (3 rows)
                  postgres=# explain (costs off) select * from t where not c;
                  QUERY PLAN
                  -------------------
                  Seq Scan on t
                  Filter: (NOT c)
                  (2 rows)

                          索引大小为276页:

                    postgres=# select relpages from pg_class where relname='t_c_idx';
                    relpages
                    ----------
                    276
                    (1 row)

                            但由于«c»列仅对1%的行具有true值,因此99%的索引实际上从未使用过。在这种情况下,我们可以构建一个部分索引:

                      postgres=# create index on t(c) where c;
                      postgres=# analyze t;

                              索引的大小减少到5页

                        postgres=# select relpages from pg_class where relname='t_c_idx1';
                        relpages
                        ----------
                        5
                        (1 row)

                               有时,大小和性能上的差异可能非常显著。

                        排序

                               如果访问方法以某种特定顺序返回行标识符,这将为优化器提供额外的选项来执行查询。

                               我们可以扫描表格,然后对数据进行排序:

                          postgres=# set enable_indexscan=off;
                          postgres=# explain (costs off) select * from t order by a;
                          QUERY PLAN
                          ---------------------
                          Sort
                          Sort Key: a
                          -> Seq Scan on t
                          (3 rows)

                                 但是我们可以很容易地按照所需的顺序使用索引读取数据:

                            postgres=# set enable_indexscan=on;
                            postgres=# explain (costs off) select * from t order by a;
                            QUERY PLAN
                            -------------------------------
                            Index Scan using t_a_idx on t
                            (1 row)

                                  所有访问方法中只有«btree»可以返回排序数据,所以让我们推迟更详细的讨论,直到考虑到这种类型的索引。


                            并发构建

                                  通常构建索引会获取表的SHARE锁。此锁允许从表中读取数据,但禁止在构建索引时进行任何更改。

                                  我们可以确保这一点,例如,在表«t»中构建索引时,我们在另一个会话中执行以下查询:

                              postgres=# select mode, granted from pg_locks where relation = 't'::regclass;
                              mode | granted
                              -----------+---------
                              ShareLock | t
                              (1 row)

                                     如果表足够大并且广泛用于插入,更新或删除,这可能是不允许的,因为修改进程将等待锁释放很长时间。

                                     在这种情况下,我们可以使用并发构建索引。

                                postgres=# create index concurrently on t(a);

                                       此命令以SHAREUPDATE EXCLUSIVE模式锁定表,该模式允许读取和更新(仅禁止更改表结构,以及并发清理,分析或在此表上构建另一个索引)。

                                      然而,也有不利的一面。首先,索引将比平常构建得更慢,因为在表中完成两次传递而不是一次,并且还需要等待完成修改数据的并行事务。

                                      其次,在并发构建索引时,可能会发生死锁或者违反唯一约束。但是,该索引将被建立,尽管尚未运行。必须删除并重建此类索引。非操作索引在psql \ d命令的输出中用INVALID字标记,下面的查询返回这些索引的完整列表:

                                  postgres=# select indexrelid::regclass index_name, indrelid::regclass table_namefrom pg_index where not indisvalid;
                                  index_name | table_name
                                  ------------+------------
                                  t_a_idx | t
                                  (1 row)

                                  下期预告

                                  下篇文章将关于“访问方法接口的结构”


                                  欢迎投稿



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

                                  投稿邮箱:

                                  press@postgresqlchina.com

                                  原文请点击下方“阅读原文”获取

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

                                  评论