Postgres 使用带有 `split_part` 的索引

Postgres use index with `split_part`

上下文:

我有一个 test table:

=> \d+ test 
                                       Table "public.test"
Column     |          Type          | Collation | Nullable | Default | Storage  | Stats target | Description 
---------------+------------------------+-----------+----------+---------+----------+-------- 
------+-------------
 id            | character varying(255) |           |          |         | extended |              
| 
 configuration | jsonb                  |           |          |         | extended |              
| 

configuration 列包含 "well-defined" json,其中有一个名为 source_url 的键(跳过其他不相关的键)。 configuration 列的示例值为:

{
"source_url": "https://<resource-address>?Signature=R1UzTGphWEhrTTFFZnc0Q4qkGRxkA5%2BHFZSfx3vNEvRsrlDcHdntArfHwkWiT7Qxi%2BWVJ4DbHJeFp3GpbS%2Bcb1H3r1PXPkfKB7Fjr6tFRCetDWAOtwrDrVOkR9G1m7iOePdi1RW%2Fn1LKE7MzQUImpkcZXkpHTUgzXpE3TPgoeVtVOXXt3qQBARpdSixzDU8dW%2FcftEkMDVuj4B%2Bwiecf6st21MjBPjzD4GNVA%2F6bgvKA6ExrdYmM5S6TYm1lz2e6juk81%2Fk4eDecUtjfOj9ekZiGJVMyrD5Tyw%2FTWOrfUB2VM1uw1PFT2Gqet87jNRDAtiIrJiw1lfB7Od1AwNxIk0Rqkrju8jWxmQhvb1BJLV%2BoRH56OHdm5nHXFmQdldVpyagQ8bQXoKmYmZPuxQb6t9FAyovGMav3aMsxWqIuKTxLzjB89XmgwBTxZSv5E9bkWUbom2%2BWq4O3%2BCrVxYwsqg%3D%3D&Expires-At=1569340020&Issued-At=1568293200"
    .
    .
}

URL 包含查询参数 Expires-At


问题:

有一个计划作业,每 24 小时 运行s。这项工作应该找到所有这些 expired/about 过期的记录(然后对其进行处理)。

解法:

我有这个查询来完成我的工作:

select * from test where to_timestamp(split_part(split_part(configuration->>'source_url', 'Expires-At=', 2), '&', 1)::bigint) <= now() + interval '24 hours';

解释:

所以,最后,在每个 运行 中,调度程序刷新所有将在接下来的 24 小时内过期的 url(包括那些已经过期的)


问题:

  1. 虽然这解决了我的问题,但我真的不喜欢这个解决方案。这有很多我觉得不干净的字符串操作。有更简洁的方法吗?
  2. 如果我们"have"采用上述解决方案,我们甚至可以为这种查询使用索引吗?我知道函数 lower()upper() extra 可以被索引,但我真的想不出任何方法可以索引这个查询。

备选方案:

除非有一个真正干净的解决方案,否则我将继续这样做:

我承认我这样重复信息Expires-At,但是在我能想到的所有可能的解决方案中,这是我认为最干净的一个。

大家能想到比这更好的方法吗?



编辑:

更新查询以使用 substring() 和正则表达式而不是内部 split_part():

select * from test where to_timestamp(split_part(substring(configuration->>'source_url' from 'Expires-At=\d+'), '=', 2)::bigint) <= now() + interval '24 hours';

鉴于您当前的数据模型,我认为您的 WHERE 条件没有那么糟糕。

你可以用

索引它
CREATE INDEX ON test ( 
   to_timestamp(
      split_part(
         split_part(
            configuration->>'source_url',
            'Expires-At=',
            2
         ),
         '&',
         1
      )::bigint
   )
);

本质上,您必须为 = 左侧的整个表达式建立索引。只有当涉及的所有函数和运算符都是 IMMUTABLE 时,你才能这样做,我认为它们就是你的情况。

不过我会更改数据模型。首先,我看不到其中包含单个值的 jsonb 列的价值。为什么不将 URL 作为 text 列呢?

您可以走得更远,将 URL 分成单独的部分,并存储在列中。

这一切是否是个好主意取决于您如何使用数据库中的值:通常将您在 WHERE 条件等中使用的数据部分拆分出来是个好主意剩下的 "in a lump"。这在某种程度上是一个品味问题。

您可以使用 URI 解析模块,如果这是您认为不干净的部分。您可以使用 plperl 或 plpythonu,以及您喜欢的任何 URI 解析器库。但是,如果您的 json 真的是 "well defined",我认为没有多大意义。除非您已经在使用 plperl 或 plpythonu,否则添加这些依赖项可能会增加 "dirt" 而不是删除的依赖项。

您可以建立索引:

create index on test (to_timestamp(split_part(split_part(configuration->>'source_url', 'Expires-At=', 2), '&', 1)::bigint));
set enable_seqscan TO off;
explain select * from test where to_timestamp(split_part(split_part(configuration->>'source_url', 'Expires-At=', 2), '&', 1)::bigint) <= now() + interval '24 hours';
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Index Scan using test_to_timestamp_idx1 on test  (cost=0.13..8.15 rows=1 width=36)
   Index Cond: (to_timestamp(((split_part(split_part((configuration ->> 'source_url'::text), 'Expires-At='::text, 2), '&'::text, 1))::bigint)::double precision) <= (now() + '24:00:00'::interval))

I would introduce a new key inside configuration json called expires_at, making sure, this gets filled with the correct value, every time a row is inserted.

那不就是重新整理一下污垢吗?它使查询看起来更好,但代价是使插入更难看。或许您可以将其放入 INSERT OR UPDATE 触发器中。