Compruebe si una matriz JSON de Postgres contiene una cadena

121

Tengo una mesa para almacenar información sobre mis conejos. Se parece a esto:

create table rabbits (rabbit_id bigserial primary key, info json not null);
insert into rabbits (info) values
  ('{"name":"Henry", "food":["lettuce","carrots"]}'),
  ('{"name":"Herald","food":["carrots","zucchini"]}'),
  ('{"name":"Helen", "food":["lettuce","cheese"]}');

¿Cómo puedo encontrar los conejos a los que les gustan las zanahorias? Se me ocurrió esto:

select info->>'name' from rabbits where exists (
  select 1 from json_array_elements(info->'food') as food
  where food::text = '"carrots"'
);

No me gusta esa consulta. Es un desastre.

Como cuidador de conejos a tiempo completo, no tengo tiempo para cambiar el esquema de mi base de datos. Solo quiero alimentar adecuadamente a mis conejos. ¿Existe una forma más legible de hacer esa consulta?

Bola de nieve
fuente
1
Interesante pregunta. He jugado con eso, pero luego me di cuenta de que no estoy seguro de lo que quieres decir con "mejor". ¿Con qué criterios estás juzgando tus respuestas? ¿Legibilidad? ¿Eficiencia? ¿Otro?
David S
@DavidS: (Actualicé la pregunta). Preferiría la legibilidad sobre la eficiencia. Ciertamente, no espero nada mejor que un escaneo completo de la tabla, ya que mantengo el esquema fijo.
Bola de nieve
11
¿Está mal que voté a favor de esta pregunta debido a los conejos?
osman
3
Acabo de votar a favor de esta pregunta debido a los conejos y luego vi su comentario @osman
1valdis
Vi tu comentario y luego me di cuenta de que necesito votar a favor por los conejos
Peter Aron Zentai

Respuestas:

186

A partir de PostgreSQL 9.4, puede utilizar el ?operador :

select info->>'name' from rabbits where (info->'food')::jsonb ? 'carrots';

Incluso puede indexar la ?consulta en la "food"clave si cambia al tipo jsonb en su lugar:

alter table rabbits alter info type jsonb using info::jsonb;
create index on rabbits using gin ((info->'food'));
select info->>'name' from rabbits where info->'food' ? 'carrots';

Por supuesto, probablemente no tengas tiempo para eso como cuidador de conejos a tiempo completo.

Actualización: Aquí hay una demostración de las mejoras de rendimiento en una mesa de 1,000,000 de conejos donde a cada conejo le gustan dos alimentos y al 10% de ellos le gustan las zanahorias:

d=# -- Postgres 9.3 solution
d=# explain analyze select info->>'name' from rabbits where exists (
d(# select 1 from json_array_elements(info->'food') as food
d(#   where food::text = '"carrots"'
d(# );
 Execution time: 3084.927 ms

d=# -- Postgres 9.4+ solution
d=# explain analyze select info->'name' from rabbits where (info->'food')::jsonb ? 'carrots';
 Execution time: 1255.501 ms

d=# alter table rabbits alter info type jsonb using info::jsonb;
d=# explain analyze select info->'name' from rabbits where info->'food' ? 'carrots';
 Execution time: 465.919 ms

d=# create index on rabbits using gin ((info->'food'));
d=# explain analyze select info->'name' from rabbits where info->'food' ? 'carrots';
 Execution time: 256.478 ms
Bola de nieve
fuente
cómo obtener las filas donde la matriz de alimentos dentro de json no está vacía, por ejemplo, si podemos considerar, son JSON, donde la matriz de alimentos también está vacía, ¿puede ayudarme?
Bravo
1
@Bravoselect * from rabbits where info->'food' != '[]';
Snowball
1
¿Alguien sabe cómo funciona esto en caso de que necesite seleccionar un número entero en lugar de una cadena / texto?
Rotareti
3
@Rotareti Puede utilizar el @> operador : create table t (x jsonb); insert into t (x) values ('[1,2,3]'), ('[2,3,4]'), ('[3,4,5]'); select * from t where x @> '2';. Tenga en cuenta que '2'es un número JSON; no se deje engañar por las citas.
Bola de nieve
@Snowball, esta consulta selecciona info - >> 'nombre' de conejos donde (info -> 'comida') :: jsonb? 'zanahorias'; funciona perfectamente para la palabra de búsqueda de JSON. Pero, ¿cómo puedo conseguir que todos los registros no contengan la palabra 'zanahorias'?
Milán
23

Podrías usar el operador @> para hacer esto algo como

SELECT info->>'name'
FROM rabbits
WHERE info->'food' @> '"carrots"';
gori
fuente
1
Esto es útil cuando el elemento también es nulo
Lucio
2
Asegúrate de prestar atención a las 'garrapatas que rodean las "zanahorias" ... se rompe si las dejas fuera, incluso si estás buscando un número entero. (Pasé 3 horas tratando de encontrar un número entero, haciéndolo funcionar mágicamente envolviendo 'garrapatas alrededor del número)
skplunkerin
@skplunkerin Debe ser un valor json rodeado de 'ticks para formar una cadena, porque todo es una cadena para SQL en tipo JSONB. Por ejemplo, boolean: 'true', cadena: '"example"', número entero: '123'.
1valdis
22

No más inteligente sino más simple:

select info->>'name' from rabbits WHERE info->>'food' LIKE '%"carrots"%';
chrmod
fuente
13

Una pequeña variación pero nada nuevo de hecho. Realmente le falta una característica ...

select info->>'name' from rabbits 
where '"carrots"' = ANY (ARRAY(
    select * from json_array_elements(info->'food'))::text[]);
Macías
fuente