zoukankan      html  css  js  c++  java
  • PostgreSQL中的索引(三) --Hash

    许多现代编程语言都将哈希表作为基本数据类型。从表面上看,哈希表看起来像一个常规数组,使用任何数据类型(例如字符串)建立索引,而不仅是使用整数。PostgreSQL中的哈希索引也是以类似的方式构造的。这是如何运作的呢?

    作为一个规则,数据类型允许的值范围非常大:在一个类型为«text»的列中,我们可以设想有多少不同的字符串?同时,在某个表的«text»的列中实际存储了多少不同的值?通常不会有那么多。

    哈希的思想是将一个小数字(从0到N−1,N个值)与任意数据类型的值相关联。这样的关联称为哈希函数。获得的数字可以用作常规数组的索引,其中存储对表行(tid)的引用。这个数组的元素称为哈希表bucket——如果同一个索引值出现在不同的行中,那么一个bucket可以存储多个tid。

    哈希函数越均匀地按桶分配原值,效果就越好。但即使是一个好的哈希函数,有时也会对不同的原值产生相同的结果——这叫做冲突。因此,一个bucket可以存储对应于不同键的TIDs,因此,需要重新检查从索引中获得的TIDs。

    举个例子,我们能想到字符串的哈希函数是什么?让桶的数目为256。然后,以桶号为例,我们可以获取第一个字符的代码(假设采用单字节字符编码)。这是个好的哈希函数吗?显然不是:如果所有字符串都以相同的字符开头,那么它们都将进入一个bucket,因此一致性是不可能的,所有的值都需要重新检查,哈希就没有任何意义了。如果我们把所有字符的编码以256的模加起来会怎么样?这样会好得多,但还远远不够理想。如果您对PostgreSQL中这样一个哈希函数的内部特性感兴趣,请查看hashfunction.c中的hash_any()定义。

    索引结构

    让我们回到哈希索引。对于某种数据类型的值(索引键),我们的任务是快速找到匹配的TID。

    当插入到索引中时,让我们计算键的哈希函数。PostgreSQL中的哈希函数总是返回«integer»类型,其范围为2的32次方≈40亿个值。桶的数量最初等于两个,然后根据数据大小动态增加。桶数可以使用位算法从哈希码计算出来。这是我们要放TID的桶。

    但是这是不够的,因为匹配不同键的TIDs可以放在同一个桶中。我们该怎么办?除了TID之外,还可以将键的原值存储在桶中,但是这将大大增加索引大小。为了节省空间,bucket存储的不是键值,而是键值的哈希代码。

    在搜索索引时,我们计算键的哈希函数并获得桶号。现在剩下的工作是遍历bucket的内容,只返回具有适当哈希码的匹配tid。这是有效的,因为存储的«哈希码--TID»对是有序的。

    然而,两个不同的键可能不仅进入了一个bucket,而且具有相同的四字节哈希码——冲突还未消除。因此,访问方法要求常规索引引擎通过重新检查表行中的条件来验证每个TID(引擎可以在进行可见性检查的同时执行此操作)。

    将数据结构映射到页

    如果我们从buffer cache管理器查看索引,而不是从查询计划和执行的角度来看,那么所有信息和所有索引行都必须打包到页中。这样的索引页存储在buffer cache中,并以与表页完全相同的方式从那里刷出。

    如图所示,哈希索引使用了四种页面(灰色矩形):

    ·元数据页--零页号,它包含了索引内部的信息。

    ·桶页--索引的主要页,它将数据存储为«哈希码--TID»对。

    ·溢出页--结构与桶页相同,当一个页面不足以容纳一个桶时使用。

    ·位图页--跟踪当前清除的溢出页面,这些溢出页面可以被其他桶重用。

    从索引页元素开始的向下箭头表示TIDs,即对表行的引用。

    每次索引增加时,PostgreSQL会立即创建两倍于上次创建的桶(因此,页面也是如此)。为了避免一次分配大量的页面,version 10进行改进。至于溢出页,它们是在需要出现时分配的,并在位图页中跟踪,位图页也是在需要出现时分配的。

    注意,哈希索引不能减小大小。如果我们删除一些索引行,分配的页将不会返回到操作系统,而只会在vacuuming后用于新数据。减少索引大小的唯一选项是使用REINDEX或VACUUM FULL命令从头重新构建。

    示例

    让我们看看哈希索引是如何创建的。为了避免设计自己的表,从现在开始,我们将使用air transport的演示数据库,这次我们将考虑flights表。

    demo=# create index on flights using hash(flight_no);
    WARNING:  hash indexes are not WAL-logged and their use is discouraged
    CREATE INDEX
    
    demo=# explain (costs off) select * from flights where flight_no = 'PG0001';
                         QUERY PLAN                     
    ----------------------------------------------------
     Bitmap Heap Scan on flights
       Recheck Cond: (flight_no = 'PG0001'::bpchar)
       ->  Bitmap Index Scan on flights_flight_no_idx
             Index Cond: (flight_no = 'PG0001'::bpchar)
    (4 rows)
    

    当前哈希索引实现的不便之处在于,与索引相关的操作没有记录在写WAL中(创建索引时,PostgreSQL会发出警告)。因此,哈希索引在失败后无法恢复,并且不参与复制。此外,哈希索引的通用性远远低于«btree»,其效率也值得怀疑。所以现在使用这些索引是不切实际的。

    然而,这种情况最早将在今年秋天(2017年)PostgreSQL的第10版发布后发生改变。在这个版本中,哈希索引最终支持写wal文件;此外,内存分配得到了优化,并发工作效率更高。

     

    哈希的语义

    但为什么哈希索引几乎从PostgreSQL诞生之初就存在到9.6无法使用?问题是DBMS广泛使用了哈希算法(特别是哈希连接和分组),系统必须知道哪个哈希函数应用于哪个数据类型。但是这种通信不是静态的,也不能一次性地设置,因为PostgreSQL允许动态地添加新的数据类型。这是一种通过哈希的访问方法,这种对应被存储、用辅助函数和操作符族的联系来表示。

    demo=# select   opf.opfname as opfamily_name,
             amproc.amproc::regproc AS opfamily_procedure
    from     pg_am am,
             pg_opfamily opf,
             pg_amproc amproc
    where    opf.opfmethod = am.oid
    and      amproc.amprocfamily = opf.oid
    and      am.amname = 'hash'
    order by opfamily_name,
             opfamily_procedure;
       opfamily_name    | opfamily_procedure
    --------------------+-------------------- 
     abstime_ops        | hashint4
     aclitem_ops        | hash_aclitem
     array_ops          | hash_array
     bool_ops           | hashchar
    ...
    

    虽然这些函数没有文档记录,但它们可用于计算适当数据类型值的哈希码。例如,«hashtext»函数如果用于«text_ops»操作符家族:

    demo=# select hashtext('one');
     hashtext  
    -----------
     127722028
    (1 row)
    
    demo=# select hashtext('two');
     hashtext  
    -----------
     345620034
    (1 row)
    

      

    属性

    让我们看看哈希索引的属性,其中该访问方法向系统提供关于自身的信息。上次我们提供了查询。

         name      | pg_indexam_has_property
    ---------------+-------------------------
     can_order     | f
     can_unique    | f
     can_multi_col | f
     can_exclude   | t
    
         name      | pg_index_has_property
    ---------------+-----------------------
     clusterable   | f
     index_scan    | t
     bitmap_scan   | t
     backward_scan | t
    
            name        | pg_index_column_has_property 
    --------------------+------------------------------
     asc                | f
     desc               | f
     nulls_first        | f
     nulls_last         | f
     orderable          | f
     distance_orderable | f
     returnable         | f
     search_array       | f
     search_nulls       | f
    

    哈希函数不保留顺序关系:如果哈希函数的一个键值小于另一个键值,不可能得出键本身是如何排序的结论。因此,通常哈希索引只能支持«equals»操作:

    demo=# select   opf.opfname AS opfamily_name,
             amop.amopopr::regoperator AS opfamily_operator
    from     pg_am am,
             pg_opfamily opf,
             pg_amop amop
    where    opf.opfmethod = am.oid
    and      amop.amopfamily = opf.oid
    and      am.amname = 'hash'
    order by opfamily_name,
             opfamily_operator;
     opfamily_name |  opfamily_operator  
    ---------------+----------------------
     abstime_ops   | =(abstime,abstime)
     aclitem_ops   | =(aclitem,aclitem)
     array_ops     | =(anyarray,anyarray)
     bool_ops      | =(boolean,boolean)
    ...
    

    因此,哈希索引不能返回有序数据(«can_order»,«orderable»)。哈希索引不操作空值也是出于同样的原因:对于空值,«equals»操作没有意义(«search_nulls»)。

    因为哈希索引不存储键(但只存储它们的哈希码),所以它不能用于index-only访问(«returnable»)。

    此访问方法也不支持多列索引(«can_multi_col»)。

    内部原理

    从version 10开始,可以通过“pageinspect”扩展来查看哈希索引的内部内容。这是它的样子:

    demo=# create extension pageinspect;
    

    元数据页(我们得到索引中的行数和最大使用桶数):

    demo=# select hash_page_type(get_raw_page('flights_flight_no_idx',0));
     hash_page_type 
    ----------------
     metapage
    (1 row)
    
    demo=# select ntuples, maxbucket
    from hash_metapage_info(get_raw_page('flights_flight_no_idx',0));
     ntuples | maxbucket 
    ---------+-----------
       33121 |       127 
    (1 row)
    

    一个桶页(我们得到live的元组和死元组的数量,也就是那些可以被vacuum的元组):

    demo=# select hash_page_type(get_raw_page('flights_flight_no_idx',1));
     hash_page_type
    ----------------
     bucket
    (1 row)
    
    demo=# select live_items, dead_items
    from hash_page_stats(get_raw_page('flights_flight_no_idx',1));
     live_items | dead_items
    ------------+------------
            407 |          0
    (1 row)
    

    但是,如果不检查源代码,几乎不可能弄清所有可用字段的含义。
    如果您希望这样做,您应该从https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/access/hash/README;hb=HEAD开始。

    原文地址:https://habr.com/en/company/postgrespro/blog/442776/

  • 相关阅读:
    luogu P4779 【模板】单源最短路径(标准版)| dijkstra
    luogu P1160 队列安排 | 链式前向星
    luogu P1996 约瑟夫问题 | 链表
    luogu P3386 【模板】二分图匹配 | 匈牙利算法
    luogu P3366 【模板】最小生成树 | kruskal
    luogu P3378 【模板】堆
    luogu P3372 【模板】线段树 1
    luogu P1776 宝物筛选_NOI导刊2010提高(02) | 多重背包(二进制拆分)
    luogu P1616 疯狂的采药 | 完全背包
    luogu P1588 丢失的牛 | 宽搜
  • 原文地址:https://www.cnblogs.com/abclife/p/13434867.html
Copyright © 2011-2022 走看看