PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Quel est l'index approprié pour interroger les structures dans les tableaux dans Postgres jsonb ?

Tout d'abord, vous ne pouvez pas accéder aux valeurs de tableau JSON comme ça. Pour une valeur json donnée

[{"event_slug":"test_1","start_time":"2014-10-08","end_time":"2014-10-12"},
 {"event_slug":"test_2","start_time":"2013-06-24","end_time":"2013-07-02"},
 {"event_slug":"test_3","start_time":"2014-03-26","end_time":"2014-03-30"}]

Un test valide par rapport au premier élément du tableau serait :

WHERE e->0->>'event_slug' = 'test_1'

Mais vous ne voulez probablement pas limiter votre recherche au premier élément du tableau. Avec le jsonb type de données dans Postgres 9.4, vous disposez d'opérateurs supplémentaires et d'un support d'index. Pour indexer les éléments d'un tableau, vous avez besoin d'un index GIN.

Les classes d'opérateurs intégrées pour les index GIN ne prennent pas en charge les opérateurs "supérieur à" ou "inférieur à" > >= < <= . Ceci est vrai pour jsonb ainsi, où vous pouvez choisir entre deux classes d'opérateurs. Par documentation :

Name             Indexed Data Type  Indexable Operators
...
jsonb_ops        jsonb              ? ?& ?| @>
jsonb_path_ops   jsonb              @>
   

(jsonb_ops étant la valeur par défaut.) Vous pouvez couvrir le test d'égalité, mais aucun de ces opérateurs ne couvre votre exigence pour >= comparaison. Vous auriez besoin d'un index btree.

Solution de base

Pour prendre en charge la vérification d'égalité avec un index :

CREATE INDEX locations_events_gin_idx ON locations
USING gin (events jsonb_path_ops);

SELECT * FROM locations WHERE events @> '[{"event_slug":"test_1"}]';

Cela peut suffire si le filtre est suffisamment sélectif.
En supposant end_time >= start_time , nous n'avons donc pas besoin de deux chèques. Vérification uniquement end_time est moins cher et équivalent :

SELECT l.*
FROM   locations l
     , jsonb_array_elements(l.events) e
WHERE  l.events @> '[{"event_slug":"test_1"}]'
AND   (e->>'end_time')::timestamp >= '2014-10-30 14:04:06 -0400'::timestamptz;

Utilisation d'un JOIN LATERAL implicite . Détails (dernier chapitre) :

  • PostgreSQL unnest() avec le numéro d'élément

Attention aux différents types de données ! Ce que vous avez dans la valeur JSON ressemble à timestamp [without time zone] , tandis que vos prédicats utilisent timestamp with time zone littéraux. L'timestamp la valeur est interprétée en fonction du fuseau horaire actuel paramètre, tandis que le timestamptz donné les littéraux doivent être convertis en timestamptz explicitement ou le fuseau horaire serait ignoré ! La requête ci-dessus devrait fonctionner comme vous le souhaitez. Explication détaillée :

  • Ignorer complètement les fuseaux horaires dans Rails et PostgreSQL

Plus d'explications pour jsonb_array_elements() :

  • Joindre PostgreSQL en utilisant JSONB

Solution avancée

Si ce qui précède n'est pas assez bon, j'envisagerais une MATERIALIZED VIEW qui stocke les attributs pertinents sous forme normalisée. Cela autorise les index btree simples.

Le code suppose que vos valeurs JSON ont un format cohérent tel qu'affiché dans la question.

Configuration :

CREATE TYPE event_type AS (
 , event_slug  text
 , start_time  timestamp
 , end_time    timestamp
);

CREATE MATERIALIZED VIEW loc_event AS
SELECT l.location_id, e.event_slug, e.end_time  -- start_time not needed
FROM   locations l, jsonb_populate_recordset(null::event_type, l.events) e;

Réponse connexe pour jsonb_populate_recordset() :

  • Comment convertir le type jsonb de PostgreSQL 9.4 en flottant
CREATE INDEX loc_event_idx ON loc_event (event_slug, end_time, location_id);

Inclut également location_id pour autoriser les analyses d'index uniquement . (Voir la page de manuel et le wiki Postgres.)

Requête :

SELECT *
FROM   loc_event
WHERE  event_slug = 'test_1'
AND    end_time  >= '2014-10-30 14:04:06 -0400'::timestamptz;

Ou, si vous avez besoin de lignes complètes à partir des locations sous-jacents tableau :

SELECT l.*
FROM  (
   SELECT DISTINCT location_id
   FROM   loc_event
   WHERE  event_slug = 'test_1'
   AND    end_time  >= '2014-10-30 14:04:06 -0400'::timestamptz
   ) le
JOIN locations l USING (location_id);