PostgreSQL сравнивает два объекта jsonb

В PostgreSQL (v9.5) форматы JSONB предоставляют потрясающие возможности. Но теперь я застрял в том, что кажется относительно простой операцией;

сравнить два объекта jsonb; увидеть, что в одном документе отличается или отсутствует в другом.

Что у меня есть на данный момент

WITH reports(id,DATA) AS (
          VALUES (1,'{"a":"aaa", "b":"bbb", "c":"ccc"}'::jsonb),
                 (2,'{"a":"aaa", "b":"jjj", "d":"ddd"}'::jsonb) )
SELECT jsonb_object_agg(anon_1.key, anon_1.value)
FROM
  (SELECT anon_2.key AS KEY,
      reports.data -> anon_2.KEY AS value
   FROM reports,
     (SELECT DISTINCT jsonb_object_keys(reports.data) AS KEY
      FROM reports) AS anon_2
   ORDER BY reports.id DESC) AS anon_1

Должен вернуть разницу строки 1 по сравнению со строкой 2:

'{"b":"bbb", "c":"ccc", "d":null}'

Вместо этого он также возвращает дубликаты ({"a": "aaa"}). Также; в целом может быть более элегантный подход!


person Joost Döbken    schedule 16.03.2016    source источник
comment
(недостаточно очков для комментариев) для stackoverflow.com/a/37278190/3920439, он работал отлично, однако jsonb_typeof ( val1) = проверка 'null' работает только для 'null' строк / значений jsonb. если вы передадите фактический null в val1, он вернет null. изменение IF val1 IS NULL OR jsonb_typeof(val1) = 'null' позволяет мне вернуть значение val2 целиком для случая, когда значение val1 было равно null (этот сценарий возникает при выполнении функций задержки для первой строки)   -  person ThePianoDentist    schedule 03.07.2019


Ответы (4)


ОБНОВЛЕНО

CREATE OR REPLACE FUNCTION jsonb_diff_val(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
  result JSONB;
  v RECORD;
BEGIN
   result = val1;
   FOR v IN SELECT * FROM jsonb_each(val2) LOOP
     IF result @> jsonb_build_object(v.key,v.value)
        THEN result = result - v.key;
     ELSIF result ? v.key THEN CONTINUE;
     ELSE
        result = result || jsonb_build_object(v.key,'null');
     END IF;
   END LOOP;
   RETURN result;
END;
$$ LANGUAGE plpgsql;

Запрос:

SELECT jsonb_diff_val(
    '{"a":"aaa", "b":"bbb", "c":"ccc"}'::jsonb,
    '{"a":"aaa", "b":"jjj", "d":"ddd"}'::jsonb
);
            jsonb_diff_val             
---------------------------------------
 {"b": "bbb", "c": "ccc", "d": "null"}
(1 row)
person Dmitry Savinkov    schedule 16.03.2016
comment
Спасибо за ответ: создание функции кажется приятным занятием. Тем не мение; '{"b":"bbb", "c":"ccc", "d":null}' не является опечаткой, поскольку "d" не находится в строке 1, и поэтому функция должна возвращать "d":null - person Joost Döbken; 17.03.2016
comment
В своем вопросе я изменил массив json на объекты json; Я не знал этих определений - person Joost Döbken; 17.03.2016
comment
Как мы можем использовать это в postgres 9.4 - person Mr. Sha; 19.07.2018
comment
Это очень элегантно, Дмитрий. Я использовал это для простого триггера аудита / журнала json в github.com/rorycl/pg_json_logger - - надеюсь, что это нормально. - person rorycl; 21.01.2021

Я создал аналогичную функцию, которая рекурсивно просканирует объект и вернет разницу между новым и старым объектами. Мне не удалось найти «более приятный» способ определить, является ли объект jsonb «пустым», поэтому был бы признателен за любое предложение, как это упростить. Я планирую использовать его для отслеживания обновлений, внесенных в объекты jsonb, поэтому я сохраняю только то, что было изменено.

Вот функция:

CREATE OR REPLACE FUNCTION jsonb_diff_val(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
    result JSONB;
    object_result JSONB;
    i int;
    v RECORD;
BEGIN
    IF jsonb_typeof(val1) = 'null'
    THEN 
        RETURN val2;
    END IF;

    result = val1;
    FOR v IN SELECT * FROM jsonb_each(val1) LOOP
        result = result || jsonb_build_object(v.key, null);
    END LOOP;

    FOR v IN SELECT * FROM jsonb_each(val2) LOOP
        IF jsonb_typeof(val1->v.key) = 'object' AND jsonb_typeof(val2->v.key) = 'object'
        THEN
            object_result = jsonb_diff_val(val1->v.key, val2->v.key);
            -- check if result is not empty 
            i := (SELECT count(*) FROM jsonb_each(object_result));
            IF i = 0
            THEN 
                result = result - v.key; --if empty remove
            ELSE 
                result = result || jsonb_build_object(v.key,object_result);
            END IF;
        ELSIF val1->v.key = val2->v.key THEN 
            result = result - v.key;
        ELSE
            result = result || jsonb_build_object(v.key,v.value);
        END IF;
    END LOOP;

    RETURN result;

END;
$$ LANGUAGE plpgsql;

Тогда простой запрос выглядит так:

SELECT jsonb_diff_val(                                                                                                                                                                                                                                                           
    '{"a":"aaa", "b":{"b1":"b","b2":"bb","b3":{"b3a":"aaa","b3c":"ccc"}}, "c":"ccc"}'::jsonb,
    '{"a":"aaa", "b":{"b1":"b1","b3":{"b3a":"aaa","b3c":"cccc"}}, "d":"ddd"}'::jsonb
);
                                jsonb_diff_val                                 
-------------------------------------------------------------------------------
 {"b": {"b1": "b1", "b2": null, "b3": {"b3c": "cccc"}}, "c": null, "d": "ddd"}
(1 row)
person J. Raczkiewicz    schedule 17.05.2016
comment
вы можете проверить пустой объект json с помощью IF object_result = '{}'::jsonb THEN - person rodney757; 14.05.2017
comment
В качестве примечания, из-за jsonb_build_object и использования || для объединения объектов jsonb это PG 9.5+. Я думаю, вы могли бы использовать json_build_object(...)::jsonb, чтобы обойти первую проблему, но я не думаю, что был способ объединить два поля jsonb до 9.5. - person Ryan Guill; 18.08.2017

Вот решение без создания новой функции;

SELECT
    json_object_agg(COALESCE(old.key, new.key), old.value)
  FROM json_each_text('{"a":"aaa", "b":"bbb", "c":"ccc"}') old
  FULL OUTER JOIN json_each_text('{"a":"aaa", "b":"jjj", "d":"ddd"}') new ON new.key = old.key 
WHERE 
  new.value IS DISTINCT FROM old.value

В результате;

{"b" : "bbb", "c" : "ccc", "d" : null}

Этот метод сравнивает только первый уровень json. Он НЕ проходит через все дерево объектов.

person Sahap Asci    schedule 25.04.2019

Мое решение не рекурсивное, но вы можете использовать его для определения общих ключей / значений:

-- Diff two jsonb objects
CREATE TYPE jsonb_object_diff_result AS (
  old jsonb,
  new jsonb,
  same jsonb
);
CREATE OR REPLACE FUNCTION jsonb_object_diff(in_old jsonb, in_new jsonb)
RETURNS jsonb_object_diff_result AS
$jsonb_object_diff$
DECLARE
  _key text;
  _value jsonb;
  _old jsonb;
  _new jsonb;
  _same jsonb;
BEGIN
  _old := in_old;
  _new := in_new;

  FOR _key, _value IN SELECT * FROM jsonb_each(_old) LOOP
    IF (_new -> _key) = _value THEN
      _old := _old - _key;
      _new := _new - _key;
      IF _same IS NULL THEN
        _same := jsonb_build_object(_key, _value);
      ELSE
        _same := _same || jsonb_build_object(_key, _value);
      END IF;
    END IF;
  END LOOP;

  RETURN (_old, _new, _same);
END;
$jsonb_object_diff$
LANGUAGE plpgsql;

Результат может выглядеть так:

SELECT * FROM jsonb_object_diff(
  '{"a": 1, "b": 5, "extra1": "woo", "old_null": null, "just_null": null}'::jsonb,
  '{"a": 1, "b": 4, "extra2": "ahoj", "new_null": null, "just_null": null}'::jsonb);

-[ RECORD 1 ]--------------------------------------
old  | {"b": 5, "extra1": "woo", "old_null": null}
new  | {"b": 4, "extra2": "ahoj", "new_null": null}
same | {"a": 1, "just_null": null}
person langpavel    schedule 16.09.2017
comment
Вау, это здорово! - person Anton Bessonov; 05.06.2021