3

我有一个大型 Postgres 表,我想在索引的 2 列中的 1 列上部分索引。我可以以及如何在部分索引的 where 子句中使用 Postgres 函数,然后让选择查询利用该部分索引?

示例场景

第一栏是“杂志”,第二栏是“卷”,第三栏是“期”。所有杂志都可以有相同的“卷”和“期”#,但我希望索引只包含该杂志最近的两卷。这是因为一本杂志可能比其他杂志更老,并且比年轻杂志的销量更高。

创建了两个不可变的严格函数来确定杂志 f_current_volume('gq') 和 f_previous_volume('gq') 的当前和去年的卷。注意:当前/过去的数量 # 每年仅更改一次。

我尝试使用这些函数创建部分索引,但是在查询中使用解释时,它只会对当前卷杂志进行 seq 扫描。


CREATE INDEX ix_issue_magazine_volume ON issue USING BTREE ( magazine, volume ) 
  WHERE volume IN (f_current_volume(magazine), f_previous_volume(magazine));

-- Both these do seq scans.
select * from issue where magazine = 'gq' and volume = 100;
select * from issue where magazine = 'gq' and volume = f_current_volume('gq');

我做错了什么来完成这项工作?如果可能的话,为什么 Postgres 需要这样做才能使用索引?


-- UPDATE: 2013-06-17, the following surprisingly used the index.
-- Why would using a field name rather than value allow the index to be used?
select * from issue where magazine = 'gq' and volume = f_current_volume(magazine);

4

2 回答 2

5

不变性和“当前”

如果你的f_current_volume函数改变了它的行为——正如它的名字所暗示的那样,并且存在一个f_previous_volume函数,那么数据库可以自由地返回完全虚假的结果

PostgreSQL 会拒绝让你创建索引,抱怨你只能使用IMMUTABLE函数。问题是,根据文档,标记一个函数IMMUTABLE意味着正在告诉PostgreSQL一些关于函数行为的信息。你是说“我保证这个函数的结果不会改变,在此基础上随意做出假设。”

最大的假设之一是在构建索引时。如果函数在多次调用时为不同的输入返回不同的输出,事情就会发生splat。或者如果你不走运,可能会繁荣。从理论上讲,您可以通过更改REINDEX所有内容来更改不可变函数,但唯一真正安全的方法是DROP使用它的每个索引,DROP函数,使用新定义重新创建函数并重新创建索引.

如果您有一些不经常更改的内容,那么这实际上非常有用,但是您确实在不同的时间点有两个不同的不可变函数恰好具有相同的名称。

部分索引匹配

PostgreSQL 的部分索引匹配非常愚蠢——但是,正如我在为此编写测试用例时发现的那样,它比以前聪明得多。它忽略了一个 dummy OR true。它使用索引WHERE (a%100=0 OR a%1000=0)进行WHERE a = 100查询。它甚至使用了不可内联的身份函数:

regress=> CREATE TABLE partial AS SELECT x AS a, x 
          AS b FROM generate_series(1,10000) x;
regress=> CREATE OR REPLACE FUNCTION identity(integer) 
          RETURNS integer AS $$
          SELECT $1; 
          $$ LANGUAGE sql IMMUTABLE STRICT;
regress=> CREATE INDEX partial_b_fn_idx 
          ON partial(b) WHERE (identity(b) % 1000 = 0);
regress=> EXPLAIN SELECT b FROM partial WHERE b % 1000 = 0;
                                      QUERY PLAN                                       
---------------------------------------------------------------------------------------
 Index Only Scan using partial_b_fn_idx on partial  (cost=0.00..13.05 rows=50 width=4)
(1 row)

但是,它无法证明IN子句匹配,例如:

regress=> DROP INDEX partial_b_fn_idx;
regress=> CREATE INDEX partial_b_fn_in_idx ON partial(b)
          WHERE (b IN (identity(b), 1));
regress=> EXPLAIN SELECT b FROM partial WHERE b % 1000 = 0;
                               QUERY PLAN                                 
----------------------------------------------------------------------------
 Seq Scan on partial  (cost=10000000000.00..10000000195.00 rows=50 width=4)

所以我的建议?重写INOR列表:

CREATE INDEX ix_issue_magazine_volume ON issue USING BTREE ( magazine, volume ) 
  WHERE (volume = f_current_volume(magazine) OR volume = f_previous_volume(magazine));

...并且在当前版本上它可能只是工作,只要您牢记上面概述的不变性规则。嗯,第二个版本:

select * from issue where magazine = 'gq' and volume = f_current_volume('gq');

可能。更新:不,不会;要使用它,Pg 必须认识到这一点magazine='gq'并意识到它f_current_volume('gq')因此等价于f_current_volume(magazine). 它不会尝试通过部分索引匹配来证明该级别的等价性,因此正如您在更新中指出的那样,您必须f_current_volume(magazine)直接编写。我应该发现的。理论上,如果计划器足够聪明,PostgreSQL 可以将索引与第二个查询一起使用,但我不确定您将如何有效地寻找像这样的替换值得的地方。

第一个例子,volume = 100永远不会使用索引,因为在查询计划时 PostgreSQL 不知道它f_current_volumne('gg');会评估为100. OR volume = 100不过,您可以在部分索引子句中添加 OR 子句,WHERE然后 PostgreSQL 会弄清楚。

于 2013-06-15T08:41:45.167 回答
1

首先,我想自愿猜测一下,因为你让它听起来像是你的f_current_volume()函数基于一个单独的表计算了一些东西。

如果是这样,请小心,因为这意味着您的函数volatile,因为它需要在每次调用时重新计算(并发事务可能正在插入、更新或删除行)。Postgres 不允许对它们进行索引,我假设您通过声明 function 来解决这个问题immutable。这不仅不正确,而且您还会遇到索引包含垃圾的问题,因为该函数在您编辑行时被评估,而不是在运行时。相反,如果我的猜测是正确的,您可能想要的是使用触发器在表本身中存储和维护总数。

关于您的具体问题,部分索引需要在查询中满足其 where 条件以提示 Postgres 使用它们。我很确定 Postgres 足够聪明,可以识别例如10isbetween 5 and 15并在该子句中使用部分索引。f_current_volume('gq')但是,考虑到上述警告,我非常怀疑它会知道在您的情况下是 100。

你可以试试这个查询,看看索引是否被使用:

select *
  from issue
 where magazine = 'gq'
   and volume in (f_current_volume('gq'), f_previous_volume('gq'));

(同样,如果你的函数实际上是 volatile 的,你也会得到一个 seq 扫描。)

于 2013-06-14T22:38:20.127 回答