PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Qual è l'indice corretto per interrogare le strutture negli array in Postgres jsonb?

Prima di tutto, non puoi accedere ai valori dell'array JSON in questo modo. Per un dato valore json

[{"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 valido rispetto al primo elemento dell'array sarebbe:

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

Ma probabilmente non vuoi limitare la tua ricerca al primo elemento dell'array. Con il jsonb tipo di dati in Postgres 9.4 hai operatori aggiuntivi e supporto per gli indici. Per indicizzare gli elementi di un array è necessario un indice GIN.

Le classi di operatori integrate per gli indici GIN non supportano gli operatori "maggiore di" o "minore di" > >= < <= . Questo è vero per jsonb inoltre, dove puoi scegliere tra due classi di operatori. Per documentazione:

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

(jsonb_ops essendo l'impostazione predefinita.) Puoi coprire il test di uguaglianza, ma nessuno di questi operatori soddisfa i tuoi requisiti per >= confronto. Avresti bisogno di un indice btree.

Soluzione di base

Per supportare il controllo di uguaglianza con un indice:

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

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

Questo potrebbe essere abbastanza buono se il filtro è sufficientemente selettivo.
Supponendo end_time >= start_time , quindi non abbiamo bisogno di due controlli. Controllo solo end_time è più economico ed equivalente:

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;

Utilizzando un implicito JOIN LATERAL . Dettagli (ultimo capitolo):

  • PostgreSQL unnest() con numero elemento

Fai attenzione ai diversi tipi di dati ! Quello che hai nel valore JSON è simile a timestamp [without time zone] , mentre i tuoi predicati usano timestamp with time zone letterali. Il timestamp il valore viene interpretato in base al fuso orario corrente impostazione, mentre il dato timestamptz i letterali devono essere trasmessi a timestamptz in modo esplicito o il fuso orario verrebbe ignorato! La query sopra dovrebbe funzionare come desiderato. Spiegazione dettagliata:

  • Ignora del tutto i fusi orari in Rails e PostgreSQL

Ulteriori spiegazioni per jsonb_array_elements() :

  • Partecipazione a PostgreSQL tramite JSONB

Soluzione avanzata

Se quanto sopra non è abbastanza buono, prenderei in considerazione una MATERIALIZED VIEW che memorizza gli attributi rilevanti in forma normalizzata. Ciò consente semplici indici btree.

Il codice presuppone che i tuoi valori JSON abbiano un formato coerente come mostrato nella domanda.

Configurazione:

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;

Risposta correlata per jsonb_populate_recordset() :

  • Come convertire il tipo jsonb di PostgreSQL 9.4 in float
CREATE INDEX loc_event_idx ON loc_event (event_slug, end_time, location_id);

Includendo anche location_id per consentire scansioni solo indice . (Vedi la pagina del manuale e il Wiki di Postgres.)

Domanda:

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

Oppure, se hai bisogno di righe complete dalle locations sottostanti tabella:

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);