web-dev-qa-db-fra.com

Chèque de confinement Tstzrange @> TimeStamptz n'utilise pas l'indice BTREE ou GIST

Schéma:

        Column        |           Type           
----------------------+--------------------------
 id                   | integer                  
 event_id             | integer                  
 started_at           | timestamp with time zone 
 ended_at             | timestamp with time zone 
 created_at           | timestamp with time zone 
    "event_seat_state_lookup_pkey" PRIMARY KEY, btree (id)
    "event_seating_lookup_created_at_idx" btree (created_at)
    "event_seating_lookup_created_at_idx2" Gist (created_at)

Mettre en doute:

SELECT id
FROM event_seating_lookup esl1
WHERE
  tstzrange(now() - interval '1 hour', now() + interval '1 hour', '[)') @> esl1.created_at;

Expliquez Analyser:

Table avec <100K lignes.

 Seq Scan on event_seating_lookup esl1  (cost=0.00..1550.30 rows=148 width=4) (actual time=0.013..19.956 rows=29103 loops=1)
   Filter: (tstzrange((now() - '01:00:00'::interval), (now() + '01:00:00'::interval), '[)'::text) @> created_at)
   Buffers: shared hit=809
 Planning Time: 0.110 ms
 Execution Time: 21.942 ms

Tableau avec 1m + rangées:

Seq Scan on event_seating_lookup esl1  (cost=10000000000.00..10000042152.75 rows=5832 width=4) (actual time=0.009..621.895 rows=1166413 loops=1)
  Filter: (tstzrange((now() - '01:00:00'::interval), (now() + '01:00:00'::interval), '[)'::text) @> created_at)
  Buffers: shared hit=12995
Planning Time: 0.092 ms
Execution Time: 697.927 ms

J'ai essayé:

VACUUM FULL event_seating_lookup;
VACUUM event_seating_lookup;
VACUUM ANALYZE event_seating_lookup;
SET enable_seqscan = OFF;

Problème:

event_seating_lookup_created_at_idx ou alors event_seating_lookup_created_at_idx2 Les index ne sont pas utilisés.

Remarques:

  • PostgreSQL 11.1.
  • btree_Gist L'extension est installée.
  • J'ai essayé une configuration équivalente avec created_at timestamp without time zone et utiliser tsrange; même résultat.
  • Je crois comprendre que la réécriture de la requête avec >=, < Les chèques feraient l'utilisation de l'indice BTREE. La question est de savoir quelle est la raison pour laquelle l'index n'est pas utilisé avec l'opérateur de confinement tstzrange et s'il existe un moyen de le faire fonctionner.
2
Gajus

En ce qui concerne mes recherches, PostgreSQL n'est pas en mesure de réécrire le contrôle de confinement dans une expression pouvant être assortie à l'aide de l'indice BTREE, c'est-à-dire.

esl1.created_at >= now() - interval '1 hour' AND
esl1.created_at < now() + interval '1 hour'

lorsqu'il est écrit de cette manière, la requête est exécutée à l'aide d'index:

Index Scan using event_seating_lookup_created_at_idx on event_seating_lookup esl1  (cost=0.44..12623.56 rows=18084 width=4) (actual time=0.013..57.084 rows=70149 loops=1)
  Index Cond: ((created_at >= (now() - '01:00:00'::interval)) AND (created_at < (now() + '01:00:00'::interval)))
Planning Time: 0.223 ms
Execution Time: 62.209 ms

Comme je préfère la syntaxe de la requête de confinement sur cette dernière forme, j'ai étudié des alternatives possibles. Ce qui est venu, c'est que je puisse écrire une procédure qui fera en ligne la condition pour moi:

CREATE OR REPLACE FUNCTION in_range(timestamptz, tstzrange)
RETURNS boolean AS $$
SELECT
  (
    $1 >= lower($2) AND
    $1 <= upper($2) AND
    (
      upper_inc($2) OR
      $1 < upper($2)
    ) AND
    (
      lower_inc($2) OR
      $1 > lower($2)
    )
  )
$$
language sql;

La raison de la première correspondance sur la condition $1 >= lower($2) AND $1 <= upper($2) _ condition puis vérifiez le upper_inc Et lower_inc Les contraintes sont d'abord bénéficier d'une analyse de la plage, puis de filtrer le résultat.

2
Gajus

La question est de savoir quelle est la raison pour laquelle l'index n'est pas utilisé avec l'opérateur de confinement tstzrange et s'il existe un moyen de le faire fonctionner.

Le raison est assez trivial. Les index b-arbres ne supportent pas l'opérateur de confinement @>. Ni pour types de gamme comme tstzrange ni pour aucun autre type (y compris les types de réseau).

le manuel :

... Une classe opérateur BTREE doit fournir cinq opérateurs de comparaison, <, <=, =, >= et >.

Et les index gist ne prennent pas en charge <, <=,> =, >= et >. Voir ces chapitres du manuel:

Dans les index des postgres sont liés aux opérateurs (qui sont mis en œuvre pour certains types), pas des types de données seules ou des fonctions ou autre chose. En rapport:

L'indice gist event_seating_lookup_created_at_idx2 vous avez est inutile . Il est créé sur la colonne timestamptzcreated_at. Un tel index gist serait utile pour A plage Type (le sens opposé de la logique).

Création d'un index GIST sur une colonne timestamptz est uniquement possible car vous avez installé l'extension de btree_Gist supplémentaire pour permettre de tels index inutiles. (Il existe des applications utiles pour les indices multicolumnistes ou les contraintes d'exclusion ...) En stock Postgres, vous obtiendriez une erreur:

Erreur: Type de données L'horodatage avec le fuseau horaire n'a pas de classe d'opérateur par défaut pour la méthode d'accès "GIST"

Donc, alors qu'il est logiquement valide et techniquement possible d'utiliser un index BTTREE (ou un indice de gist) pour la requête, le boîtier n'est pas implémenté: Aucun support d'index pour timestamptz <@ tstzrange (où timestamptz serait l'expression indexée!). Il peut être résolu avec <, <=, >, >= plus efficacement. Je suppose donc qu'aucun développeur ne ressentait (ou ne ressentira) la nécessité de la mettre en œuvre.

Fonction pour réécrire l'expression avec des opérateurs favorables

Votre implémentation a du sens pour moi. Cela fait ce que vous voulez et due à Fonction Inlinge Utilise un indice de BTTREE uni sur la colonne TIMESTAMP - event_seating_lookup_created_at_idx dans votre exemple. Pour les appels avec une plage constante (comme dans un appel unique), je suggère cette version modifiée:

CREATE OR REPLACE FUNCTION in_range(timestamptz, tstzrange)
  RETURNS boolean AS
$func$
SELECT CASE WHEN lower_inc($2) THEN $1 >= lower($2) ELSE $1 > lower($2) END
   AND CASE WHEN upper_inc($2) THEN $1 <= upper($2) ELSE $1 < upper($2) END
$func$  LANGUAGE sql IMMUTABLE;

Déclarez-le IMMUTABLE, car c'est en fait. Ne pas aider à fonctionner la fonction (peut même l'empêcher si la déclaration est fausse) mais pour d'autres gains. En rapport:

Il peut être inliné et utilise l'index comme votre version. La différence: celle-ci supprime une condition d'index redondante aux limites exclusives. Heureusement, votre considération à cet égard est légèrement hors cible:

La raison de la première correspondance sur the $1 >= lower($2) AND $1 <= upper($2) Condition puis de la vérification des contraintes upper_inc et lower_inc consiste à bénéficier d'une balayage de la plage, puis à filtrer le résultat.

Postgres 11 (au moins) est encore plus intelligent que cela. Je ne vois pas Filter étape pour votre version. Pour les limites [) par défaut (comme dans votre exemple), je reçois ce plan de requête (rupture de ligne dans les conditions ajoutées par moi):

  ->  Index Only Scan using foo_idx on foo (actual rows=5206 loops=1)
        Index Cond: ((datetime >= '2018-09-05 22:00:00+00'::timestamp with time zone)
                 AND (datetime <= '2018-09-05 22:30:00+00'::timestamp with time zone)
                 AND (datetime < '2018-09-05 22:30:00+00'::timestamp with time zone))

Une étape réelle Filter pourrait ajouter un coût plus substantiel pour les cas d'angle avec de nombreux hits sur une liaison exclue. Ceux-ci seraient récupérés de l'indice, puis jetés. Assez pertinent pour les gammes de temps où les valeurs se retrouvent souvent sur des limites - comme des horodatages à l'heure complète.

La différence effective est la suivante: est mineure, mais pourquoi ne pas le prendre?

  ->  Index Only Scan using foo_idx on foo (actual rows=5206 loops=1)
        Index Cond: ((datetime >= '2018-09-05 22:00:00+00'::timestamp with time zone)
                 AND (datetime < '2018-09-05 22:30:00+00'::timestamp with time zone))
2
Erwin Brandstetter