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     
$$;

問題と質問

私が言ったように、これは機能的な観点から非常にうまく機能します。しかし、その機能は非常に遅いです。

1つの発見は、 merged.value の条件がクエリを遅くするということでした。それを削除すると実行時間が約70%短縮されますが、明らかに結果は必要なものではありません。

それでは、どのようにしてjsonbオブジェクトの高速で深いマージを達成できるでしょうか。

Postgres 9.5の || 演算子は意図したとおりには動作しません。つまり、不要な要素が残ります。クエリを複雑にするような特別なremove操作を使うこともできますが、それが速くなるかどうかはわかりません。

検討中のオプション

これまでのところ、我々は以下の(不満足な)選択肢を検討した。

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

そうは言っても、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 
$$;  
3
ru de
@a_horse_with_no_nameは実際には問題です。DBサーバーのダウンタイムは、それらの更新が数ヶ月に1回しか適用されず、多くの調整を必要とするという大きな影響を及ぼします(私はその設定には賛成ではありませんが、これが問題です)。データベースをアップグレードするには、さらに調整が必要になり、現在の開発が大幅に遅くなります。
追加された 著者 Ezekiel Templin,
@ a_horse_with_no_name申し訳ありませんが、おそらくそれを誤解を招くような方法で表現しました。つまり、アップグレードを必要とする機能にすぐに頼ってもプロダクションサーバーをアップグレードすることはできないので、ソフトウェアの更新を妨げることになります。その間は、必要な修正や機能を回避する必要があります。より早く配達されるようになる(今ではもっと明確になってほしい)。
追加された 著者 Ezekiel Templin,
9.6(または9.5)は少なくとも jsonb_strip_nulls()を持つでしょう。
追加された 著者 a_horse_with_no_name,
さらに本番用サーバーをアップグレードまたは再起動することは、できる限り回避することを目的としています」 - では、どのようにして必要なセキュリティ修正を適用しますか?
追加された 著者 a_horse_with_no_name,
(今話題にならないように)サーバをアップグレードすることが開発を遅らせることになるとは思わない。確かに、開発、テスト、そして運用に同じデータベースサーバーを使わないのですか?
追加された 著者 a_horse_with_no_name,

4 答え

I am with a_horse on this: Upgrade to Postgres 9.6 to have new options at your disposal (and for other reasons).

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
追加された
THX、私たちのインフラストラクチャーの人々を説得しようとするでしょう:)その間に私たちは現在の状況を改善しようとします - そして私はより速い解決策を見つけたかもしれません(私の最新情報を見てください)。それは合理的に見えますか?
追加された 著者 Ezekiel Templin,
いいね。この関数はPg> = 9.5でどのように見えるでしょうか。
追加された 著者 ikaruss,

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

そうだと思います。

PostgreSQL 9.5+を使用する

First we need to create an aggregate function that does what you want, I copied this from here

CREATE AGGREGATE jsonb_object_agg(jsonb) (  
  SFUNC = 'jsonb_concat',
  STYPE = jsonb,
  INITCOND = '{}'
);

Now we use it with 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
  }
}
1
追加された
面白そうですが、ローカルのPostgres 9.6インスタンスを起動して、再度実行したら、これをチェックします。共有してくれてありがとう。
追加された 著者 Ezekiel Templin,

おそらく最善の解決策はC言語でそれを書くことです、それはそれが聞こえるよりもっと簡単です。

私はJSONBを深く追加するという同様の問題を抱えていました。ネイティブソリューションはSQLよりも4倍高速で、深さはわずか1層です。

これはリポジトリです。ディープマージに適応するのは簡単なはずです。

私はpostgres 9.6でコードを実行しましたが、9.4でも動作するかもしれません。

1
追加された
リンクをありがとう、私はそれを調べます。残念ながら、私の質問で述べたように、新しいネイティブライブラリを追加するためにデータベースサーバーを再起動する必要がある(または再起動しなくても動作するでしょうか)ということは、チームリーダーには受け入れられません。
追加された 著者 Ezekiel Templin,
@トーマス、ネイティブ拡張子を追加するためにデータベースを再起動する必要はありません。
追加された 著者 vikas,

これは大きく書き直されたが最適化されたバージョン(元のバージョンの約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
追加された
JavaScript - 日本のコミュニティ
JavaScript - 日本のコミュニティ
2 参加者の

日本人コミュニティのjavascript