关于索引:将日期时间约束添加到PostgreSQL多列部分索引

Add datetime constraint to a PostgreSQL multi-column partial index

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

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

这是我的问题:

我目前通过运行以下方法在这两列上创建了我的多列索引:

1
CREATE INDEX CONCURRENTLY some_index_name ON queries_query (user_sid, created)

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

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

但这引发了一个异常,说明我的函数必须是不可变的。

我很乐意让这个工作,以便我可以优化我的索引,并削减Postgres需要执行这些重复查询的资源。


您尝试使用now()时会遇到异常,因为该函数不是IMMUTABLE(显然),我在这里引用手册:

All functions and operators used in an index definition must be"immutable" ...

我在这里看到了两种利用(效率更高)部分索引的方法:

1.使用常数日期的条件的部分索引:

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

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

  • 在Rails和PostgreSQL中完全忽略时区

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE OR REPLACE FUNCTION f_index_recreate()
  RETURNS void 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$  LANGUAGE plpgsql;

呼叫:

1
SELECT f_index_recreate();

now()(与您一样)相当于CURRENT_TIMESTAMP并返回timestamptz。使用now()::timestamp转换为timestamp或使用LOCALTIMESTAMP

  • 仅选择今天(从午夜开始)时间戳

用Postgres 9.2 - 9.4测试。
SQL小提琴。

如果必须处理并发访问,请使用CREATE INDEX CONCURRENTLY。但是你不能将这个命令包装成一个函数,因为根据文档:

... a regular CREATE INDEX command can be performed within a transaction
block, but CREATE INDEX CONCURRENTLY cannot.

因此,有两个单独的交易:

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

然后:

1
DROP INDEX CONCURRENTLY IF EXISTS queries_recent_idx;

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

1
ALTER INDEX queries_recent_idx2 RENAME TO queries_recent_idx;

2.具有"存档"标签条件的部分索引

在表中添加archived标记:

1
ALTER queries_query ADD COLUMN archived BOOLEAN NOT NULL DEFAULT FALSE;

UPDATE您选择"退休"旧行的时间间隔列,并创建一个索引,如:

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

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

您不必删除并重新创建索引,但表中的UPDATE可能比索引重新创建更昂贵,并且表格略大。

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

随着时间的推移,两种解决方案都保持其有用性,随着索引中包含更多过时的行,性能会逐渐恶化。