8

我有一个名为 的 PostgreSQL 表queries_query,它有很多列。

其中两个列createduser_sid经常在我的应用程序的 SQL 查询中一起使用,以确定给定用户在过去 30 天内执行了多少次查询。我在最近 30 天之前的任何时间查询这些统计数据是非常非常罕见的。

这是我的问题:

我目前通过运行在这两列上创建了多列索引:

CREATE INDEX CONCURRENTLY some_index_name ON queries_query (user_sid, created)

但我想进一步限制索引只关心创建日期在过去 30 天内的那些查询。我尝试过执行以下操作:

CREATE INDEX CONCURRENTLY some_index_name ON queries_query (user_sid, created)
WHERE created >= NOW() - '30 days'::INTERVAL`

但这会引发一个异常,指出我的函数必须是不可变的。

我很想让这个工作,这样我就可以优化我的索引,并减少 Postgres 执行这些重复查询所需的资源。

4

1 回答 1

14

你得到一个异常使用now(),因为该功能不是(显然),并且IMMUTABLE引用手册

索引定义中使用的所有函数和运算符都必须是“不可变的”...

我看到了两种利用(更有效的)部分索引的方法:

1. 以固定日期为条件的部分索引:

CREATE INDEX queries_recent_idx ON queries_query (user_sid, created)
WHERE created > '2013-01-07 00:00'::timestamp;

假设 created实际上定义为timestamp。为列 ( ) 提供timestamp常量是行不通的。转换from to (反之亦然)取决于当前时区设置,并且不是 immutable。使用匹配数据类型的常量。了解带/不带时区的时间戳的基础知识:timestamptztimestamp with time zonetimestamptimestamptz

在流量较低的几个小时删除并重新创建该索引,可能每天或每周使用一个 cron 作业(或任何对您来说足够好的东西)。创建索引非常快,尤其是相对较小的部分索引。此解决方案也不需要向表中添加任何内容。

假设没有对表的并发访问,可以使用如下函数完成自动索引重新创建:

CREATE OR REPLACE FUNCTION f_index_recreate()
  RETURNS void
  LANGUAGE plpgsql AS
$func$
BEGIN
   DROP INDEX IF EXISTS queries_recent_idx;
   EXECUTE format('
      CREATE INDEX queries_recent_idx
      ON queries_query (user_sid, created)
      WHERE created > %L::timestamp'
    , LOCALTIMESTAMP - interval '30 days');  -- timestamp constant
--  , now() - interval '30 days');           -- alternative for timestamptz
END
$func$;

称呼:

SELECT f_index_recreate();

now()(就像你一样) 相当于CURRENT_TIMESTAMPand returns timestamptz。改用或timestamp使用。now()::timestampLOCALTIMESTAMP

db<>fiddle here
sqlfiddle


如果您必须处理对表的并发访问,请使用DROP INDEX CONCURRENTLYand CREATE INDEX CONCURRENTLY。但是您不能将这些命令包装到一个函数中,因为根据文档

CREATE INDEX...可以在事务块内执行常规命令,但CREATE INDEX CONCURRENTLY不能。

因此,有两个单独的交易

CREATE INDEX CONCURRENTLY queries_recent_idx2 ON queries_query (user_sid, created)
WHERE  created > '2013-01-07 00:00'::timestamp;  -- your new condition

然后:

DROP INDEX CONCURRENTLY IF EXISTS queries_recent_idx;

(可选)重命名为旧名称:

ALTER INDEX queries_recent_idx2 RENAME TO queries_recent_idx;

2.“归档”标签条件下的部分索引

archived标签添加到您的表中:

ALTER queries_query ADD COLUMN archived boolean NOT NULL DEFAULT FALSE;

UPDATE每隔您选择“淘汰”旧行并创建索引的列,如:

CREATE INDEX some_index_name ON queries_query (user_sid, created)
WHERE NOT archived;

向您的查询添加匹配条件(即使它看起来是多余的)以允许它使用索引。检查EXPLAIN ANALYZE查询计划器是否赶上 - 它应该能够在较新的日期使用索引进行查询。但它不会理解不完全匹配的更复杂的条件。

您不必删除并重新创建索引,但UPDATE表上的索引可能比重新创建索引更昂贵,并且表会稍大一些。

我会选择第一个选项(索引娱乐)。事实上,我在几个数据库中使用了这个解决方案。第二个会导致更昂贵的更新。

随着时间的推移,这两种解决方案都保留了它们的有用性,随着索引中包含更多过时的行,性能会慢慢下降。

于 2013-02-07T08:13:28.313 回答