什么索引可用于大量重复值?


14

让我们做一些假设:

我有这样的表:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

关于我的场景的事实:

  • 整个表的大小是〜10 10行。

  • 我有〜100k行,其中acolumn中有值,a其他值也类似(例如c)。

  • 这意味着“ a”列中的〜100k个不同的值。

  • 我的大部分查询将读取例如中的给定值的全部或大部分值select sum(b) from t where a = 'c'

  • 该表以这样的方式编写,即连续值在物理上接近(或者按顺序编写,或者我们假设CLUSTER已在该表和column上使用过a)。

  • 该表很少更新(如果有的话),我们只关心读取速度。

  • 该表相对较窄(例如每个元组约25个字节,+ 23个字节的开销)。

现在的问题是,我应该使用哪种索引?我的理解是:

  • BTree我的问题是BTree索引将是巨大的,因为据我所知它将存储重复值(它必须这样做,因为它不能假定表是物理排序的)。如果BTree很大,那么我最终必须同时读取索引和该索引指向的表的各个部分。(我们可以用来fillfactor = 100稍微减小索引的大小。)

  • BRIN我的理解是,我可以在这里建立一个小的索引,而以阅读无用的页面为代价。使用较小的值pages_per_range表示索引较大(这是BRIN的问题,因为我需要读取整个索引),使用较大的值pages_per_range表示我将读取很多无用的页面。pages_per_range考虑到这些折衷,是否有一个神奇的公式可以找到一个好的价值?

  • GIN / GiST不确定它们在这里是否相关,因为它们主要用于全文本搜索,但我也听说它们擅长处理重复键。a GINGiSTindex会在这里帮助吗?

另一个问题是,Postgres是否会使用CLUSTER在查询计划程序中编辑表(假设没有更新)的事实(例如,通过二进制搜索相关的起始/结束页)?某种程度上相关,我是否可以将所有列存储在BTree中并完全删除表(或实现等效的操作,我相信那些是SQL Server中的聚集索引)?有一些混合的BTree / BRIN索引在这里会有所帮助吗?

我宁愿避免使用数组来存储我的值,因为我的查询最终将以这种方式降低可读性(我知道这将通过减少元组的数量来减少每个元组开销的23个字节的开销)。


“最主要用于全文搜索” GiST被PostGIS广泛使用。
jpmc26,2017年

Answers:


15

我的问题是BTree索引会很大,因为它会存储重复的值(它也是如此,因为它不能假设表是物理排序的)。如果BTree很大,我最终不得不同时读取索引和索引所指向的表的各个部分...

不一定—具有“覆盖”的btree索引将是最快的读取时间,如果这就是您想要的(即,如果您能负担得起额外的存储空间),那么这是您的最佳选择。

布林

我的理解是,在这里我可以有一个小的索引,但以读取无用的页面为代价。使用较小的值pages_per_range表示索引较大(这是BRIN的问题,因为我需要读取整个索引),使用较大的值pages_per_range表示我将读取很多无用的页面。

如果您负担不起覆盖btree索引的存储开销,则BRIN非常适合您,因为您已经建立了集群(这对于BRIN有用至关重要)。BRIN索引很小,因此,如果选择合适的值,则所有页面都可能在内存中pages_per_range

是否有一个神奇的公式可以找到考虑到这些折衷因素的pages_per_range的良好价值?

没有魔术公式,但开头pages_per_range 要比平均值所占的平均大小(以页为单位)小一些a。您可能正在尝试最小化:(典型扫描的)((扫描的BRIN页的数量)+(扫描的堆页面的数量)。查找Heap Blocks: lossy=n在执行计划pages_per_range=1,并与其他值进行比较的pages_per_range-即看看有多少不必要的堆块进行扫描。

杜松子酒

由于它们主要用于全文搜索,因此不确定此处是否相关,但我也听说它们擅长处理重复键。GIN/ GiST索引在这里有帮助吗?

GIN可能值得考虑,但GiST可能不值得考虑-但是,如果自然聚类确实很好,那么BRIN可能会是一个更好的选择。

这是一个与您的虚拟数据不同的索引类型之间的示例比较:

表和索引:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

关系大小:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
名称| 尺寸 页数| 行/页
:----------------- | :------ | ----:| --------:
foo | 149 MB | 19118 | 135
foo_btree_covering | 56 MB | 7132 | 364
foo_btree | 56 MB | 7132 | 364
foo_gin | 2928 kB | 366 | 7103
foo_brin_2 | 264 kB | 33 | 78787
foo_brin_4 | 136 kB | 17 | 152941

覆盖btree:

explain analyze select sum(b) from foo where a='a';
| 查询计划|
| :------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
| 总计(成本= 3282.57..3282.58行= 1宽度= 8)(实际时间= 45.942..45.942行= 1循环= 1)|
| ->仅索引在foo上使用foo_btree_covering进行扫描(成本= 0.43..3017.80行= 105907宽度= 4)(实际时间= 0.038..27.286行= 100000循环= 1)|
| 索引条件:(a ='a':: text)|
| 堆访存:0 |
| 计划时间:0.099毫秒|
| 执行时间:45.968 ms |

普通btree:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
| 查询计划|
| :------------------------------------------------- -------------------------------------------------- ----------------------------- | |
| 总计(成本= 4064.57..4064.58行= 1宽度= 8)(实际时间= 54.242..54.242行= 1循环= 1)|
| ->在foo上使用foo_btree进行索引扫描(成本= 0.43..3799.80行= 105907宽度= 4)(实际时间= 0.037.0.33.04行= 100000循环= 1)|
| 索引条件:(a ='a':: text)|
| 计划时间:0.135毫秒|
| 执行时间:54.280毫秒|

BRIN pages_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
| 查询计划|
| :------------------------------------------------- -------------------------------------------------- ----------------------------- | |
| 总计(成本= 21595.38..21595.39行= 1宽度= 8)(实际时间= 52.455..52.455行= 1循环= 1)|
| ->在foo上进行位图堆扫描(成本= 888.78..21330.61行= 105907宽度= 4)(实际时间= 2.738..31.967行= 100000循环= 1)|
| 重新检查条件:(a ='a':: text)|
| 通过索引删除的行重新检查:96 |
| 堆块:有损= 736 |
| ->在foo_brin_4上进行位图索引扫描(成本= 0.00..862.30行= 105907宽度= 0)(实际时间= 2.720..2.720行= 7360循环= 1)|
| 索引条件:(a ='a':: text)|
| 计划时间:0.101毫秒|
| 执行时间:52.501毫秒|

BRIN pages_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
| 查询计划|
| :------------------------------------------------- -------------------------------------------------- ----------------------------- | |
| 总计(成本= 21659.38..21659.39行= 1宽度= 8)(实际时间= 53.971..53.971行= 1循环= 1)|
| ->在foo上进行位图堆扫描(成本= 952.78..21394.61行= 105907宽度= 4)(实际时间= 5.286..33.492行= 100000循环= 1)|
| 重新检查条件:(a ='a':: text)|
| 通过索引删除的行重新检查:96 |
| 堆块:有损= 736 |
| ->在foo_brin_2上进行位图索引扫描(成本= 0.00..926.30行= 105907宽度= 0)(实际时间= 5.275..5.275行= 7360循环= 1)|
| 索引条件:(a ='a':: text)|
| 计划时间:0.095毫秒|
| 执行时间:54.016毫秒|

杜松子酒:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
| 查询计划|
| :------------------------------------------------- -------------------------------------------------- ------------------------------ |
| 总计(成本= 21687.38..21687.39行= 1宽度= 8)(实际时间= 55.331..55.331行= 1循环= 1)|
| ->在foo上进行位图堆扫描(成本= 980.78..21422.61行= 105907宽度= 4)(实际时间= 12.377..33.956行= 100000循环= 1)|
| 重新检查条件:(a ='a':: text)|
| 堆块:确切= 736 |
| ->在foo_gin上进行位图索引扫描(成本= 0.00..954.30行= 105907宽度= 0)(实际时间= 12.271..12.271行= 100000循环= 1)|
| 索引条件:(a ='a':: text)|
| 计划时间:0.118毫秒|
| 执行时间:55.366毫秒|

dbfiddle 在这里


因此,覆盖索引会完全跳过表的读取,但会浪费磁盘空间?似乎是一个很好的权衡。我认为通过“读取整个索引”(如果我错了,请更正我)对于BRIN索引具有相同的含义,我的意思是扫描整个BRIN索引,我认为这是dbfiddle.uk/…中发生的事情,不是吗?
foo

@foo有关“(它也有,因为它不能假定表是物理排序的)。” 该表的物理顺序(是否群集)无关紧要。索引具有正确顺序的值。但是Postgres B树索引必须存储所有值(是的,要多次存储)。这就是它们的设计方式。仅将每个不同的值存储一次将是一个不错的功能/改进。您可以将其建议给Postgres开发人员(甚至帮助实现它。)Jack应该发表评论,我认为Oracle的b-tree实现可以做到这一点。
ypercubeᵀᴹ

1
@foo-完全正确,对BRIN索引的扫描始终会扫描整个索引(pgcon.org/2016/schedule/attachments / ...,最后一张幻灯片)-尽管小提琴中的解释计划未显示, 是吗?
杰克说请尝试topanswers.xyz

2
@ypercubeᵀᴹ您可以在Oracle上使用COMPRESS,该压缩每个块存储每个不同的前缀一次。
杰克说尝试topanswers.xyz

@JackDouglas我读的Bitmap Index Scan意思是“读了整个brin索引”,但也许是错误的读法。Oracle COMPRESS看起来在这里很有用,因为它可以减小B树的大小,但是我对pg感到困惑!
foo

6

除了btreebrin似乎是最明智的选择之外,还有一些其他的奇特选择可能值得研究-它们在您的情况下可能有用或无效:

  • INCLUDE索引。希望它们将在Postgres的下一个主要版本(10)中,大约在2017年9月。索引on (a) INCLUDE (b)的结构与索引的结构相同,(a)但在叶子页中包含的所有值b(但无序)。这意味着您不能将其用于SELECT * FROM t WHERE a = 'a' AND b = 2 ;。可以使用索引,但是当(a,b)索引将通过一次查找找到匹配的行时,include索引将必须经过(匹配您的情况为100K)匹配a = 'a'并检查这些b值的值。
    另一方面,索引的宽度略小于索引的宽度,因此(a,b)不需要b查询的顺序即可进行计算SUM(b)。你也可以有例如(a) INCLUDE (b,c,d) 可以用于与您的查询类似的查询,这些查询汇总在所有3列上。

  • 过滤后的(部分)索引。一开始可能听起来有些疯狂的建议*

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;
    

    每个a值一个索引。在您的情况下,大约有10万个索引。尽管这听起来很多,但请注意每个索引在大小(行数)和宽度(因为它将仅存b储值)方面都非常小。但是,在所有其他方面,它(一起使用100K索引)将(a,b)在使用(b)索引空间的同时充当b树索引。
    缺点是,每次将新值a添加到表中时,您都必须自己创建和维护它们。由于您的表相当稳定,没有很多(或任何)插入/更新,因此这似乎不是问题。

  • 汇总表。由于表格相当稳定,因此您始终可以使用所需的最常见的汇总(sum(b), sum(c), sum(d), avg(b), count(distinct b),等等)创建并填充汇总表格。它很小(仅10万行),仅在主表中插入/更新/删除行时才需要填充一次并进行更新。

*:从该公司复制的想法,该公司在其生产系统中运行1000万个索引:The Heap:在生产中运行1000万个Postgresql索引(并在计数)


1很有趣,但是正如您指出的,第10页尚未发布。2 听起来确实很疯狂(或至少反对“常识”),但我会读一读,因为正如您指出的那样,这几乎可以用于我几乎没有写入的工作流程。3.将不适合我的工作,我曾经SUM作为一个例子,但在实践中我查询不能预先计算(他们更像select ... from t where a = '?' and ??wjere ??将一些其它用户定义的条件。

1
好吧,如果我们不知道这??是什么,我们就无能为力了;)
ypercubeᵀᴹ17

您提到了过滤索引。分区表呢?
jpmc26,2017年

@ jpmc26有趣,我想在答案中添加过滤索引的建议在某种意义上是一种分区形式。分区在这里也可能会有所帮助,但我不确定。这将导致很多小的索引/表。
ypercubeᵀᴹ

2
我希望部分覆盖btree索引在这里成为性能之王,因为数据几乎从未更新。即使这意味着100k索引。总索引大小最小(BRIN索引除外,但Postgres必须另外读取和过滤堆页面)。可以使用动态SQL自动生成索引。相关答案中的示例DO语句
Erwin Brandstetter,2017年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.