現在位置: ホーム / OSSブログ / PostgreSQL 9.4で強化されたJSON型

PostgreSQL 9.4で強化されたJSON型

2014年12月18日にPostgreSQL 9.4がリリースされました。 サイオスではリリース前にβ版よりPostgreSQL 9.4に搭載が予定されている機能をご紹介しましたが、概ねご紹介した通りの内容でリリースが行われています。今回はRelease Notesの概要から振り返りをしてみたいと思います。

PostgreSQL 9.4 Release Notesは下記を参照ください。
http://www.postgresql.org/docs/devel/static/release-9-4.html

今回追加された主な機能としては下記の内容になります。今回の目玉はJSONB型に対応したところでしょうか。JSONBについては旧"OSSよろずブログ"掲載しておりましたので、改めてこちらのブログにも転載をします。

Major enhancements in PostgreSQL 9.4 include:

    Add jsonb, a more capable and efficient data type for storing JSON data

    Add new SQL command ALTER SYSTEM for changing postgresql.conf configuration file entries

    Reduce lock strength for some ALTER TABLE commands

    Allow materialized views to be refreshed without blocking concurrent reads

    Add support for logical decoding of WAL data, to allow database changes to be streamed out in a customizable format

    Allow background worker processes to be dynamically registered, started and terminated

PsotgreSQL9.4で更に強化されるJSON型

これまでにもJSON型については何度か解説してきました。現在のJSON型も非常に強力ですが、PostgreSQL9.4ではJSON型が更に強化されます。どのように強化されたのか解説します。

■ 現在のJSON型と新しいJSONB型

現在のJSON型はテキストデータとして保存されています。JSON型に保存されたデータにアクセスするには、テキストとして保存されたJSONデータをアンシリアライズしてからアクセスする必要がありました。新しいJSONB型はJSONデータをPostgreSQLネイティブのデータ型として保存します。PostgreSQLネイティブのデータ型として保存されるため、JSONデータのテキストをアンシリアライズせずにアクセスできるようになります。つまりより効率的に保存・アクセスできるようになります。

JSON型とJSONB型はほぼ同じ動作を行います。特別な理由が無い場合はJSON型よりJSONB型を利用する事が推奨されます。動作が最も異なる部分は重複キーが存在する場合です。JSON型では重複キーもテキストとして保存されますがJSONB型では最後に定義された値が有効になり、重複キーは保存されません。JSONBとJSONの動作の違いをまとめると以下のようになります。

  • 重複キーは保存されなくなり、最後の値が保存される(関数などでJSON型にアクセスした場合も結果的には同じ)
  • スペース文字が保存されなくなる
  • キーの順序が保存されなくなる
  • JSONデータに要素が含まれるか確認可能になる

多くのアプリケーションではキーの順序が変わる事に問題はないと思われますが、単純にJSONデータ型の中身を順番に出力しているアプリケーションでは注意が必要です。

■■ 新しいJSONB型のJSONデータ型とPostgreSQLデータ型のマッピング

RFC-7159/JSONデータ型   PostgreSQLデータ型
  string                text >
  number                numeric
  boolean               boolean
  null                  unknown

■ 基本的な動作

JSONB型とJSON型で大きな違いはありません。

基本的な動作

  username@127 ~=# SELECT '5'::jsonb;
  jsonb
  -------
  5
  (1 行)
  時間: 1.044 ms
  username@127 ~=# SELECT '5'::json;
  json
  ------
  5
  (1 行)
  時間: 0.356 ms
  username@127 ~=# SELECT '[1, 2, "foo", null]'::jsonb;
  jsonb
  ---------------------
  [1, 2, "foo", null]
  (1 行)
  時間: 0.223 ms
  username@127 ~=# SELECT '[1, 2, "foo", null]'::json;
  json
  ---------------------
  [1, 2, "foo", null]
  (1 行)
  時間: 0.193 ms
  username@127 ~=# SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::jsonb;
    jsonb
  --------------------------------------------------
  {"bar": "baz", "active": false, "balance": 7.77}
  (1 行)
  時間: 0.242 ms
  username@127 ~=# SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::json;
  json
  -------------------------------------------------
  {"bar": "baz", "balance": 7.77, "active":false}
  (1 行)
  時間: 0.194 ms

最後の例ではキーの順序が保持されていない事が分かります。この他にも、同じキーを保つ場合、最後の要素が優先されます。

重複キーを保つデータ

  username@127 ~=# SELECT '{"bar": "baz", "bar": "foo", "balance": 7.77, "active":false}'::jsonb;
  jsonb
  --------------------------------------------------
  {"bar": "foo", "active": false, "balance": 7.77}
  (1 行)
  時間: 0.255 ms
  username@127 ~=# SELECT '{"bar": "baz", "bar": "foo", "balance": 7.77, "active":false}'::json;
  json
  ---------------------------------------------------------------
  {"bar": "baz", "bar": "foo", "balance": 7.77, "active":false}
  (1 行)
  時間: 0.241 ms

JSONB型では空白は無視されます。

空白付きのデータ

  username@127 ~=# SELECT '     5'::jsonb;
  jsonb
  -------
  5
  (1 行)
  時間: 0.197 ms
  usernme@127 ~=# SELECT '     5'::json;
  json
  --------
  5
  (1 行)
  時間: 0.196 ms

■ "@>" オペレータ

JSONB型は"@>"オペレータで要素が含まれるか確認できます。この機能はJSON型にはありません。

"@>"オペレータの利用例

  username@127 ~=# SELECT '{"product": "PostgreSQL", "version": 9.4, "jsonb":true}'::jsonb @> '{"version":9.4}'::jsonb;
  ?column?
  ----------
  t
  (1 行)
  時間: 0.678 ms

JSONデータを操作する場合、特定の要素が含まれるか確認したい場合は多くあります。"@>"オペレータは便利なオペレータです。配列のチェックよりオブジェクトのチェックに向いています。

■ GINインデックス

JSONB型はGIN(汎用転置インデックス)をサポートしています。 "@>", "?", "?&"および"?|"オペレータをサポートしています。 このため、関数インデックスを利用しなくてもJSONデータを高速に検索可能です。

GINインデックスの使い方

  CREATE INDEX index_name ON table_has_jsonb USING GIN (jsonb_column);

オプションを指定しない場合、デフォルトのopclassが利用されます。opclass以外にjsonb_hash_opsもサポートされています。

GINインデックスの使い方

  CREATE INDEX index_name ON table_has_jsonb USING GIN (jsonb_column jsonb_hash_ops);

jsonb_hash_opsを指定すると"@>"オペレータのみが利用できます。Webアプリなどでtag検索を実装する場合に非常に便利です。(レストランなどを検索する場合に、目的、ジャンル、設備、サービスなどタグとして保存し、適合するレコードを抽出する検索を高速に行えます)

Bツリーとハッシュインデックス

JSONB型のデータはPostgreSQLの複合型と同じように比較されます。

比較のルール
 −  Object > Array > Boolean > Number > String > Null
 −  Nの要素を持つObject > N-1の要素を持つobject
 −  Nの要素を持つArray > N-1の要素を持つArray

各要素が比較されるので、比較にはそれなりの時間が必要であることに注意が必要です。

Bツリーインデックスの場合、比較に等価と不等を判別する必要があります。この為、JSONBデータ型は"="オペレータのみでなく、""も利用できます。オブジェクトの場合、要素の数が同じ場合

key1, value1, key2, value2 ...

の順番に比較され、key、valueのどれかが"不等"と判断された時点で比較が終了します。

PostgreSQLは短いキーを先に保存するため、比較の結果が予測しづらい結果となる事があります。

username@127 test=# SELECT '{ "aa": 1, "c": 1}'::jsonb > '{"b": 1, "d": 1}'::jsonb;
 ?column? 
----------
 t
(1 行)

時間: 0.247 ms
username@127 test=# SELECT '{ "z": 1, "c": 1}'::jsonb > '{"b": 1, "d": 1}'::jsonb;
 ?column? 
----------
 t
(1 行)

時間: 0.236 ms
username@127 test=# SELECT '{ "a": 1, "z": 1}'::jsonb > '{"b": 1, "d": 1}'::jsonb;
 ?column? 
----------
 f
(1 行)

時間: 0.262 ms
username@127 test=# SELECT '{ "x": 1, "z": 1}'::jsonb > '{"b": 1, "d": 1}'::jsonb;
 ?column? 
----------
 t
(1 行)

時間: 0.236 ms

補足:

'{ "aa": 1, "c": 1}'::jsonb > '{"b": 1, "d": 1}'::jsonb

は"c"と"b"のキーが最初に比較され、"c">"b"なので"t"(真)が返ります。

配列比較も結果しづらい結果かも知れません。オブジェクトと同様に要素数が同じ場合に

value1, value2 ...

の順番に比較され、valueのどれかが不等と判断された時点で比較が終了します。

username@127 test=# SELECT '[1, 2, 3]'::jsonb > '[2, 3, 4]'::jsonb;
 ?column? 
----------
 f
(1 行)

時間: 0.255 ms
username@127 test=# SELECT '[5, 2, 3]'::jsonb > '[2, 3, 4]'::jsonb;
 ?column? 
----------
 t
(1 行)

時間: 0.232 ms
username@127 test=# SELECT '[5, 2, 3]'::jsonb > '[5, 3, 4]'::jsonb;
 ?column? 
----------
 f
(1 行)

時間: 0.220 ms
username@127 test=# SELECT '[2, 4, 3]'::jsonb > '[2, 3, 4]'::jsonb;
 ?column? 
----------
 t
(1 行)

時間: 0.229 ms

関数インデックス

従来のJSON型でもJSON関数を利用して検索を高速化できました。JSONB型ではGINインデックスがサポートされていますが、GINインデックスに比べるとデフォルトのBツリーインデックスやハッシュインデックスの方が高速です。ニーズに応じて関数インデックスも利用すると良いでしょう。

JSONオペレータ

JSONデータ型用のJSON関数はJSONB型をサポートしています。JSON型と同様にJSONB型でも利用可能です。

オペレータ   オペランド型  説明                                      例
->        int        JSON配列要素の取得                          '[1,2,3]'::json->2
->        text        JSONオブジェクト要素の取得                  '{"a":1,"b":2}'::json->'b'
->>        int        JSON配列要素をテキストとして取得              '[1,2,3]'::json->>2
->>        text        JSONオブジェクト要素をテキストとして取得      '{"a":1,"b":2}'::json->>'b'
#>        text[]    パスで指定したJSONオブジェクトの取得          '{"a":[1,2,3],"b":[4,5,6]}'::json#>'{a,2}'
#>>        text[]    パスで指定したJSONオブジェクトをテキストとして取得 '{"a":[1,2,3],"b":[4,5,6]}'::json#>>'{a,2}'

JSONB型に追加されたオペレータ

=          jsonb       等価比較                     '[1,2,3]'::jsonb = '[1,2,3]'::jsonb
@>        jsonb   包含比較                                  '{"a":1, "b":2}'::jsonb @> '{"b":2}'::jsonb

JSON関数

JSONデータ型用のJSON関数はJSONB型をサポートしています。JSON型と同様にJSONB型でも利用可能です。多数存在するので個別の解説は省略しますが、JSONデータ型用のjson_*()関数とJSONBデータ型用のjsonb_*()関数が用意されています。

簡単なベンチマーク

JSONB型はPostgreSQLネイティブのデータ型に変換されて保存されるので効率良くアクセスできます。どの程度高速化されたのか簡単なベンチマークで確認します。

環境
Fedora 20 x86_64
PostgreSQL 9.4 (gitのマスターブランチ: 2014/3/27)
PHP 5.5.10
JSONB/JSONのみの性能を計測したいのでpostgresql.confでfsync=offに設定しています。

ベンチマークスクリプト(benchmark.php)

出力結果の例

[username@dev 201403]$ php benchmark.php 
Inserting test data. Takes a while...
Inserted row counrs:
Array
(
    [json_count] => 10000
)
Array
(
    [jsonb_count] => 10000
)
Simple JSON select: 1.1431810855865
Simple JSONB select: 1.122545003891
Simple SELECT: 1.8051454800766% faster
JSON "->": 1.2706871032715
JSONB "->": 1.1284899711609
"->" Operator: 11.190570183997% faster

まとめ

JSONBで惜しい部分は要素の順序が維持されない部分です。そかし、その代わりにより高速にデータを検索できるようになりました。多くの言語では、JSONデータは順序を持たないハッシュとして実装されています。まれに順序を持たない事が問題になる場合もありますが、これは許容可能なトレードオフでしょう。

簡易なベンチマーク結果からはオペレータを使った要素の取得は1割ほど高速であることが分かりました。挿入やレコードの取得には大きな違いはありません。PostgreSQL 9.4から包含をチェックするオペレータが追加された事は、開発者にとって大きな意味があると思います。デフォルトでGINインデックスが利用できるようになった事も大きなメリットです。

■ 参考URL
http://www.postgresql.org/docs/devel/static/functions-json.html
http://www.postgresql.org/docs/devel/static/datatype-json.html
http://tools.ietf.org/html/rfc7159

※本ブログのJSONBに関する記事は下記の内容を転載したものとなります。
http://sios-oss.blogspot.jp/2014/08/postgresql-94json.html