web-dev-qa-db-ja.com

PostgreSQL:NULLを非NULL値からすばやく区別するためのインデックスを作成します

次のWHERE述部を持つSQLクエリを検討してください。

...
WHERE name IS NOT NULL
...

ここで、nameはPostgreSQLのテキストフィールドです。

NULLかどうかだけで、この値のテキストプロパティをチェックするクエリは他にありません。したがって、- 完全なbtreeインデックス は、 この区別をサポートします にもかかわらず、やり過ぎのようです。

また、インデックス列でIS NULLまたはIS NOT NULL条件をBツリーインデックスで使用できます。

NULLsと非NULLsをすばやく区別するための正しいPostgreSQLインデックスは何ですか?

26
Adam Matan

私はあなたがそれが2つの方法で「過剰」であるとあなたが主張していると解釈しています:複雑さ(単なるリストの代わりにBツリーを使用する)とスペース/パフォーマンスの点で。

複雑さについては、やり過ぎではありません。 Bツリーインデックスは、deletesが何らかの種類の「順序付けられていない」インデックス(より良い用語がないため)よりも速いため、好ましいです。 (順序付けされていないインデックスは、削除するために完全なインデックススキャンが必要になります。)その事実を考慮すると、通常、順序付けされていないインデックスから得られる利益は不利益を上回るため、開発作業は正当化されません。

ただし、スペースとパフォーマンスのために、効率のために高度に選択的なインデックスが必要な場合は、 fine manual で説明されているように、インデックスにWHERE句を含めることができます。

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

PostgreSQLがクエリを実行するときにlargeの行数を無視できる場合にのみ、このインデックスの利点が表示されることに注意してください。たとえば、行の99%にname IS NOT NULLが含まれている場合、インデックスはテーブル全体のスキャンを実行するだけでは何も買いません。実際、追加のディスク読み取りが必要になるため、( @ CraigRinger notesのように)効率が低下します。ただし、name IS NOT NULLを持つ行が1%のみの場合、PostgreSQLはクエリのテーブルのほとんどを無視できるため、これは大幅な節約になります。テーブルが非常に大きい場合は、行の50%を削除しても価値があります。これはチューニングの問題であり、インデックスが価値があるかどうかは、データのサイズと分布に大きく依存します。

さらに、name IS NULL行に別のインデックスがまだ必要な場合、スペースの面ではほとんど利益がありません。詳細については、 Craig Ringer's answer を参照してください。

24
jpmc26

式インデックスを使用できますが、使用しないでください。シンプルに保ち、単純な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)
_

そのため、実際には、2つの独立した式インデックス(nullに1つ、null以外のセットに1つ)を使用する方が適切です。

_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)
_
14
Craig Ringer

インデックス列として(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チェック列に大きな値が含まれる場合(ここのタイトルテキストフィールドなど)、このインデックス方法は、述語インデックスを使用するよりもはるかにスペース効率が高くなります。

3
fxtentacle