【问题标题】:Searching on expression indexes搜索表达式索引
【发布时间】:2019-09-19 01:32:00
【问题描述】:

搜索表达式索引

我正在构建一个表达式索引,它对源字段进行子串化以避免溢出 B 树上的 2172 个字符限制:

CREATE INDEX record_changes_log_detail_old_value_ix_btree
    ON record_changes_log_detail 
    USING btree ((substring(old_value,1,1024)::text) text_pattern_ops);

记录在案:

-- RDS 上的 Postgres 11.4,家庭 macOS 上的 11.5。
-- 在我的测试设置中,record_changes_log_detail 表大约有 8M。
-- old_value 字段的类型为 citext。
-- 字段中的值的长度范围从 1 个字符到超过 5,000 个字符。大多数都很短。

此搜索使用上面指定的索引:

select * from record_changes_log_detail 
where substring(old_value,1,1024) = 'Gold Kerrison Neuro';

此搜索使用索引:

where old_value = 'Gold Kerrison Neuro';

我觉得这很令人惊讶,也很令人沮丧。如果我正确理解 jjanes 对另一个问题的评论,则规划器仅在您的查询语句使用 exactly 相同的表达式时才识别出索引适用。换句话说,任何编写查询的人都需要知道索引定义的详细信息,否则索引将不会被使用。

我一直假设在构建表达式索引时,缩写/提取/等。值已存储,计划者将对其进行检查。 除了概括整个表达式之外,还有什么方法可以提示规划器?索引有正确的数据,但规划器似乎跳过了它。

我根据 Erwin Brandstetter 的回答添加了一些细节:

我有很多类似的情况,这就是我在这里挖掘细节的原因。在这种情况下,在我的约 8M 行中,只有 6 行的值超过 2172 个字符,并且 99.93% 的值是 100 个字符或更少。

我希望的是一种其他人容易接受的方法。阴影字段很可能是答案,因为必须知道索引构造的确切细节让我觉得完全错误的可见性。一旦你知道使用它,阴影场就不会遇到这个问题。正如您所提到的,我可以使用 LEFT(old_field,128) 或其他长度或 texthash(old_field) 填充它。我会试验一下。我的数据非常偏向于短值,散列似乎会导致高冲突率。

对于它的价值,我和团队来自一个系统,当在 B 树中索引时,文本字段被静默修剪为 1024 个字符。它对用户完全透明,搜索查询索引。苹果和火花塞,我知道。关键是我不希望 Postgres 成为 AI,但我 am 提出了不准确的先验。因此,感谢您和其他所有人帮助我了解有关 Postgres 实际工作原理的更多信息。


跟进

这个问题已经回答了,但我想为档案添加一些后续行动。我一直在从旧答案中学到很多东西,其中一些非常古老。所以这里有一些关于未来的信息。我尝试了四种解决方案:

  • 部分 citext 字段上的 B-tree。
  • citext 字段哈希上的 B-tree。
  • citext 字段的哈希索引。
  • citext 字段的 Tri-gram GIN 索引。

由于似乎没有任何方法可以在文本可能太长的 citext 上获取 LIKE 类型的查询,因此目标是为 = 创建一个索引。以上三个中的任何一个都可以正常工作,但它们有很大的不同。这是测试的一些设置代码:

DROP INDEX IF EXISTS record_changes_log_detail_old_value_ix_btree;
CREATE INDEX record_changes_log_detail_old_value_ix_btree
    ON record_changes_log_detail
    USING btree ((left(old_value,1024)::citext) citext_pattern_ops);


DROP INDEX IF EXISTS record_changes_log_detail_old_value_hash_ix_btree;
CREATE INDEX record_changes_log_detail_old_value_hash_ix_btree
    ON record_changes_log_detail
    USING btree (hashtext(old_value));

DROP INDEX IF EXISTS record_changes_log_detail_old_value_ix_hash;
CREATE INDEX record_changes_log_detail_old_value_ix_hash
    ON record_changes_log_detail
    USING hash (old_value);    

DROP INDEX IF EXISTS record_changes_log_detail_old_value_ix_tgrm;
CREATE INDEX record_changes_log_detail_old_value_ix_tgrm
    ON record_changes_log_detail 
    USING gin (old_value gin_trgm_ops);

VACUUM ANALYZE;

这些索引都用于查找记录,但语法不同:

-- Uses the LEFT()::citext index
explain analyze
select * from record_changes_log_detail 
where left(old_value,1024)::citext = 'Gold Kerrison Neuro';

-- Uses the HASH index
explain analyze
select * from record_changes_log_detail 
where old_value = 'Gold Kerrison Neuro';

-- Uses the HASHTEXT() index
explain analyze
select * from record_changes_log_detail 
where hashtext(old_value) = hashtext('Gold Kerrison Neuro');

-- Uses the tri-gram() index
explain analyze
select * from record_changes_log_detail 
where old_value::text LIKE '%Gold Kerrison Neuro%';

散列索引提供了最好的语法,因为它是透明的……但是散列索引在其他所有方面都是最差的。这是尺寸搜索和结果。我在这里手动添加了报告的索引构建时间。

select
'B-tree on LEFT(old_value,1024)::citext' as index_description,
pg_size_pretty(pg_relation_size ('record_changes_log_detail_old_value_ix_btree')) as pretty

union all

select
'B-tree on HASHTEXT(old_value)' as index_description,
pg_size_pretty(pg_relation_size ('record_changes_log_detail_old_value_hash_ix_btree')) as pretty

union all

select
'Hash index on old_value' as index_description,
pg_size_pretty(pg_relation_size ('record_changes_log_detail_old_value_ix_hash')) as pretty

union all

select
'GIN tri-gram index on old_value' as index_description,
pg_size_pretty(pg_relation_size ('record_changes_log_detail_old_value_ix_tgrm')) as pretty;


index_description                       pretty  seconds
B-tree on LEFT(old_value,1024)::citext  238 MB       38
B-tree on HASHTEXT(old_value)           166 MB        7
Hash index on old_value                 362 MB    3,802
GIN tri-gram index on old_value         106 MB       56

我会说这个数据与哈希索引的匹配很糟糕,所以请不要将这些结果视为典型结果。尽管如此,时间和规模还是很糟糕的。 = 搜索的明显赢家是 Erwin Brandstetter 对 B-tree 哈希的巧妙建议。好的!搜索所需的额外语法糖在这里并不像基于 LEFT 的索引那样糟糕。展望未来,这将受益于 PG 12 中承诺的 B-tree 改进。

还有更多好消息,三元组索引非常棒。 Laurenz Albe 建议尝试一下,我很高兴我做到了。即时包含/喜欢搜索,完美。这正是我所需要的。再次在这里,我怀疑索引大小是典型的......我的数据很奇怪。对于使用 citext 的用户,请注意您必须将搜索条件转换为要使用的索引的文本:

select * from record_changes_log_detail 
where old_value::text LIKE '%Gold Kerrison Neuro%';

对于那些不知道的人,tri-gram 是长度为 3 的 n-gram 的一个实例。N-gram 有时被称为 q-gram 或 k-gram。不管怎样,都是一样的。在所有幼稚(非概率或统计)模糊文本匹配算法中,它可能是最好的。在不同的数据集和语言上鲁棒,灵活,真棒。所以我对它在 Postgres 中的表现非常满意。

【问题讨论】:

  • 有用的回顾,感谢您的跟进。也让我想起了对哈希冲突的缺失防御。

标签: postgresql indexing expression


【解决方案1】:

就像你从其他地方的 jjanes 中读到的一样:只有当表达式在查询谓词中完全匹配时,才会考虑表达式索引。 Postgres 查询计划器不是人工智能。如果计划查询时间过长,这将很快破坏快速查询的目的。

如果有什么安慰的话,您可以稍微优化一下索引。 left()substring() 更简单更快:

CREATE INDEX record_changes_log_detail_old_value_ix_btree
ON record_changes_log_detail (left(old_value,1024) text_pattern_ops);

此外,对于 btree 索引,最大行大小为 2704 字节,而不是 “B-trees 上的 2172 个字符限制”

最重要的是,就像您的问题所暗示的那样,仅对于相等性检查,使用 md5(old_value)hashtext(old_value) 的哈希值上的 btree 索引会更加更有效率。如果这样做,请记住防御 哈希冲突,如下所示:

SELECT *
FROM   record_changes_log_detail 
WHERE  hashtext(old_value) = hashtext('Gold Kerrison Neuro')
AND    old_value = 'Gold Kerrison Neuro';

第一个谓词为您提供快速索引访问。第二个排除误报。碰撞应该是非常罕见的。但有可能。而且这种可能性随着桌子的大小而增加。

相关:

或者hash index,就像您已经考虑过的那样:

(这里您无需担心哈希冲突;内部处理。)

【讨论】:

  • 感谢 cmets,我已经用一些背景信息更新了我的答案。感谢有关 LEFT() 的提示,我应该尝试一下!我去看看。
  • @MorrisdeOryx:注意对哈希冲突的额外防御。
  • 感谢有关碰撞的提醒,以及处理它的第二个搜索子句,非常好!我绝对忘了这样做。对于稍后阅读本文的任何人,第二个子句 - 和 - 过滤散列比较返回的候选行。因此,索引用于快速搜索,然后比较该值池的真正相等性。 EXPLAIN ANALYZE 整个星期都是我的朋友 ;-)
猜你喜欢
  • 2011-10-13
  • 1970-01-01
  • 1970-01-01
  • 2016-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-03
相关资源
最近更新 更多