現在、JSONB列を使用して、データベース内の任意の検索を高速化していますが、これまでのところ問題なく機能しています。データを更新する場合の要件は次のとおりです。
それを説明するために、次の例を検討してください。
既存(説明のためにnull値が含まれています):
{
"a":null,
"b":1,
"c":1,
"f":1,
"g": {
"nested": 1
}
}
これを既存のオブジェクトにマージする必要があります:
{
"b":2,
"d":null,
"e":2,
"f":null,
"g":{
"nested": 2
}
}
ご覧のとおり、いくつかのフィールドを上書きしてf
を削除しています。したがって、予想される出力は次のようになります。
{
"b": 2, //overridden
"c": 1, //kept
"e": 2, //added
"g": {
"nested": 2 //overridden
}
}
これを実現するには、次の関数を使用します。
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
$$;
私が言ったように、これは機能的な観点からはかなりうまくいきます。ただし、その機能は非常に遅いです。
一つの発見は、merged.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
$$;
これは大幅に書き直された最適化バージョンです(元のバージョンより約20倍速く、私の質問の更新バージョンと同じくらい高速ですが、要件の点ではより正確です-「本質的に空の」オブジェクトに関しては更新にいくつかの欠陥がありました。それらが何であるかについては以下):
PlPgSQLで書き直されているため、非常に「きれい」です。 ;)
まず、jsonb_each(...)
によって返されるレコードを基本的に表すマージ関数の新しいタイプが必要です。
CREATE TYPE jsonEachRecord AS (KEY text, value JSONB);
次に、jsonオブジェクトが「本質的に空」かどうかをチェックする関数があります。つまり、null値、空のオブジェクト、空の配列、またはネストされた「本質的に空」のオブジェクトのみが含まれます。
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$;
最後に、実際のマージ関数を次に示します。
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$;
あなたが見ることができるように、それはかなり醜く、おそらくまだたくさんの欠陥として、例えば配列の定数のネスト解除とjsonオブジェクトの集約。私が他の場所で読んだものから、インデックスを介して配列にアクセスするには、要素にアクセスするたびにエンジンがフロント要素から再び開始する必要があるため、おそらく最適化することもできます。
これについてa_horseを使用しています :Postgres 9.6にアップグレードして、(およびその他の理由で)新しいオプションを利用できます。
9.4で立ち往生していますが、次のように単純化すると役立つ場合があります。
CREATE OR REPLACE FUNCTION jsonb_merge2(jsonb1 JSONB, jsonb2 JSONB)
RETURNS JSONB LANGUAGE sql IMMUTABLE AS
$func$
SELECT
CASE
WHEN jsonb_typeof($1) = 'object' AND jsonb_typeof($2) = 'object' THEN
(
SELECT jsonb_object_agg(merged.key, merged.value)
FROM (
SELECT key
, CASE WHEN p1.value <> p2.value -- implies both are NOT NULL
THEN jsonb_merge2(p1.value, p2.value)
ELSE COALESCE(p2.value, p1.value) -- p2 trumps p1
END AS value
FROM jsonb_each($1) p1
FULL JOIN jsonb_each($2) p2 USING (key) -- USING helps to simplify
) AS merged
WHERE merged.value IS NOT NULL -- simpler, might help query planner
AND merged.value NOT IN ( '[]', 'null', '{}' )
)
WHEN $2 IN ( '[]', 'null', '{}' ) THEN -- just as simple as above
NULL
ELSE
$2
END
$func$;
解決策:
create or replace function jsonb_merge_recurse(orig jsonb, delta jsonb)
returns jsonb language sql as $$
select
jsonb_object_agg(
coalesce(keyOrig, keyDelta),
case
when valOrig isnull then valDelta
when valDelta isnull then valOrig
when (jsonb_typeof(valOrig) <> 'object' or jsonb_typeof(valDelta) <> 'object') then valDelta
else jsonb_merge_recurse(valOrig, valDelta)
end
)
from jsonb_each(orig) e1(keyOrig, valOrig)
full join jsonb_each(delta) e2(keyDelta, valDelta) on keyOrig = keyDelta
$$;
見つかりました ここ
使用例:2つの入力テーブルを1つのjsonドキュメント列にマージします。
与えられたテーブルA、列を持つ標準SQLテーブル。次のターゲットテーブルとそのjdoc列のように、 'docという名前の単一のJSONB列を含むテーブルB。
create unlogged table target(jdoc jsonb);
CREATE INDEX idx_gin_target ON target USING GIN ((jdoc->'keyA'),(jdoc->'keyB'),(jdoc->'jkeyC'),(doc->'jkeyD'));
insert into target select jsonb_merge_recurse(t2.doc,to_jsonb(t1)) from A t1 join B t2 on t1."keyA" = t2.doc->>'keyA' and t1."keyB"::TEXT = t2.doc->>'keyB';
結果テーブルには、テーブルBに含まれるjsonドキュメントのフィールドとテーブルAのcolumn-valuesをマージしたjsonドキュメントを含むjson列が含まれています。
Jsonb_merge_recurse関数の再帰的な機能を使用するには、それぞれがマルチレベルのjsonドキュメントを含む2つのjson列をマージすることをお勧めします。
9.4サーバーを9.5または9.6にアップグレードします。問題:新しい演算子が必要な方法で機能しないため、関数を使用するか、クエリを大幅にリファクタリングする必要があります。さらに、本番サーバーのアップグレードや再起動は、できる限り回避することを目的としています。
そうだと思います。
最初に、あなたが望むことをする集約関数を作成する必要があります、私はコピーしました this from here
CREATE AGGREGATE jsonb_object_agg(jsonb) (
SFUNC = 'jsonb_concat',
STYPE = jsonb,
INITCOND = '{}'
);
これをjsonb_strip_nulls
で使用します
SELECT jsonb_pretty(
jsonb_strip_nulls(jsonb_object_agg(d ORDER BY id))
)
FROM ( VALUES
( 1, '{ "a":null, "b":1, "c":1, "f":1, "g": { "nested": 1 } }'::jsonb ),
( 2, '{ "b":2, "d":null, "e":2, "f":null, "g":{ "nested": 2 } }' )
) AS t(id,d);
{
"b": 2,
"c": 1,
"e": 2,
"g": {
"nested": 2
}
}
何を望んだか(スクロールする必要がないため)。
{
"b": 2, //overridden
"c": 1, //kept
"e": 2, //added
"g": {
"nested": 2 //overridden
}
}
おそらく最良の解決策は、Cで記述することです。思ったよりも簡単です。
JSONBを深く追加することで同様の問題があり、ネイティブソリューションはSQLの4倍の速さで、1層の深さしかありませんでした。
これは、ディープサムの repo です。それをディープマージに適応させるのは簡単なはずです。
私はpostgres 9.6でコードを実行しましたが、9.4で動作する可能性があります