PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Qual é o índice adequado para consultar estruturas em arrays no Postgres jsonb?


Em primeiro lugar, você não pode acessar valores de matriz JSON assim. Para um determinado valor 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"}]

Um teste válido contra o primeiro elemento da matriz seria:
WHERE e->0->>'event_slug' = 'test_1'

Mas você provavelmente não deseja limitar sua pesquisa ao primeiro elemento da matriz. Com o jsonb tipo de dados no Postgres 9.4 você tem operadores adicionais e suporte a índices. Para indexar elementos de um array você precisa de um índice GIN.

As classes de operadores integradas para índices GIN não suportam operadores "maior que" ou "menor que" > >= < <= . Isso é verdade para jsonb também, onde você pode escolher entre duas classes de operadores. Por documentação:
Name             Indexed Data Type  Indexable Operators
...
jsonb_ops        jsonb              ? ?& ?| @>
jsonb_path_ops   jsonb              @>
   

(jsonb_ops sendo o padrão.) Você pode cobrir o teste de igualdade, mas nenhum desses operadores cobre seu requisito para >= comparação. Você precisaria de um índice btree.

Solução básica


Para dar suporte à verificação de igualdade com um índice:
CREATE INDEX locations_events_gin_idx ON locations
USING gin (events jsonb_path_ops);

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

Isso pode ser bom o suficiente se o filtro for seletivo o suficiente.
Supondo que end_time >= start_time , então não precisamos de duas verificações. Verificando apenas end_time é mais barato e 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 um JOIN LATERAL implícito . Detalhes (último capítulo):
  • PostgreSQL unnest() com número do elemento

Cuidado com os diferentes tipos de dados ! O que você tem no valor JSON se parece com timestamp [without time zone] , enquanto seus predicados usam timestamp with time zone literais. O timestamp o valor é interpretado de acordo com o fuso horário atual configuração, enquanto o timestamptz fornecido literais devem ser convertidos em timestamptz explicitamente ou o fuso horário seria ignorado! A consulta acima deve funcionar como desejado. Explicação detalhada:
  • Ignorando completamente os fusos horários no Rails e no PostgreSQL

Mais explicações para jsonb_array_elements() :
  • Ingressando no PostgreSQL usando JSONB

Solução avançada


Se o acima não for bom o suficiente, eu consideraria uma MATERIALIZED VIEW que armazena atributos relevantes em formato normalizado. Isso permite índices btree simples.

O código pressupõe que seus valores JSON tenham um formato consistente conforme exibido na pergunta.

Configuração:
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;

Resposta relacionada para jsonb_populate_recordset() :
  • Como converter o tipo jsonb do PostgreSQL 9.4 para float
CREATE INDEX loc_event_idx ON loc_event (event_slug, end_time, location_id);

Incluindo também location_id para permitir verificações somente de índice . (Consulte a página de manual e o Postgres Wiki.)

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

Ou, se você precisar de linhas completas dos locations subjacentes tabela:
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);