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