sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Cuál es el índice adecuado para consultar estructuras en matrices en Postgres jsonb?

En primer lugar, no puede acceder a los valores de matriz JSON de esa manera. Para un valor json dado

[{"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"}]

Una prueba válida contra el primer elemento de la matriz sería:

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

Pero probablemente no quiera limitar su búsqueda al primer elemento de la matriz. Con el jsonb tipo de datos en Postgres 9.4 tiene operadores adicionales y soporte de índice. Para indexar elementos de una matriz, necesita un índice GIN.

Las clases de operadores integradas para los índices GIN no admiten los operadores "mayor que" o "menor que" > >= < <= . Esto es cierto para jsonb también, donde puede elegir entre dos clases de operadores. Por documentación:

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

(jsonb_ops siendo el predeterminado). Puede cubrir la prueba de igualdad, pero ninguno de esos operadores cubre su requisito para >= comparación. Necesitaría un índice btree.

Solución básica

Para apoyar la comprobación de igualdad con un índice:

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

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

Esto podría ser lo suficientemente bueno si el filtro es lo suficientemente selectivo.
Suponiendo que end_time >= start_time , por lo que no necesitamos dos controles. Comprobando solo end_time es más barato y 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;

Utilizando un JOIN LATERAL implícito . Detalles (último capítulo):

  • PostgreSQL unnest() con número de elemento

Cuidado con los diferentes tipos de datos ! Lo que tiene en el valor JSON parece timestamp [without time zone] , mientras que sus predicados usan timestamp with time zone literales. La timestamp el valor se interpreta de acuerdo con la zona horaria actual configuración, mientras que el timestamptz dado los literales deben convertirse a timestamptz ¡explícitamente o la zona horaria sería ignorada! La consulta anterior debería funcionar como se desee. Explicación detallada:

  • Ignorar las zonas horarias por completo en Rails y PostgreSQL

Más explicación para jsonb_array_elements() :

  • Unirse a PostgreSQL usando JSONB

Solución avanzada

Si lo anterior no es lo suficientemente bueno, consideraría una MATERIALIZED VIEW que almacena atributos relevantes en forma normalizada. Esto permite índices btree simples.

El código asume que sus valores JSON tienen un formato consistente como se muestra en la pregunta.

Configuración:

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;

Respuesta relacionada para jsonb_populate_recordset() :

  • Cómo convertir el tipo jsonb de PostgreSQL 9.4 a flotante
CREATE INDEX loc_event_idx ON loc_event (event_slug, end_time, location_id);

También incluye location_id para permitir escaneos de solo índice . (Consulte la página del manual y el wiki de Postgres).

Consulta:

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

O, si necesita filas completas de las locations subyacentes tabla:

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