【问题标题】:Indexing a JSONB key in combination with other columns结合其他列索引 JSONB 键
【发布时间】:2021-05-04 12:47:46
【问题描述】:

为了在 jsonb 列中搜索特定键,我想在该列上创建一个索引。

使用:Postgres 10.2

忽略一些不相关的列,我的表 animals 包含这些列(省略了一些不相关的列):

animalid PK number
location (text)
type (text)
name (text)
data (jsonb) for eg: {"age": 2, "tagid": 11 }

我需要根据:locationtypetagId 进行搜索。喜欢:

where location = ? and type = 'cat' and (data ->> 'tagid') = ?

其他要点:

  • 只有猫类型的动物会有标签 ID,这是现在添加的新动物类型。
  • 与其他类型的动物相比,整个表中“猫”的数量会更少。
  • 表很大,有数百万行 - 并且是分区的。

如何确保搜索速度很快?我考虑过的选项:

  1. 制作一个单独的表cats来存储:animal_idlocationtagId(虽然FK到分区的父表是不可能的)
  2. locationtype 和 jsonb 键上创建索引。
  3. 创建一个新的(索引的)列 tagId - 对于除猫以外的所有动物,该列都为空。

我确实在表的其他列上有一个索引 - 但对于如何创建索引以根据 tagid 快速搜索猫有点困惑。有什么建议吗?

更新(忽略分区):

(在分区表上测试)

所以我决定采用 Erwin 建议的选项并尝试创建索引

CREATE INDEX ON animals_211 (location, ((data->>'tagid')::uuid)) WHERE  type = 'cat';

并尝试对查询进行解释(使用分区表保持简单):

explain select * from animals_211 a
where a.location  = 32341
and a.type  = 'cat'
and (data->>'tagid')::uuid = '5e54c1d9-3ea0-4bca-81d6-1000d90cc42c'

从结果来看,它似乎没有使用创建的索引并进行顺序扫描:

Seq Scan on animals_211  e  (cost=0.00..121.70 rows=1 width=327)                                                                                                        |
  Filter: ((location = 32341) AND ((type)::text = 'cat'::text) AND (((data ->> 'tagid'::text))::uuid = '5e54c1d9-3ea0-4bca-81d6-1000d90cc42c'::uuid

UPDATE 2(不使用部分索引)

它似乎是部分索引,没有它 - 它似乎工作:

CREATE INDEX tag_id_index ON animals_211 (location, type, ((data->>'tagid')::uuid))

当我制定解释计划时:

Index Scan using tag_id_index on animals_211 e  (cost=0.28..8.30 rows=1 width=327)                                                                                         
  Index Cond: ((location = 32341) AND ((type)::text = 'cat'::text) AND (((data ->> 'tagid'::text))::uuid = '5e54c1d9-3ea0-4bca-81d6-1000d90cc42c'::uuid))

【问题讨论】:

  • 您的更新令人困惑。部分索引在父表上,但 Postgres 10 不支持父表上的索引。您应该会看到一个错误消息。我围绕该主题在我的答案中添加了更多内容。我建议将最佳索引问题与最佳分区问题分开......
  • @ErwinBrandstetter 能够创建索引,没有错误消息。但它没有出现在表的索引列表中。我同意,让我们将分区排除在外(我需要为所有分区手动执行此操作) - 我尝试专门在一个分区上创建索引。创建了索引,但解释计划显示它仍然不使用索引。我只尝试了位置+类型只是为了查看并且有效。所以不知何故,这似乎是 jsonb 索引的问题?
  • 部分索引肯定可以工作。也许你需要运行ANALYZE。考虑一下我添加到答案中的注释和链接。

标签: postgresql indexing database-design jsonb postgresql-performance


【解决方案1】:

基础知识(忽略分区)

根据您的三个“要点”,我建议在表达式上使用partial index

CREATE INDEX ON animals ((data->>'tagid'))
WHERE  type = 'cat';

使用CREATE INDEX CONCURRENTLY ... 来避免并发写访问同一个表时出现锁定问题。

Postgres 还收集部分索引的特定统计信息,这有助于查询规划器获得适当的估计。 注意如果您在创建后立即测试索引,则需要手动运行ANALYZE(或VACUUM ANALYZE),然后autovacuum 才能启动。请参阅:

如果tagid 确实是text 之外的其他数据类型,您还可以强制转换表达式以进行更多优化。见:

您的 更新 建议 tagid 存储 UUID 值。阅读:

所以考虑这个索引:

CREATE INDEX ON animals (((data->>'tagid')::uuid))  -- !
WHERE  type = 'cat';

(data->>'tagid')::uuid 周围的额外括号是为了使语法明确。
和一个匹配的查询:

SELECT *
FROM   animals
WHERE  location = 32341
AND    type = 'cats'
AND    (data->>'tagid')::uuid = '5e54c1d9-3ea0-4bca-81d6-1000d90cc42c';  -- !

或者 - 根据每个谓词的选择性以及可能的查询变体 - 包括 location 以使其成为多列索引:

CREATE INDEX ON animals (location, ((data->>'tagid')::uuid))
WHERE  type = 'cat';

或者tagid,如果您有未过滤位置的查询。见:

由于只有相对较少的行属于“cat”类型,因此索引将相对较小,不包括“数百万行”中的大部分。我们只需要tagid 上的索引就可以让猫开始。双赢。

如果可能,将 json 键 data->>'tagid' 拆分为专用列。 (就像您认为的选项 3.)在不适用的情况下可以为 null,null 存储非常便宜。使存储和索引更便宜,但查询更简单。

分区

Postgres 10 不支持分区表的父表上的索引。这是在 Postgres 11 中添加的。此后,声明式分区得到了改进很多。考虑升级到当前版本 13 或更高版本。

还有“旧式”partitioning with inheritance 的选项。然后,您可以为猫创建一个单独的分区,其中只有一个附加列 tagidThe manual:

对于声明性分区,分区必须具有与分区表完全相同的列集,而对于表继承,​​子表可能具有父表中不存在的额外列。

听起来很合适。但是继承在 Postgres 中已经失宠,所以在这样做之前我会三思而后行。

无论是声明式还是继承式 - 如果您将所有“猫”都放在单独的分区中,则非部分索引显然可以完成这项工作:

CREATE INDEX ON cats (location, ((data->>'tagid')::uuid));

并且查询可以针对分区cats而不是父表:

SELECT *
FROM   cats
WHERE  location = 32341
AND    (data->>'tagid')::uuid = '5e54c1d9-3ea0-4bca-81d6-1000d90cc42c';

定位父表也应该有效。 (不确定 Postgres 10。)

SELECT *
FROM   animals
WHERE  type = 'cat'
AND    location = 32341
AND    (data->>'tagid')::uuid = '5e54c1d9-3ea0-4bca-81d6-1000d90cc42c';

但是为此激活partition pruning。手册:

请注意,分区修剪仅由定义的约束驱动 隐含地由分区键,而不是由索引的存在。 因此没有必要在键列上定义索引。

应该修剪所有其他分区,然后您应该只对cats 分区进行索引扫描...

【讨论】:

  • 刚刚更新了我的问题,我按照你的建议做了。但似乎没有使用索引。我可能哪里出错了?
  • 升级 Postgresql 不是一个选项 atm(虽然我理解它的重要性\相关性),我们不想为此创建一个单独的列,因为我们希望这些列用于跨动物的公共字段并且不希望将来为 cat\new 动物单独列
  • 仅作记录:如果超过百分之几是猫,通常有一个专用列 tagid 仍然有效,对于非猫来说它是 NULL。可选择使用CHECK 约束强制执行。 NULL 存储非常便宜。按照上面的链接。
  • 我已经成功地创建了索引器,但不得不重试很多次——因为它会引发死锁错误(这些是同时发生写入的活动表)。我正在考虑在创建索引时使用 CONCURRENTLY - 它有助于防止死锁吗? (在文档中我可以看到它不会锁定表以防止写入,并且会等待当前事务完成 - 也允许在索引创建期间进行事务)
  • @Chillax:是的,这正是CONCURRENTLY 的用途。我也在上面添加了一个链接。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-08-26
  • 2020-12-08
  • 1970-01-01
  • 2013-03-27
  • 1970-01-01
  • 2020-09-25
  • 2018-05-13
相关资源
最近更新 更多