web-dev-qa-db-fra.com

PostgreSQL: créer un index pour distinguer rapidement les valeurs NULL des valeurs non NULL

Considérez une requête SQL avec le prédicat WHERE suivant:

...
WHERE name IS NOT NULL
...

name est un champ textuel dans PostgreSQL.

Aucune autre requête ne vérifie les propriétés textuelles de cette valeur, que ce soit NULL ou non. Par conséquent, n index btree complet semble être un excès, même si il prend en charge cette distinction :

De plus, une condition IS NULL ou IS NOT NULL sur une colonne d'index peut être utilisée avec un index B-tree).

Quel est le bon index PostgreSQL pour distinguer rapidement NULLs des non -NULLs?

26
Adam Matan

J'interprète que vous prétendez que c'est "surpuissant" de deux manières: en termes de complexité (en utilisant un B-Tree au lieu d'une simple liste) et d'espace/performance.

Pour la complexité, ce n'est pas exagéré. Un index B-Tree est préférable car les suppressions de celui-ci seront plus rapides qu'une sorte d'index "non ordonné" (faute d'un meilleur terme). (Un index non ordonné nécessiterait un balayage d'index complet juste pour être supprimé.) À la lumière de ce fait, tous les gains d'un index non ordonné seraient généralement contrebalancés par les inconvénients, de sorte que l'effort de développement n'est pas justifié.

Pour l'espace et les performances, cependant, si vous voulez un index hautement sélectif pour l'efficacité, vous pouvez inclure une clause WHERE sur un index, comme indiqué dans le manuel fin :

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

Notez que vous ne verrez les avantages de cet index que s'il peut permettre à PostgreSQL d'ignorer une grande quantité de lignes lors de l'exécution de votre requête. Par exemple, si 99% des lignes ont name IS NOT NULL, l'index ne vous achète rien en laissant simplement une analyse complète de la table se produire; en fait, il serait moins efficace (comme @ CraigRinger notes) car il nécessiterait des lectures de disque supplémentaires. Si toutefois, seulement 1% des lignes ont name IS NOT NULL, cela représente une énorme économie car PostgreSQL peut ignorer la plupart des tables de votre requête. Si votre table est très grande, même l'élimination de 50% des lignes en vaut la peine. Il s'agit d'un problème de réglage, et la valeur de l'indice dépendra fortement de la taille et de la distribution des données.

De plus, il y a très peu de gain en termes d'espace si vous avez encore besoin d'un autre index pour le name IS NULL Lignes. Voir réponse de Craig Ringer pour plus de détails.

24
jpmc26

Vous pouvez utiliser un index d'expression, mais vous ne devriez pas. Restez simple et utilisez un arbre b simple.


Un index d'expression peut être créé sur 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)

... mais Pg ne se rend pas compte qu'il est également utilisable pour 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)

et transforme même NOT (name IS NOT NULL) en name IS NULL, ce qui est généralement ce que vous voulez.

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)

vous êtes donc en fait mieux avec deux index d'expressions disjointes, un sur l'ensemble nul et l'autre sur l'ensemble non nul.

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)

C'est assez horrible cependant. Pour les utilisations les plus sensées, j'utiliserais simplement un index b-tree simple. L'amélioration de la taille de l'index n'est pas trop excitante, du moins pour les entrées de petite taille, comme le mannequin que j'ai créé avec un tas de valeurs 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

Vous pouvez utiliser une expression comme (title IS NULL) comme colonne indexée. Cela fonctionne donc comme prévu:

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

Cela a le gros avantage par rapport à l'utilisation d'un prédicat que dans ce cas, la valeur stockée dans l'index est uniquement un booléen oui/non et non la valeur de la colonne complète. Donc, surtout si votre colonne vérifiée NULL a tendance à contenir de grandes valeurs (comme un champ de texte de titre ici), cette méthode d'indexation est beaucoup plus efficace en termes d'espace que l'utilisation d'un index prédicat.

3
fxtentacle