現在位置: ホーム / OSSブログ / PostgreSQL 9.5のJSONBの部分更新

PostgreSQL 9.5のJSONBの部分更新

PostgreSQL 9.5 よりJSONB型の機能が拡張され、部分更新が可能になる予定です。 開発版の内容より概要についてご紹介します。

こんにちは。渡辺です。

PostgreSQL 9.2 から導入された JSON 型はリリースの度に機能が強化されています。PostgreSQL 9.5 では JSONB 型の機能が拡張され、部分更新が可能になる予定です。PostgreSQL 9.5 は執筆時点では開発版です。リリース時点では多少機能などに変更があるかも知れない点に留意をお願いします。

■ JSONB の部分更新

JSON 型は JSON データをテキストとして保存します。JSONB 型は JSON データをハッシュとして保存します。このため、仕組み的には JSONB 型は部分更新が可能でしたが、サポートされていませんでした。JSONB の部分更新をサポートするため、オペレータと関数が強化されます。

** オペレータ

  • jsonb || jsonb (追加または上書き)
  • jsonb - text / int (キーまたは配列の削除)
  • jsonb - text[] / int (パスのキーまたは配列の削除)

** 関数

  • jsonb_replace (要素の更新)
  • jsonb_pretty (JSON データの整形表示)

jsonb_pretty 関数は JSON / JSONB データ型を整形 (インデント) して表示する関数ですが、それ以外は JSONB の部分更新をサポートする機能です。

■ 部分更新の例

言葉で説明するより実際の動作を見た方が解りやすいと思います。PostgreSQL Wiki より実行例を紹介します。

■■ || オペレータの動作

# SELECT '{"name": "Joe", "age": 30}'::jsonb || '{"town": "London"}'::jsonb;
                    ?column?                   
 ----------------------------------------------
  {"age": 30, "name": "Joe", "town": "London"}
 (1 row)

|| オペレータにより、"town" : "London" が追加されています。

# SELECT '{"town": "Dataville", "population": 4096}'::jsonb || '{"population": 8192}'::jsonb;
                  ?column?                  
 -------------------------------------------
  {"town": "Dataville", "population": 8192}
 (1 row)

"population" は既に存在するキーなので、値が更新されています。

# SELECT '{"name": "Jane", "contact": {"phone": "01234 567890", "mobile": "07890 123456"}}'::jsonb || '{"contact": {"fax": "01987 654321"}}'::jsonb;
                        ?column?                       
 ------------------------------------------------------
  {"name": "Jane", "contact": {"fax": "01987 654321"}}
 (1 row)

キーの更新はトップレベルだけ行われます。ネストしたデータの場合、更新データは上書きされます。この例では "contact" に "fax" が設定されています。"phone" と "mobile" が消えている点に注目してください。

■■ - オペレータの動作

# SELECT '{"name": "James", "email": "james@localhost"}'::jsonb - 'email';
       ?column?      
  -------------------
   {"name": "James"}
  (1 row)

文字列で "email" を指定しているので、"email" キーが削除されます。

# SELECT '["red","green","blue"]'::jsonb - 1;
     ?column?     
 -----------------
  ["red", "blue"]
 (1 row)

整数の場合、配列のインデックスを削除した事になります。PostgreSQL の配列先頭のインデックスは 0 なので "-1" で二番目の "green" が削除されています。

# SELECT '{"name": "James", "contact": {"phone": "01234 567890", "fax": "01987 543210"}}'::jsonb - '{contact,fax}'::text[];
                         ?column?                         
 ---------------------------------------------------------
  {"name": "James", "contact": {"phone": "01234 567890"}}
 (1 row)

テキスト配列の場合、指定したキーの要素から削除できます。この例では "contact" の中の "fax" が削除されています。

# SELECT '{"name": "James", "aliases": ["Jamie","The Jamester","J Man"]}'::jsonb - '{aliases,1}'::text[];
                      ?column?                     
 --------------------------------------------------
  {"name": "James", "aliases": ["Jamie", "J Man"]}
 (1 row)

インデックス(整数)を指定した場合、インデックスで削除されます。この例では 2 番目の要素が削除されています。

■ jsonb_replace 関数

# SELECT jsonb_replace('{"name": "James", "contact": {"phone": "01234 567890", "fax": "01987 543210"}}'::jsonb, '{contact,phone}', '"07900 112233"'::jsonb);
                                  jsonb_replace                                  
 --------------------------------------------------------------------------------
  {"name": "James", "contact": {"fax": "01987 543210", "phone": "07900 112233"}}
 (1 row)

オペレータでは子要素の更新ができませんが、jsonb_replace 関数では可能です。この例では "contact" の中の "phone" の値を更新しています。

■ jsonb_pretty 関数

# SELECT jsonb_pretty(jsonb_replace('{"name": "James", "contact": {"phone": "01234 567890", "fax": "01987 543210"}}'::jsonb, '{contact,phone}', '"07900 112233"'::jsonb));
           jsonb_pretty           
 ---------------------------------
  {                              +
      "name": "James",           +
      "contact": {               +
          "fax": "01987 543210", +
          "phone": "07900 112233"+
      }                          +
  }
 (1 row)

部分更新ではありませんが、新しく追加された関数です。上の例のようにインデントされ整形された出力を行います。

■ アプリケーションのメリット

残念ながら JSON データを取得し、アプリケーションで一部を更新するような場合はあまりメリットはありません。普通の JSON API は API として特定のキーを更新する機能がない為、取得した JSON データの一部を更新する場合でも、全体を渡す必要があります。アプリケーション側ではオブジェクト / 配列として JSON データが処理されることがほとんどです。キーを追加したり、キーの値を変更することは容易です。

JSONB の一部を更新するストアードプロシージャの場合、パラメータとして渡した JSONB 型の更新をより簡単に記述できるようになります。ストアードプロシージャを記述する場合、これらの JSONB 型の拡張は簡潔なプログラムの記述にとても役立ちます。

■ ベンチマーク

簡易なベンチマークスクリプトで、アプリケーションでどの程度の速度的なメリットがあるのか計測してみます。

ベンチマークスクリプト

<?php
const NUM_DATA=10000;
const DUMMY_BYTES=3600;
$db = pg_connect('host=localhost dbname=yohgaki') || die('Failed to connect db');


$dummy = base64_encode(file_get_contents('/dev/urandom', false, null, -1, DUMMY_BYTES));
$dummy = substr($dummy, 0, DUMMY_BYTES);
// PostgreSQLはTOAST機能によりテキストを圧縮しているので、以下のようなダミーデータでは
// 適切なベンチマークが取得できない
//$dummy = str_repeat('a', DUMMY_BYTES);

// Clean up
@pg_query('DROP TABLE jsonb_test;');
$ddl = <<< EOD
 CREATE TABLE IF NOT EXISTS jsonb_test (
  id INT8 PRIMARY KEY,
  data JSONB NOT NULL
 );
EOD;
pg_query($ddl);

// Prepare test data
pg_prepare('JSONB_TEST1', 'INSERT INTO jsonb_test (id, data) VALUES ($1, $2);');
$json = '{"dummy":"'.$dummy.'", "array": ["a","b","c"]}';
for ($i = 0; $i < NUM_DATA; $i++) {
 pg_execute('JSONB_TEST1', array($i, $json));
}


// 従来通り全体を更新
pg_prepare('UPDATE1', 'UPDATE jsonb_test SET data = $2 WHERE id = $1;');
$json = '{"dummy":"'.$dummy.'", "new": "abc", "array": ["a","b","c"]}';
$start = microtime(true);
for ($i = 0; $i < NUM_DATA; $i++) {
 pg_execute('UPDATE1', array($i, $json));
}
echo 'TIME: '. (microtime(true)-$start) .PHP_EOL;


// Clean up
@pg_query('DROP TABLE jsonb_test;');
$ddl = <<< EOD
 CREATE TABLE IF NOT EXISTS jsonb_test (
  id INT8 PRIMARY KEY,
  data JSONB NOT NULL
 );
EOD;
pg_query($ddl);

// Prepare test data
pg_prepare('JSONB_TEST2', 'INSERT INTO jsonb_test (id, data) VALUES ($1, $2);');
$json = '{"dummy":"'.$dummy.'", "array": ["a","b","c"]}';
for ($i = 0; $i < NUM_DATA; $i++) {
 pg_execute('JSONB_TEST2', array($i, $json));
}


// 部分更新
pg_prepare('UPDATE2', 'UPDATE jsonb_test SET data = data || $2 WHERE id = $1;');
$json = '{"new":"xyz"}';
$start = microtime(true);
for ($i = 0; $i < NUM_DATA; $i++) {
 pg_execute('UPDATE2', array($i, $json));
}
echo 'TIME: '. (microtime(true)-$start) .PHP_EOL;

出力

$ php -d dispaly_errors=Off benchmark.php 
TIME: 1.1106388568878
TIME: 0.9919102191925

データベースは PostgreSQL 9.5 の開発版、ベンチマークスクリプトは PHP 5.6 で実行しています。

高速化されていることが確認できました。しかし、JSON データの量が増えれば差が開くのではなく件数に応じて増えるようです。部分更新 (追加) に

data = data || JSONB 型

を利用しています。この形式だと data のデータ量に応じて高速化するようなコードになっていないようです。残念ながら劇的に高速になるわけではありませんでした。しかし、全体を従来通り更新するよりは高速化されています。リリースまでに何らかの最適化が行われるかも知れません。

■ まとめ

JSONB の部分更新機能はアプリケーション側のメリットは大きくありませんが、PostgreSQL 側で JSON データを処理しようとした場合に大きなメリットがあります。従来は煩雑なコードが必要でした。アプリケーションで JSON を利用する場面が増えるとサーバー側での処理でも JSON データを柔軟に扱える機能が必要になります。PostgreSQL のストアードプロシージャで JSON を処理しようとした場合のコードが簡略化できることはシステム開発の自由度を高めます。

アプリケーション側では高速化のメリットが大きくない、とは言っても確実に以前よりは速く動作します。