Tho*_*mas 5 postgresql json postgresql-9.4
我们目前正在使用 JSONB 列来加速我们数据库中的任意搜索,并且到目前为止工作正常。更新数据时,要求如下:
为了说明这一点,请考虑以下示例:
现有(为了说明目的包含空值):
{
"a":null,
"b":1,
"c":1,
"f":1,
"g": {
"nested": 1
}
}
Run Code Online (Sandbox Code Playgroud)
这应该合并到现有对象中:
{
"b":2,
"d":null,
"e":2,
"f":null,
"g":{
"nested": 2
}
}
Run Code Online (Sandbox Code Playgroud)
如您所见,我们覆盖了一些字段并删除了f
. 所以预期的输出是:
{
"b": 2, //overridden
"c": 1, //kept
"e": 2, //added
"g": {
"nested": 2 //overridden
}
}
Run Code Online (Sandbox Code Playgroud)
为了实现这一点,我们使用以下函数:
CREATE OR REPLACE FUNCTION jsonb_merge(jsonb1 JSONB, jsonb2 JSONB)
RETURNS JSONB LANGUAGE sql IMMUTABLE
AS $$
SELECT
CASE
WHEN jsonb_typeof($1) = 'object' AND jsonb_typeof($2) = 'object' THEN
(
SELECT jsonb_object_agg(merged.key, merged.value) FROM
(
SELECT
COALESCE( p1.key, p2.key ) as key,
CASE
WHEN p1.key IS NULL then p2.value
WHEN p2.key IS NULL THEN p1.value
ELSE jsonb_merge( p1.value, p2.value )
END AS value
FROM jsonb_each($1) p1
FULL OUTER JOIN jsonb_each($2) p2 ON p1.key = p2.key
) AS merged
-- Removing this condition reduces runtime by 70%
WHERE NOT (merged.value IS NULL OR merged.value in ( '[]', 'null', '{}') )
)
WHEN jsonb_typeof($2) = 'null' OR (jsonb_typeof($2) = 'array' AND jsonb_array_length($2) < 1) OR $2 = '{}' THEN
NULL
ELSE
$2
END
$$;
Run Code Online (Sandbox Code Playgroud)
正如我所说,从功能的角度来看,这非常有效。但是,该功能非常慢。
一个发现是条件 onmerged.value
减慢了查询速度。删除它会导致执行时间减少约 70%,但显然结果并不符合要求。
那么我们如何才能实现 jsonb 对象的快速深度合并呢?
请注意 Postgres 9.5||
操作符没有按预期工作,即它保留了不需要的元素。虽然我们可以使用会使我们的查询复杂化的特殊删除操作,但我不确定这会更快。
到目前为止,我们考虑了以下(不满意的)选项:
话虽如此,我们希望解决 Postgres 9.4 上的问题,并尽可能使用 SQL 或 PL/pgSQL。
更新
我进一步试验,发现以下函数通过了我的测试,并且比我以前的(10 倍)快得多。我很确定这会按预期工作,但由于我不是数据库专家,欢迎再次查看:
CREATE OR REPLACE FUNCTION jsonb_update(jsonb1 JSONB, jsonb2 JSONB)
RETURNS JSONB LANGUAGE sql IMMUTABLE
AS $$
SELECT json_object_agg(merged.key, merged.value)::jsonb FROM
(
WITH existing_object AS (
SELECT key, value FROM jsonb_each($1)
WHERE NOT (value IS NULL OR value in ('[]', 'null', '{}') )
),
new_object AS (
SELECT key, value FROM jsonb_each($2)
),
deep_merge AS (
SELECT lft.key as key, jsonb_merge( lft.value, rgt.value ) as value
FROM existing_object lft
JOIN new_object rgt ON ( lft.key = rgt.key)
AND jsonb_typeof( lft.value ) = 'object'
AND jsonb_typeof( rgt.value ) = 'object'
)
-- Any non-empty element of jsonb1 that's not in jsonb2 (the keepers)
SELECT key, value FROM existing_object
WHERE key NOT IN (SELECT key FROM new_object )
UNION
-- Any non-empty element from jsonb2 that's not to be deep merged (the simple updates and new elements)
SELECT key, value FROM new_object
WHERE key NOT IN (SELECT key FROM deep_merge )
AND NOT (value IS NULL OR value in ('[]', 'null', '{}') )
UNION
-- All elements that need a deep merge
SELECT key, value FROM deep_merge
) AS merged
$$;
Run Code Online (Sandbox Code Playgroud)
这是一个经过大量重写但优化的版本(比原始版本快大约 20 倍,大约与我的问题中的更新版本一样快,但在要求方面更正确 - 当涉及“本质上为空”的对象时,更新有一些缺陷,请参阅下面了解它们是什么):
它是用 PlPgSQL 重写的,因此非常“漂亮”。;)
首先,我们需要一个新的合并函数类型,它基本上代表 . 返回的记录jsonb_each(...)
。
CREATE TYPE jsonEachRecord AS (KEY text, value JSONB);
Run Code Online (Sandbox Code Playgroud)
然后有一个函数来检查 json 对象是否“本质上为空”,这意味着它只包含空值、空对象、空数组或嵌套的“本质上为空”对象。
CREATE OR REPLACE FUNCTION jsonb_is_essentially_empty(jsonb1 jsonb )
RETURNS BOOLEAN LANGUAGE plpgsql IMMUTABLE
AS $func$
DECLARE
result BOOLEAN = TRUE;
r RECORD;
BEGIN
IF jsonb_typeof( jsonb1 ) <> 'object' THEN
IF jsonb1 IS NOT NULL AND jsonb1 NOT in ('[]', 'null') THEN
result = FALSE;
END IF;
ELSE
for r in SELECT key, value FROM jsonb_each(jsonb1) loop
if jsonb_typeof(r.value) = 'object' then
IF NOT jsonb_is_essentially_empty(r.value) THEN
result = FALSE;
exit;
end if;
ELSE
IF r.value IS NOT NULL AND r.value NOT IN ('[]', 'null', '{}') THEN
result = FALSE;
exit;
END IF;
END IF;
end loop;
END IF;
return result;
END;
$func$;
Run Code Online (Sandbox Code Playgroud)
最后这是实际的合并函数:
CREATE OR REPLACE FUNCTION jsonb_merge(jsonb1 JSONB, jsonb2 JSONB)
RETURNS JSONB LANGUAGE plpgsql IMMUTABLE
AS $func$
DECLARE
result jsonEachRecord[];
json_property jsonEachRecord;
idx int;
origArrayLength INT;
mergedValue JSONB;
mergedRecord jsonEachRecord;
BEGIN
FOR json_property IN (SELECT key, value FROM jsonb_each(jsonb1) ORDER BY key) LOOP
IF json_property.value IS NOT NULL AND json_property.value NOT IN ('[]', 'null', '{}') THEN
result = array_append(result, json_property);
END IF;
END LOOP;
idx = 1;
origArrayLength = array_length( result, 1);
FOR json_property IN (SELECT key, value FROM jsonb_each(jsonb2) ORDER BY key) LOOP
WHILE result[idx].key < json_property.key AND idx <= origArrayLength LOOP
idx = idx + 1;
END LOOP;
IF idx > origArrayLength THEN
IF NOT jsonb_is_essentially_empty( json_property.value ) THEN
result = array_append(result, json_property);
END IF;
ELSIF result[idx].key = json_property.key THEN
if jsonb_typeof(result[idx].value) = 'object' AND jsonb_typeof(json_property.value) = 'object' THEN
mergedValue = jsonb_merge( result[idx].value, json_property.value );
mergedRecord.key = json_property.key;
mergedRecord.value = mergedValue;
result[idx] = mergedRecord;
ELSE
result[idx] = json_property;
END IF;
idx = idx + 1;
ELSE
IF NOT jsonb_is_essentially_empty( json_property.value ) THEN
result = array_append(result, json_property);
END IF;
END IF;
END LOOP;
-- remove any remaining potentially empty elements
IF result IS NOT NULL THEN
FOR i IN REVERSE array_length( result, 1)..1 LOOP
IF jsonb_is_essentially_empty( result[i].value ) THEN
result = array_remove(result, result[i] );
END IF;
END LOOP;
END IF;
return (select json_object_agg(key, value) from unnest(result));
END;
$func$;
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,它非常丑陋,并且可能仍然存在一堆缺陷,例如数组的不断嵌套和 json 对象的聚合。根据我在其他地方读到的内容,通过索引访问数组需要引擎在每次访问元素时再次从前面的元素启动,这样也可以进行优化。