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