Условно заменить одно значение на строку в столбце jsonb - PullRequest
0 голосов
/ 17 декабря 2018

Мне нужен более эффективный способ обновления строк одной таблицы в 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"}
    ...

Требуется провести массовый поиск всех ключей от C1 до C30, а затем найти ихЗначение «Берлин» и заменить на «Мадрид» и только если Мадрид не повторяется.то есть id: 1 с ключом C3 и id: 2 с C2.id: 3 будет пропущен, поскольку C30 уже существует с этим значением

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

Ответы [ 3 ]

0 голосов
/ 17 декабря 2018

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

Метод 1 предложено Клин.Работает идеально и полностью нормально, за исключением того, что ключ имеет имя, подобное значению, тогда оба будут заменены ключом и значением.то есть: "Berlin": "Berlin" становится "Madrid": "Madrid"

Метод 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"')

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

{"Berlin": "Berlin"} становится {" Berlin ":" Madrid "} т.е. ключ не трогается, просто значение

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

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

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

0 голосов
/ 19 декабря 2018

Что делает это трудным, так это то, что вы ищете неизвестные ключи , содержащие значения интереса.Инфраструктура 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 <> fiddle здесь для Postgres 9.5, демонстрирующих все.

0 голосов
/ 17 декабря 2018

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

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');
...