有条件地替换 jsonb 列中每行的单个值
Conditionally replace single value per row in jsonb column
我需要一种更有效的方法来更新 Postgres 9.5 中单个 table 的行。
我目前正在使用 pg_dump 执行此操作,并在 Linux OS 环境中进行搜索和替换操作后使用更新后的值重新导入。
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
它必须在 PostgreSQL 9.5 中的单个 SQL 命令中,一次并考虑 jsonb
列中的所有键。
最快最简单的方法是将列修改为文本:
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');
好的,我已经测试了所有方法,我可以说你做得很好
这对我帮助很大。让我与您分享我的反馈。
Klin 推荐的方法 1。工作完美,完全没问题,除非
key 和 value 一样命名,那么两者都将被替换为 key 和 value。
即:"Berlin":"Berlin" 变为 "Madrid":"Madrid"
带有 plv8 扩展名的方法 2 无效,因为我缺少控制文件
我必须安装它,我只是跳过了这个方法,所以我没有
关于此方法的反馈。
我得到的错误是:
错误:无法打开扩展控制文件
"/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和索引
它工作得非常棒而且速度超快。
而NOT EXISTS半反连接确实又被迫使用了Index。
它的执行速度让我震惊!!!
但是我发现如果 json 字符串如下所示,所有这些方法都有效:
{"key":"value"}
例如,如果我要更新一个 json 对象的值,它将不会更新
像这样:
{"C30":{"id":10044,"value":"Berlin","created_by":"John Doe"}}
非常感谢你们。@klin 和@erwin-brandstetter。 这帮助我学到了新东西!
困难在于您正在寻找未知的 keys 持有 values 兴趣。 Postgres 基础架构已优化以查找 键(或数组值)。
可能是由次优的 table 设计引起的。 jsonb
列的许多顶级对象可能会被 数组 替换,完全丢弃不相关的键名。 (或者可能是另一个键名数组。)或者,理想情况下,以完整的规范化数据库模式开始。
尽管如此,这里有一个概念验证,如何又快又干净 stock Postgres 9.5 或更高版本 无论如何。
附加难度1:不知道是否可以重复值
附加难点2:数值频率也未知
附加难度 3:仅替换找到的 first 值,并且仅当目标值不存在时。使用基于集合的操作来实现这一点是可能的,但很笨拙。我改写了一个 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"')';
可以增强以选择性地替换 all 出现等,但这超出了这个问题的范围。
现在这很简单:
UPDATE table_a
SET json_col = jsonb_replace_value(json_col, '"Berlin"', '"Madrid"'); -- note jsonb literal syntax!
如果所有行需要更新,我们可以到此为止。不会变快。 (除非可能有 等替代方案。)
如果所有行的 大百分比 需要更新,请添加 WHERE
条件以避免空更新:
...
WHERE json_col <> jsonb_replace_value(json_col, '"Berlin"', '"Madrid"');
参见:
- How do I (or can I) SELECT DISTINCT on multiple columns?
通常,只有非常几行实际上需要更新。然后使用上述查询遍历所有行是昂贵的。我们需要 index 支持 来让它更快。这个案子不容易。我建议基于 IMMUTABLE
函数提取值数组的表达式索引:
CREATE OR REPLACE FUNCTION jsonb_object_val_arr(jsonb)
RETURNS text[] LANGUAGE sql IMMUTABLE AS
'SELECT ARRAY (SELECT value FROM jsonb_each_text())';
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
semi-anti-join 是为第二次利用索引而精心起草的。
如果 'Berlin' 和 'Madrid' 的行很少,则注释更简单的替代方法更快 - 那么查询计划中的过滤步骤会更便宜.
应该非常快。
db<>fiddle here 用于 Postgres 9.5 演示所有内容。
我需要一种更有效的方法来更新 Postgres 9.5 中单个 table 的行。 我目前正在使用 pg_dump 执行此操作,并在 Linux OS 环境中进行搜索和替换操作后使用更新后的值重新导入。
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
它必须在 PostgreSQL 9.5 中的单个 SQL 命令中,一次并考虑 jsonb
列中的所有键。
最快最简单的方法是将列修改为文本:
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');
好的,我已经测试了所有方法,我可以说你做得很好 这对我帮助很大。让我与您分享我的反馈。
Klin 推荐的方法 1。工作完美,完全没问题,除非 key 和 value 一样命名,那么两者都将被替换为 key 和 value。 即:"Berlin":"Berlin" 变为 "Madrid":"Madrid"
带有 plv8 扩展名的方法 2 无效,因为我缺少控制文件 我必须安装它,我只是跳过了这个方法,所以我没有 关于此方法的反馈。 我得到的错误是: 错误:无法打开扩展控制文件 "/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和索引
它工作得非常棒而且速度超快。
而NOT EXISTS半反连接确实又被迫使用了Index。
它的执行速度让我震惊!!!
但是我发现如果 json 字符串如下所示,所有这些方法都有效:
{"key":"value"}
例如,如果我要更新一个 json 对象的值,它将不会更新
像这样:
{"C30":{"id":10044,"value":"Berlin","created_by":"John Doe"}}
非常感谢你们。@klin 和@erwin-brandstetter。 这帮助我学到了新东西!
困难在于您正在寻找未知的 keys 持有 values 兴趣。 Postgres 基础架构已优化以查找 键(或数组值)。
可能是由次优的 table 设计引起的。 jsonb
列的许多顶级对象可能会被 数组 替换,完全丢弃不相关的键名。 (或者可能是另一个键名数组。)或者,理想情况下,以完整的规范化数据库模式开始。
尽管如此,这里有一个概念验证,如何又快又干净 stock Postgres 9.5 或更高版本 无论如何。
附加难度1:不知道是否可以重复值
附加难点2:数值频率也未知
附加难度 3:仅替换找到的 first 值,并且仅当目标值不存在时。使用基于集合的操作来实现这一点是可能的,但很笨拙。我改写了一个 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"')';
可以增强以选择性地替换 all 出现等,但这超出了这个问题的范围。
现在这很简单:
UPDATE table_a
SET json_col = jsonb_replace_value(json_col, '"Berlin"', '"Madrid"'); -- note jsonb literal syntax!
如果所有行需要更新,我们可以到此为止。不会变快。 (除非可能有
如果所有行的 大百分比 需要更新,请添加 WHERE
条件以避免空更新:
...
WHERE json_col <> jsonb_replace_value(json_col, '"Berlin"', '"Madrid"');
参见:
- How do I (or can I) SELECT DISTINCT on multiple columns?
通常,只有非常几行实际上需要更新。然后使用上述查询遍历所有行是昂贵的。我们需要 index 支持 来让它更快。这个案子不容易。我建议基于 IMMUTABLE
函数提取值数组的表达式索引:
CREATE OR REPLACE FUNCTION jsonb_object_val_arr(jsonb)
RETURNS text[] LANGUAGE sql IMMUTABLE AS
'SELECT ARRAY (SELECT value FROM jsonb_each_text())';
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
semi-anti-join 是为第二次利用索引而精心起草的。
如果 'Berlin' 和 'Madrid' 的行很少,则注释更简单的替代方法更快 - 那么查询计划中的过滤步骤会更便宜.
应该非常快。
db<>fiddle here 用于 Postgres 9.5 演示所有内容。