Фильтровать строки на основе значений внутри нескольких столбцов JSONB

#postgresql #jsonb #postgresql-9.6

#postgresql #jsonb #postgresql-9.6

Вопрос:

Я пытаюсь выполнить поиск в таблице определенным LIKE %str% образом, но по полям внутри значений json по нескольким столбцам.

У меня есть таблица, в которой есть три столбца jsonb change , previous и specific_changes . Как вы можете себе представить, содержимое — это JSON, но структура этого json заранее неизвестна, поэтому я не могу использовать -> or ->> в запросе следующим образом:

select * from change_log where change -> 'field' = '"something"'

 create table change_log
(
    id               serial      not null
        constraint pk_change_log
            primary key,
    change           jsonb       not null,
    previous         jsonb,
    changed_at       timestamp with time zone default timezone('utc'::text, now()),
    specific_changes jsonb
);

INSERT INTO public.change_log (id, change, previous, changed_at, specific_changes) VALUES (1, '{"val": 2, "test": "test", "nested": {"nval": 1}}', 'null', '2020-11-12 16:53:28.827896', '{"val2": "Value2"}');
INSERT INTO public.change_log (id, change, previous, changed_at, specific_changes) VALUES (2, '{"val": "testNewChange", "test": "testChange", "nested": {"key": 1}}', '{"val": "2", "test": "testChange", "nested": {"nval": 1}}', '2020-11-15 12:18:35.021843', '{"new": "testValue"}');
INSERT INTO public.change_log (id, change, previous, changed_at, specific_changes) VALUES (3, '{"val": "newewChange", "test": "changeNew", "nested": {"val": 3}}', '{"val": "testNewChange", "test": "testValue", "nested": {"key": 1}}', '2020-11-15 12:19:40.832843', '{"new": "testChange", "nested": {"val": 1}}');
  

Мой вопрос:

  1. Как будет выглядеть запрос, в котором заданная строка вернет все строки из таблицы change_log, любой из 3 упомянутых столбцов jsonb содержит любые поля, имеющие значение like %string% .

  2. как бы вы сделали запрос нечувствительным к регистру

Примеры:


   INPUT           OUTPUT(ids)
  "2"              (1,2)
  "Change"         (2,3)
  "Chan"           (2,3)
  "Value"         (1,2,3)
  

EDIT1: я использую postgres версии 9.6

ПРАВКА2: исправлены вставленные изменения, отражающие желаемое поведение

Комментарии:

1. Почему значение id=1 опущено Change , пока оно содержит "test": "testChange" ключ? Вы хотите также проверить вложенные объекты? Если да, то почему id=3 2 значение не включено, пока оно содержит "nested": {"val": 2} ?

2. @Abelisto вы уловили мою ошибку, я обновил вставленные значения, чтобы они соответствовали указанным входным и выходным данным

Ответ №1:

Если вы используете Postgres 12 или более позднюю версию, вы можете использовать выражение пути SQL / JSON:

 select *
from change_log
where change @@ '$.** like_regex "change" flag "i"'
   or previous @@ '$.** like_regex "change" flag "i"'
   or specific_changes @@ '$.** like_regex "change" flag "i"'
  

Ответ №2:

Общий подход для старых версий PostgreSQL заключается в использовании exists некоторой функции, например

 select *
from table_name
where exists (
    select 1
    from jsonb_each_text(column_name) as t(k,v)
    where v ilike '%string%');
  

Для нескольких столбцов это можно сделать с помощью or :

 select *
from table_name
where
    exists (
        select 1
        from jsonb_each_text(column1) as t(k,v)
        where v ilike '%string%') or
    exists (
        select 1
        from jsonb_each_text(column2) as t(k,v)
        where v ilike '%string%');
  

или union :

 select *
from table_name
where
    exists (
        select 1
        from (
            select * from jsonb_each_text(column1) union all
            select * from jsonb_each_text(column2)) as t(k,v)
        where t.v ilike '%string%');
  

ДЕМОНСТРАЦИЯ

Обратите внимание, что вложенные объекты не будут обрабатываться должным образом, поскольку они будут проверяться как весь текст, включая ключи.

Чтобы исправить это, вам необходимо создать хранимую функцию, которая рекурсивно возвращает все значения из JSON.

Но это тема для другого вопроса 🙂

Комментарии:

1. Спасибо, у меня была похожая проблема, и это решение действительно помогло мне. Насколько я понимаю, какое имя оператора вы использовали в качестве псевдонима для возвращаемого набора jsonb_each_text ? Я попробовал (k,v) сам по себе, но запрос не удался; он работал только тогда, когда я использовал t like t(k, v) .

Ответ №3:

Вы можете запросить, например

 SELECT DISTINCT l.id
  FROM change_log l
 CROSS JOIN JSONB_EACH_TEXT( l.change ) AS c(e)
 CROSS JOIN JSONB_EACH_TEXT(  nullif(l.previous, 'null') ) AS p(e) 
 CROSS JOIN JSONB_EACH_TEXT( l.specific_changes ) AS s(e)
 WHERE c.value ~* 'change' OR s.value ~* 'change' OR p.value ~* 'change'
  

где ~* оператор выполняет поиск соответствия заданному ключевому слову без учета регистра, а функция JSONB_EACH_TEXT() расширяет самый внешний объект JSON в набор пар ключ / значение.

PS Необходимо исправить значение 'null' путем преобразования null в 1 значение идентификатора previous столбца или использовать nullif(l.previous, 'null') в качестве аргумента для второго JSONB_EACH_TEXT() в запросе

Demo

Комментарии:

1. nullif(value, 'null') Однако something cross join empty вернется empty (просто удалите where в своем запросе, чтобы убедиться)…

2. Поскольку я не упомянул, что я использую postgres 9.6, предоставленный запрос выдает [22023] ERROR: cannot call jsonb_each_text on a non-object , уверен, что это связано с версией

3. нет, @vuk , эта проблема связана не с версией, а с литералом 'null' внутри previous столбца. Либо преобразуйте его null , как я упоминал ранее, либо примените nullif() функцию, как указано в приведенном выше комментарии, и мое редактирование выполнено только что. Кстати, сейчас демонстрация в версии 9.6.

4. Большое вам спасибо, и я ценю терпение, которое потребовалось из-за моих плохих навыков sql!