web-dev-qa-db-ja.com

PostgreSQL 9.4ディープマージjsonb値

要件と状況

現在、JSONB列を使用して、データベース内の任意の検索を高速化していますが、これまでのところ問題なく機能しています。データを更新する場合の要件は次のとおりです。

  • 新しい値があると、既存の値が上書きされます。これにはnullと配列が含まれます。
  • (ネストされた)オブジェクトはマージされます。
  • Null値、空の配列、オブジェクトは削除されます(存在する場合)。

それを説明するために、次の例を検討してください。

既存(説明のために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 ||演算子は意図したとおりに機能しません。つまり、不要な要素を保持します。クエリを複雑にする特別な削除操作を使用することもできますが、それが高速になるかどうかはわかりません。

検討されたオプション

これまでのところ、次の(満足できない)オプションを検討しました。

  • 9.4サーバーを9.5または9.6にアップグレードします。問題:新しい演算子が必要な方法で機能しないため、関数を使用するか、クエリを大幅にリファクタリングする必要があります。さらに、本番サーバーのアップグレードや再起動は、できる限り回避することを目的としています。
  • Python etc.などのスクリプト言語を使用します。ここでも、サーバーの再起動を回避する必要があるという問題があります。さらに、最初に完全なセキュリティレビューを行う必要があります。

そうは言っても、可能であれば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 
$$;  
5
Thomas

これは大幅に書き直された最適化バージョンです(元のバージョンより約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オブジェクトの集約。私が他の場所で読んだものから、インデックスを介して配列にアクセスするには、要素にアクセスするたびにエンジンがフロント要素から再び開始する必要があるため、おそらく最適化することもできます。

0
Thomas

これについて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$;
3

解決策:

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列をマージすることをお勧めします。

2
user1767316

9.4サーバーを9.5または9.6にアップグレードします。問題:新しい演算子が必要な方法で機能しないため、関数を使用するか、クエリを大幅にリファクタリングする必要があります。さらに、本番サーバーのアップグレードや再起動は、できる限り回避することを目的としています。

そうだと思います。

PostgreSQL 9.5以降の使用

最初に、あなたが望むことをする集約関数を作成する必要があります、私はコピーしました 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
  }
}
2
Evan Carroll

おそらく最良の解決策は、Cで記述することです。思ったよりも簡単です。

JSONBを深く追加することで同様の問題があり、ネイティブソリューションはSQLの4倍の速さで、1層の深さしかありませんでした。

これは、ディープサムの repo です。それをディープマージに適応させるのは簡単なはずです。

私はpostgres 9.6でコードを実行しましたが、9.4で動作する可能性があります

1