Условно заменить одно значение в строке в столбце jsonb

Мне нужен более эффективный способ обновления строк одной таблицы в Postgres 9.5. В настоящее время я делаю это с помощью pg_dump и повторно импортирую с обновленными значениями после операций поиска и замены в среде ОС Linux.

table_a имеет 300000 строк с 2 столбцами: id bigint и json_col jsonb. json_col имеет около 30 клавиш: от "C1" до "C30", как в этом примере:

Table_A

    id,json_col
    1  {"C1":"Paris","C2":"London","C3":"Berlin","C4":"Tokyo", ... "C30":"Dallas"}
    2  {"C1":"Dublin","C2":"Berlin","C3":"Kiev","C4":"Tokyo", ... "C30":"Phoenix"}
    3  {"C1":"Paris","C2":"London","C3":"Berlin","C4":"Ankara", ... "C30":"Madrid"}
    ...

The requirement is to mass search all keys from C1 to C30 then look in them for the value "Berlin" and replace with "Madrid" and only if Madrid is not repeated. i.e. id:1 with Key C3, and id:2 with C2. id:3 will be skipped because C30 exists with this value already

Он должен быть в одной команде SQL в PostgreSQL 9.5, один раз и с учетом всех ключей из столбца jsonb.

ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
4
0
1 447
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Самый быстрый и простой способ - изменить столбец как текст:

update table_a
set json_col = replace(json_col::text, '"Berlin"', '"Madrid"')::jsonb
where json_col::text like '%"Berlin"%'
and json_col::text not like '%"Madrid"%'

Это практичный выбор. Вышеупомянутый запрос представляет собой скорее операцию поиска и замены (как в текстовом редакторе), чем изменение атрибутов объектов. Второй вариант более сложный и, конечно, намного дороже. Даже при использовании быстрого движка Javascript (пример ниже) более формальное решение будет во много раз медленнее.

Вы можете попробовать Postgres Javascript:

create extension if not exists plv8;

create or replace function replace_item(data jsonb, from_str text, to_str text)
returns jsonb language plv8 as $$
    var found = 0;
    Object.keys(data).forEach(function(key) {
        if (data[key] == to_str) {
            found = 1;
        }
    })
    if (found == 0) {
        Object.keys(data).forEach(function(key) {
            if (data[key] == from_str) {
                data[key] = to_str;
            }
        })
    }
    return data;
$$;

update table_a
set json_col = replace_item(json_col, 'Berlin', 'Madrid');

Даже если он рассматривается как текст, все равно может работать. Спасибо, удовлетворительное решение.

Dmitry 17.12.2018 04:28

@ Дмитрий: если это ответ на ваш вопрос, подумайте о том, чтобы принять ответ.

Erwin Brandstetter 18.12.2018 23:44

Хорошо, я проверил все методы и могу сказать, что вы проделали отличную работу Это мне очень помогло. Позвольте мне поделиться с вами своим мнением.

Способ 1 по предложению Клина. Прекрасно работает и полностью нормально, кроме случаев, когда ключ называется как значение, тогда оба будут заменены ключом и значением. то есть: «Берлин»: «Берлин» становится «Мадрид»: «Мадрид»

Способ 2 с расширением plv8 не работал, потому что мне не хватает контрольного файла мне пришлось установить его, и я просто пропустил этот метод, так что у меня нет отзывы об этом методе. Я получал следующую ошибку: ОШИБКА: не удалось открыть файл управления расширениями "/usr/pgsql-9.5/share/extension/plv8.control": нет такого файла или каталога.

Способ 3 похож на метод 2 с функция jsonb_replace_value отлично работает, заменяет строки, содержащие определенное значение, независимо от ключа. И добавляем условие

WHERE json_col <> jsonb_replace_value(json_col, '"Berlin"', '"Madrid"')

будет избегать пустых обновлений и пропускать строки, которые не нужно обновлять И что-то вроде этого

{"Берлин": "Берлин"} становится {"Берлин": "Мадрид"}, т.е. ключ не трогается, только значение

Метод 4 немного сложнее, он использует Метод 3 и указатели Он работает потрясающе и очень быстро.
И НЕ СУЩЕСТВУЕТ полу-анти-объединение действительно заставляет снова использовать Индекс.
Я был шокирован тем, как быстро он работал !!!

Однако я обнаружил, что все эти методы будут работать, если строка json выглядит так: {"ключ": "значение"} Если мне нужно, например, обновить значение, являющееся объектом json, оно не будет обновляться примерно так:
{"C30":{"id":10044,"value":"Berlin","created_by":"John Doe"}}

БОЛЬШОЕ СПАСИБО вам, ребята. @klin и @ erwin-brandstetter. Это помогло мне узнать что-то новое!

«Я был шокирован тем, как быстро он работал !!!» Ах, это тот эффект, к которому я стремился. :) Что касается plv8: вам необходимо получить программное обеспечение, прежде чем вы сможете его установить (его нет в стандартном дистрибутиве). Например: pgxn.org/dist/plv8 (сам обычно не использую). Наконец, чтобы заменить значения во вложенных объектах, вам придется сделать больше. Я добавил примечание об этом.
Erwin Brandstetter 21.12.2018 03:11

Что делает это трудным, так это то, что вы ищете неизвестный ключи, содержащий интересующий ценности. Инфраструктура Postgres оптимизирована для поиска ключи (или значений массива).

Возможно, это вызвано неоптимальной конструкцией стола. Многие объекты верхнего уровня вашего столбца jsonb могут быть заменены на множество, полностью отбрасывая нерелевантные имена ключей. (Или, может быть, другой массив для имен ключей.) Или, в идеале, для начала с полной нормализованной схемой БД.

Как бы то ни было, вот доказательство концепции, как это может быть быстро и чисто с стандартный Postgres 9.5 или новее в любом случае.

Дополнительная трудность 1: неизвестно, возможны ли повторяющиеся значения. Дополнительная сложность 2: значения частот тоже неизвестны. Дополнительная трудность 3: заменяется только найденное значение первый, и только если целевого значения еще нет. Реализация этого с помощью операций на основе наборов возможна, но громоздка. Вместо этого я написал функцию plpgsql:

CREATE OR REPLACE FUNCTION jsonb_replace_value(_j jsonb, _old jsonb, _new jsonb)
   RETURNS jsonb AS
$func$
DECLARE
   _key text;
   _val jsonb;
BEGIN
   FOR _key, _val IN
      SELECT * FROM jsonb_each(_j)
   LOOP
      IF _val = _old THEN
         RETURN jsonb_set(_j, ARRAY[_key], _new);  -- update 1st key
      END IF;
   END LOOP;

   RETURN _j;  -- nothing found, return original
END
$func$ LANGUAGE plpgsql IMMUTABLE;

COMMENT ON FUNCTION jsonb_replace_value(jsonb, jsonb, jsonb) IS '
Replace the first occurrence of _old value with _new.
Call:
    SELECT jsonb_replace_value('{"C1":"Paris","C3":"Berlin","C4":"Berlin"}', '"Berlin"', '"Madrid"')';

Можно улучшить, чтобы при желании заменить вхождения все и т. д., Но это выходит за рамки этого вопроса.

Теперь это будет просто:

UPDATE table_a
SET    json_col = jsonb_replace_value(json_col, '"Berlin"', '"Madrid"'); -- note jsonb literal syntax!

Если строки все нуждаются в обновлении, мы можем остановиться на этом. Быстрее не станет. (За исключением, возможно, альтернатив, таких как продемонстрировано @klin.)
Если большой процент всех строк нуждается в обновлении, добавьте условие WHERE, чтобы избежать пустых обновлений:

...
WHERE  json_col <> jsonb_replace_value(json_col, '"Berlin"', '"Madrid"');

Видеть:

Обычно, только очень несколько рядов действительно нуждается в обновлении. Тогда итерация по всем строкам с указанным выше запросом стоит дорого. Нам нужен поддержка индекса, чтобы сделать это быстро. Непросто для дела. Я предлагаю индекс выражения на основе функции IMMUTABLE, извлекающей массив значений:

CREATE OR REPLACE FUNCTION jsonb_object_val_arr(jsonb)
   RETURNS text[] LANGUAGE sql IMMUTABLE AS
'SELECT ARRAY (SELECT value FROM jsonb_each_text($1))';

COMMENT ON FUNCTION jsonb_object_val_arr(jsonb) IS '
   Generates text array of values in outermost jsonb object.
   Of limited use if there can be nested objects.';

CREATE INDEX table_a_val_arr_idx ON table_a USING gin (jsonb_object_val_arr(json_col));

Связанные, с дополнительными пояснениями:

Запрос, использующий этот индекс:

UPDATE table_a a
SET    json_col = jsonb_replace_value(a.json_col, '"Berlin"', '"Madrid"')
WHERE  jsonb_object_val_arr(json_col) @> '{Berlin}' -- has Berlin, possibly > 1x ..
-- AND    NOT jsonb_object_val_arr(json_col) @> '{Madrid}'
AND    NOT EXISTS (                                         -- .. but not Madrid
   SELECT FROM table_a b
   WHERE  jsonb_object_val_arr(json_col) @> '{Madrid}'  -- note array literal syntax
   AND    b.id = a.id
   );

Полуавтоматическое соединение NOT EXISTS тщательно разработано, чтобы использовать индекс во второй раз.

Прокомментированная более простая альтернатива будет быстрее, если есть несколько строк с 'Berlin' и 'Madrid' - тогда шаг фильтрации в плане запроса будет дешевле.

Должно быть очень быстро.

db <> рабочий пример здесь для Postgres 9.5 демонстрирует все.

Другие вопросы по теме