PostgreSQL:创建索引以快速区分 NULL 和非 NULL 值
PostgreSQL: Create an index to quickly distinguish NULL from non-NULL values
考虑使用以下 WHERE
谓词的 SQL 查询:
...
WHERE name IS NOT NULL
...
其中 name
是 Postgre 中的文本字段SQL。
没有其他查询检查此值的任何文本 属性,只是检查它是否为 NULL
。因此,a full btree index seems like an overkill, even though it supports this distinction:
Also, an IS NULL or IS NOT NULL condition on an index column can be used with a B-tree index.
什么是正确的 PostgreSQL 索引来快速区分 NULL
s 和非 NULL
s?
我从两个方面解释你声称它是 "overkill":在复杂性方面(使用 B 树而不只是列表)和 space/performance。
对于复杂性,这并不过分。 B-Tree 索引更可取,因为从中删除 比某种 "unordered" 索引更快(因为缺少更好的术语)。 (无序索引需要完整的索引扫描才能删除。)鉴于这一事实,无序索引的任何收益通常都会被损害所抵消,因此开发工作是不合理的。
为了 space 和性能,但是,如果您想要一个高度选择性的索引以提高效率,您可以在索引中包含一个 WHERE
子句,如 fine manual 中所述:
CREATE INDEX ON my_table (name) WHERE name IS NOT NULL;
请注意,只有当它允许 PostgreSQL 在执行查询时忽略 大量 行时,您才会从该索引中受益。例如,如果 99% 的行具有 name IS NOT NULL
,索引不会给您带来任何好处,而只是让完整的 table 扫描发生;事实上,它会降低效率(如 @CraigRinger 注释),因为它需要额外的磁盘读取。但是,如果只有 1% 的行具有 name IS NOT NULL
,那么这代表着巨大的节省,因为 PostgreSQL 可以忽略查询的大部分 table。如果您的 table 非常大,即使删除 50% 的行也可能是值得的。这是一个调整问题,索引是否有价值将在很大程度上取决于数据的大小和分布。
此外,如果您仍然需要为 name IS NULL
行创建另一个索引,那么 space 的收益将非常小。有关详细信息,请参阅 。
您可以使用表达式索引,但您不应该这样做。保持简单,使用普通的 b 树。
可以在 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 树索引。索引大小的改进并不太令人兴奋,至少对于小的输入来说是这样,比如我用一堆 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)
您可以使用类似 (title IS NULL) 的表达式作为索引列。所以这按预期工作:
CREATE INDEX index_articles_on_title_null ON articles ( (title IS NULL) );
SELECT * FROM articles WHERE (title IS NULL)='t';
这比使用谓词有很大的优势,在这种情况下,索引中存储的值只是一个 yes/no 布尔值,而不是完整的列值。因此,特别是如果您的 NULL-checked 列往往包含较大的值(例如此处的标题文本字段),那么这种索引方式 space-efficient 比使用谓词索引要多得多。
考虑使用以下 WHERE
谓词的 SQL 查询:
...
WHERE name IS NOT NULL
...
其中 name
是 Postgre 中的文本字段SQL。
没有其他查询检查此值的任何文本 属性,只是检查它是否为 NULL
。因此,a full btree index seems like an overkill, even though it supports this distinction:
Also, an IS NULL or IS NOT NULL condition on an index column can be used with a B-tree index.
什么是正确的 PostgreSQL 索引来快速区分 NULL
s 和非 NULL
s?
我从两个方面解释你声称它是 "overkill":在复杂性方面(使用 B 树而不只是列表)和 space/performance。
对于复杂性,这并不过分。 B-Tree 索引更可取,因为从中删除 比某种 "unordered" 索引更快(因为缺少更好的术语)。 (无序索引需要完整的索引扫描才能删除。)鉴于这一事实,无序索引的任何收益通常都会被损害所抵消,因此开发工作是不合理的。
为了 space 和性能,但是,如果您想要一个高度选择性的索引以提高效率,您可以在索引中包含一个 WHERE
子句,如 fine manual 中所述:
CREATE INDEX ON my_table (name) WHERE name IS NOT NULL;
请注意,只有当它允许 PostgreSQL 在执行查询时忽略 大量 行时,您才会从该索引中受益。例如,如果 99% 的行具有 name IS NOT NULL
,索引不会给您带来任何好处,而只是让完整的 table 扫描发生;事实上,它会降低效率(如 @CraigRinger 注释),因为它需要额外的磁盘读取。但是,如果只有 1% 的行具有 name IS NOT NULL
,那么这代表着巨大的节省,因为 PostgreSQL 可以忽略查询的大部分 table。如果您的 table 非常大,即使删除 50% 的行也可能是值得的。这是一个调整问题,索引是否有价值将在很大程度上取决于数据的大小和分布。
此外,如果您仍然需要为 name IS NULL
行创建另一个索引,那么 space 的收益将非常小。有关详细信息,请参阅
您可以使用表达式索引,但您不应该这样做。保持简单,使用普通的 b 树。
可以在 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 树索引。索引大小的改进并不太令人兴奋,至少对于小的输入来说是这样,比如我用一堆 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)
您可以使用类似 (title IS NULL) 的表达式作为索引列。所以这按预期工作:
CREATE INDEX index_articles_on_title_null ON articles ( (title IS NULL) );
SELECT * FROM articles WHERE (title IS NULL)='t';
这比使用谓词有很大的优势,在这种情况下,索引中存储的值只是一个 yes/no 布尔值,而不是完整的列值。因此,特别是如果您的 NULL-checked 列往往包含较大的值(例如此处的标题文本字段),那么这种索引方式 space-efficient 比使用谓词索引要多得多。