web-dev-qa-db-fra.com

Performances PostgreSQL avec (col = valeur ou col est null)

Cette question concerne la performance de la requête PostgreSQL 9.5.

La table est:

CREATE TABLE big_table
(
   id integer NOT NULL,
   flag bigint NOT NULL,
   time timestamp with timezone NOT NULL,
   val int,
   primary key (id)
)

Considérez la requête:

SELECT * 
FROM big_table 
WHERE time > '0666-06-06 00:00:00+00' AND 
      flag & 2 = 2 AND 
      (val = 5 or val IS NULL) 
ORDER by time desc

L'objectif est de rendre cette performance de requête aussi bonne que possible tout en utilisant un syndicat. J'ai atteint la meilleure performance en utilisant l'index:

CREATE INDEX ind 
ON big_table USING btree (time DESC, val)
WHERE (flag & 2::smallint = 2::smallint);

ce qui rend la requête utilise l'index deux fois et utilisez bitmap ou entre eux. C'est encore beaucoup pire que si je pouvais faire val = ANY('{NULL,5}') qui n'est pas possible.

Cela me fait envisager de mettre une valeur spéciale telle que -1 au lieu de NULL qui me permettra de faire val = ANY('{-1,5}') pour exécuter une analyse complète d'index avec de meilleures performances.

Ainsi, utilise une valeur spéciale au lieu de NULL mieux dans de telles situations? Ou PostgreSQL a-t-il une certaine optimisation pour la situation où vous voulez faire un "ou" entre une valeur non nulle et une null?

J'ai lu cet excellent article mais je n'ai pas vu une solution pour ce que je viens de demander sans utiliser de syndicat: https://www.ycybertec-postgresql.com/fr/avoid-or-for-better-performance /

J'ai également cherché une telle question sur le débordement de pile et ici, mais les questions connexes ne parlent pas de la performance ou ne parlent pas de non-null avec NULL. Cette question similaire (et la réponse qu'il a reçue) a parlé de SQL Server et non PostgreSQL: meilleure façon d'écrire une requête SQL qui vérifie une colonne de valeur non nulle ou null

J'ai également envisagé de faire de la regroupement pour se débarrasser de NULL, mais j'ai lu que cela rend la performance bien pire.

La table compte plus de 4 millions d'enregistrements. D'eux seulement 56 enregistrements ont NULL dans la colonne VAL.

Mise à jour: Explique originale:

"Limit  (cost=1112.32..1112.32 rows=2 width=519)"
"  ->  Sort  (cost=1112.32..1112.32 rows=2 width=519)"
"        Sort Key: time DESC"
"        ->  Bitmap Heap Scan on big_table  (cost=1104.29..1112.31 rows=2 width=519)"
"              Recheck Cond: (((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val = 3) AND ((flag & '2'::smallint) = '2'::smallint)) OR ((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val IS NULL) AND ((flag & '2'::smallint) = '2'::smallint)))"
"              ->  BitmapOr  (cost=1104.29..1104.29 rows=2 width=0)"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..552.14 rows=1 width=0)"
"                          Index Cond: ((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val = 3))"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..552.14 rows=1 width=0)"
"                          Index Cond: ((time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND (val IS NULL))"

Expliquez après avoir changé (val = 3 or val IS NULL) Sur coalesce(val,-1) = ANY('{3,-1}') et créez l'index:

CREATE INDEX ind2 ON big_table USING btree (coalesce(val,-1), time DESC) WHERE (flag & 2::smallint = 2::smallint);

est:

"Limit  (cost=863.45..863.50 rows=20 width=519)"
"  ->  Sort  (cost=863.45..863.99 rows=216 width=519)"
"        Sort Key: time DESC"
"        ->  Bitmap Heap Scan on big_table  (cost=11.08..857.71 rows=216 width=519)"
"              Recheck Cond: ((COALESCE(val, '-1'::integer) = ANY ('{3,-1}'::integer[])) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND ((flag & '2'::smallint) = '2'::smallint))"
"              ->  Bitmap Index Scan on ind2  (cost=0.00..11.03 rows=216 width=0)"
"                    Index Cond: ((COALESCE(val, '-1'::integer) = ANY ('{3,-1}'::integer[])) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone))"

Exécution d'une requête originale, mais la modification de l'ordre des temps et des colonnes VAL dans l'index d'origine donne le plan d'explication beaucoup mieux:

"Limit  (cost=16.92..16.92 rows=2 width=519)"
"  ->  Sort  (cost=16.92..16.92 rows=2 width=519)"
"        Sort Key: time DESC"
"        ->  Bitmap Heap Scan on big_table  (cost=8.89..16.91 rows=2 width=519)"
"              Recheck Cond: (((val = 3) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND ((flag & '2'::smallint) = '2'::smallint)) OR ((val IS NULL) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone) AND ((flag & '2'::smallint) = '2'::smallint)))"
"              ->  BitmapOr  (cost=8.89..8.89 rows=2 width=0)"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..4.44 rows=1 width=0)"
"                          Index Cond: ((val = 3) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone))"
"                    ->  Bitmap Index Scan on ind  (cost=0.00..4.44 rows=1 width=0)"
"                          Index Cond: ((val IS NULL) AND (time > '0666-06-06 00:00:00+00'::timestamp with time zone))"

Et en fait, la requête de la coalesse ralentit réellement la requête (même avec des colonnes inversées mais remplacées par des regroupements dans l'index)!

2
Guy L.

Remplacement NULL avec une valeur de substitution peut ou non aider un peu. NULL La manutention est un peu plus chère dans les index mais, d'autre part, il est généralement plus petit en stockage, ce qui a également une incidence sur la performance.

J'attends un impact beaucoup plus important de ceci: retournez l'ordre des expressions d'index :

CREATE INDEX ON big_table (val, time DESC)
WHERE flag & 2::smallint = 2::smallint;

Règle de la règle: index pour l'égalité d'abord - alors pour les gammes. Voir:

À votre consolation: val = ANY('{-1,5}') Brûle pour être Syntaxe sténographique pour (val = -1 OR val = 5), Ce qui n'est guère meilleur que (val IS NULL OR val = 5). (Le facteur plus important est le nombre de lignes pour NULL vs. vs -1 - le même dans votre cas si vous ne remplacez que NULL avec -1.).

Envisagez également de la mise à niveau vers une version actuelle de Postgres. 9.5 vieillit, il y a eu diverses améliorations de performance pour les grandes tables.

Pour renvoyer seulement quelques colonnes, un Scan d'index uniquement serait une optimisation possible, mais vous devez renvoyer la plupart de vos 21 colonnes en fonction des commentaires.

Pour économiser 8 octets par ligne perdamment perdus au rembourrage d'alignement, réorganisez les colonnes de votre tableau de démonstration comme celui-ci:

CREATE TABLE big_table (
   flag bigint NOT NULL,
   time timestamp with timezone NOT NULL,
   id integer PRIMARY KEY,
   val int
);

Plus petit est plus rapide dans l'ensemble. Maintenant, c'est évidemment juste une table de démonstration, mais les mêmes principes s'appliquent à votre table réelle. Voir:

Sorte

Pour un seul val, Postgres peut renvoyer directement des données pré-triées à partir de l'index. Mais pour plus d'une valeur, il doit fusionner également de nombreux sous-ensembles triés (un ensemble de tri pour val IS NULL, Un autre pour val = 5 Dans votre exemple), donc une autre étape de tri en haut de l'index L'accès est inévitable. Les ensembles pré-triés de l'index peuvent toujours le rendre moins chers - et vous avez besoin de tuples d'index triés dans tous les cas. Le plan de requête réelle dépend également de la méthode d'accès à l'indice choisi. Il est trivial de renvoyer des données pré-triées à partir d'un Numérisation d'index (ou Numérisation de l'index uniquement). Pas tellement pour A Scan d'index bitmap.

Cas spécial: très peu de NULL valeurs utilisées tout le temps

Puisque vous n'avez mentionné qu'une poignée de valeurs nulles pour des millions de lignes, j'ajouterais un indice sur mesure pour ce cas particulier:

CREATE INDEX ON big_table (time DESC)
WHERE flag & 2::smallint = 2::smallint
AND   val IS NULL;

Peut-être même ajouter toutes les autres colonnes d'intérêt pour cet index spécial très minuscule pour obtenir un scan d'index unique. (Dépend des conditions préalables.) Encore plus dans Postgres 11 ou plus tard avec un véritable index de couverture utilisant la clause INCLUDE. Les résultats de cet indice et l'autre index sont fusionnés dans un nœud BitmapOr, comme vous le voyez pour plusieurs sous-ensemble du même index maintenant. Postgres a des estimations précises pour le cas spécial et cela devient complètement non pertinent si le cas particulier est null ou -1 ou autre. (Pas que cela comptait beaucoup de choses à commencer.) Voir:

3
Erwin Brandstetter