PostgreSQL:创建索引以快速区分 NULL 和非 NULL 值

Posted

技术标签:

【中文标题】PostgreSQL:创建索引以快速区分 NULL 和非 NULL 值【英文标题】:PostgreSQL: Create an index to quickly distinguish NULL from non-NULL values 【发布时间】:2015-08-12 13:08:42 【问题描述】:

考虑使用以下 WHERE 谓词的 SQL 查询:

...
WHERE name IS NOT NULL
...

name 是 PostgreSQL 中的文本字段。

没有其他查询检查此值的任何文本属性,只是检查它是否为NULL。因此,a full btree index 似乎有点矫枉过正,尽管it supports this distinction:

此外,索引列上的 IS NULL 或 IS NOT NULL 条件可以与 B 树索引一起使用。

什么是正确的 PostgreSQL 索引来快速区分NULLs 和非NULLs?

【问题讨论】:

您可以将谓词添加到create index 以至少最小化其大小。 除了 btree、gist、gin 和 hash 之外,您别无选择,这是不鼓励的,我认为没有其他选择。 您可以create index i on t (coalesce('NULL',col)); 实际索引NULL 并避免将一个空值与其他空值分开 @VaoTsun o_O? NULL 已编入索引。你从哪里知道它不是? @CraigRinger 是的,它已被索引,但所有空值都不同,我相信亚当希望看到空值相同。亚当?.. 【参考方案1】:

我在解释你声称它在两个方面“矫枉过正”:在复杂性(使用 B-Tree 而不仅仅是一个列表)和空间/性能方面。

对于复杂性,这并不过分。 B-Tree 索引更可取,因为从中 deletes 会比某种“无序”索引更快(因为没有更好的术语)。 (无序索引需要完整的索引扫描才能删除。)鉴于这一事实,无序索引的任何收益通常都会被损害所抵消,因此开发工作是不合理的。

但是,对于空间和性能,如果您想要一个高度选择性的索引以提高效率,您可以在索引上包含一个WHERE 子句,如fine manual 中所述:

CREATE INDEX ON my_table (name) WHERE name IS NOT NULL;

请注意,只有当它允许 PostgreSQL 在执行查询时忽略 大量 行时,您才能从该索引中看到好处。例如,如果 99% 的行有 name IS NOT NULL,那么索引不会给你带来任何好处,而不仅仅是让全表扫描发生;事实上,它的效率会更低(如@CraigRinger 所述),因为它需要额外的磁盘读取。然而,如果只有 1% 的行有 name IS NOT NULL,那么这代表着巨大的节省,因为 PostgreSQL 可以忽略大部分表来进行查询。如果您的表非常大,那么即使消除 50% 的行也可能是值得的。这是一个调优问题,索引是否有价值在很大程度上取决于数据的大小和分布。

此外,如果您仍需要为 name IS NULL 行创建另一个索引,则空间方面的收益非常小。详情请见Craig Ringer's answer。

【讨论】:

很好解释。事实上,将索引用于匹配 99% 的行会非常低效,比 seqscan 慢得多。【参考方案2】:

您可以使用表达式索引,但不应该。保持简单,并使用普通的 b-tree。


可以在colname IS NOT NULL上创建表达式索引:

test=> CREATE TABLE blah(name text);
CREATE TABLE
test=> CREATE INDEX name_notnull ON blah((name IS NOT NULL));
CREATE INDEX
test=> INSERT INTO blah(name) VALUES ('a'),('b'),(NULL);
INSERT 0 3
test=> SET enable_seqscan = off;
SET
craig=> SELECT * FROM blah WHERE name IS NOT NULL;
 name 
------
 a
 b
(2 rows)

test=> EXPLAIN SELECT * FROM blah WHERE name IS NOT NULL;
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Bitmap Heap Scan on blah  (cost=9.39..25.94 rows=1303 width=32)
   Filter: (name IS NOT NULL)
   ->  Bitmap Index Scan on name_notnull  (cost=0.00..9.06 rows=655 width=0)
         Index Cond: ((name IS NOT NULL) = true)
(4 rows)

test=> SET enable_bitmapscan = off;
SET
test=> EXPLAIN SELECT * FROM blah WHERE name IS NOT NULL;
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Index Scan using name_notnull on blah  (cost=0.15..55.62 rows=1303 width=32)
   Index Cond: ((name IS NOT NULL) = true)
   Filter: (name IS NOT NULL)
(3 rows)

...但 Pg 没有意识到它也可用于IS NULL

test=> EXPLAIN SELECT * FROM blah WHERE name IS NULL;
                               QUERY PLAN                                
-------------------------------------------------------------------------
 Seq Scan on blah  (cost=10000000000.00..10000000023.10 rows=7 width=32)
   Filter: (name IS NULL)
(2 rows)

甚至将NOT (name IS NOT NULL) 转换为name IS NULL,这通常是您想要的。

test=> EXPLAIN SELECT * FROM blah WHERE NOT (name IS NOT NULL);
                               QUERY PLAN                                
-------------------------------------------------------------------------
 Seq Scan on blah  (cost=10000000000.00..10000000023.10 rows=7 width=32)
   Filter: (name IS NULL)
(2 rows)

所以实际上你最好使用两个不相交的表达式索引,一个在空集上,一个在非空集上。

test=> DROP INDEX name_notnull ;
DROP INDEX
test=> CREATE INDEX name_notnull ON blah((name IS NOT NULL)) WHERE (name IS NOT NULL);
CREATE INDEX
test=> EXPLAIN SELECT * FROM blah WHERE name IS NOT NULL;
                                QUERY PLAN                                
--------------------------------------------------------------------------
 Index Scan using name_notnull on blah  (cost=0.13..8.14 rows=3 width=32)
   Index Cond: ((name IS NOT NULL) = true)
(2 rows)

test=> CREATE INDEX name_null ON blah((name IS NULL)) WHERE (name IS NULL);
CREATE INDEX
craig=> EXPLAIN SELECT * FROM blah WHERE name IS NULL;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Index Scan using name_null on blah  (cost=0.12..8.14 rows=1 width=32)
   Index Cond: ((name IS NULL) = true)
(2 rows)

虽然这很可怕。对于大多数明智的用途,我只使用普通的 b-tree 索引。索引大小的改进并不太令人兴奋,至少对于小的输入,比如我用一堆 md5 值创建的虚拟对象:

test=> SELECT pg_size_pretty(pg_relation_size('blah'));
 pg_size_pretty 
----------------
 9416 kB
(1 row)

test=> SELECT pg_size_pretty(pg_relation_size('blah_name'));
 pg_size_pretty 
----------------
 7984 kB
(1 row)

test=> SELECT pg_size_pretty(pg_relation_size('name_notnull'));
 pg_size_pretty 
----------------
 2208 kB
(1 row)

test=> SELECT pg_size_pretty(pg_relation_size('name_null'));
 pg_size_pretty 
----------------
 2208 kB
(1 row)

【讨论】:

【参考方案3】:

您可以使用 (title IS NULL) 之类的表达式作为索引列。所以这可以按预期工作:

CREATE INDEX index_articles_on_title_null ON articles ( (title IS NULL) );
SELECT * FROM articles WHERE (title IS NULL)='t';

这比使用谓词有很大的优势,在这种情况下,存储在索引中的值只是一个是/否布尔值,而不是完整的列值。因此,特别是如果您的 NULL 检查列往往包含较大的值(如此处的标题文本字段),那么这种索引方式比使用谓词索引更节省空间。

【讨论】:

您不需要= 't'。使用WHERE title IS NULL 就可以了

以上是关于PostgreSQL:创建索引以快速区分 NULL 和非 NULL 值的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL常用索引

postgresql----唯一索引,表达式索引,部分索引

如何快速掌握 Navicat for PostgreSQL表索引

postgresql----Btree索引

在 PostgreSQL 中优化窗口函数以使用索引

Postgresql 9.4:索引在模式搜索中不起作用