sql >> データベース >  >> RDS >> PostgreSQL

PostgreSQLでのJSONBの使用:PostgreSQLでJSONデータを効果的に保存およびインデックス付けする方法

    JSONはJavaScriptObjectNotationの略です。これは、RFC 7159で詳述されているキー/値のペアと配列にデータを編成するオープンスタンダード形式です。JSONは、データの交換、ドキュメントの保存、非構造化データなどにWebサービスで使用される最も一般的な形式です。 JSONデータを効果的に保存してPostgreSQLにインデックスを付ける方法に関するヒントとテクニックを紹介します。

    このトピックの詳細については、PostgresConfと連携したPostgreSQLとMongoDBのJSONデータの操作に関するウェビナーを確認してください。SlideShareページも確認してください。スライドをダウンロードします。

    JSONをPostgreSQLに保存する理由

    リレーショナルデータベースが非構造化データを気にする必要があるのはなぜですか?それが役立つシナリオがいくつかあることがわかりました。

    1. スキームの柔軟性

      JSON形式を使用してデータを保存する主な理由の1つは、スキーマの柔軟性です。データをJSONに保存すると、スキーマが流動的で頻繁に変更される場合に役立ちます。各キーを列として保存すると、DML操作が頻繁に発生します。たとえば、イベントトラッキング、分析、タグなどのデータセットが大きい場合、これは困難になる可能性があります。注:特定のキーが常に存在する場合ドキュメントでは、ファーストクラスの列として保存するのが理にかなっている場合があります。このアプローチの詳細については、以下の「JSONパターンとアンチパターン」のセクションで説明します。

    2. ネストされたオブジェクト

      データセットにネストされたオブジェクト(シングルレベルまたはマルチレベル)がある場合、データを列または複数のテーブルに非正規化するよりも、JSONで処理する方が簡単な場合があります。

    3. 外部データソースとの同期

      多くの場合、外部システムはデータをJSONとして提供しているため、データがシステムの他の部分に取り込まれる前の一時的なストアである可能性があります。たとえば、Stripeトランザクション。

    PostgreSQLでのJSONサポートのタイムライン

    PostgreSQLでのJSONサポートは9.2で導入され、今後のすべてのリリースで着実に改善されています。

    • Wave 1:PostgreSQL 9.2(2012)でJSONデータ型のサポートが追加されました

      9.2のJSONデータベースはかなり制限されていました(そしておそらくその時点で誇大宣伝されていました)–基本的にはJSON検証がスローされた栄光の文字列です。着信JSONを検証してデータベースに保存すると便利です。詳細については、以下をご覧ください。

    • Wave 2:PostgreSQL 9.4(2014)でJSONBデータ型のサポートが追加されました

      JSONBは、誰に尋ねるかによって、「JSONBinary」または「JSONbetter」の略です。これは、JSONを格納するための分解されたバイナリ形式です。 JSONBは、JSONデータのインデックス作成をサポートしており、JSONデータの解析とクエリを非常に効率的に実行できます。ほとんどの場合、PostgreSQLでJSONを使用するときは、JSONBを使用する必要があります。

    • Wave 3:PostgreSQL 12(2019)は、SQL/JSON標準およびJSONPATHクエリのサポートを追加しました

      JSONPathは、強力なJSONクエリエンジンをPostgreSQLにもたらします。

    JSONとJSONBをいつ使用する必要がありますか?

    ほとんどの場合、JSONBを使用する必要があります。ただし、JSONの方がうまく機能する特定のケースがいくつかあります。

    • JSONは、元のフォーマット(別名空白)とキーの順序を保持します。
    • JSONは重複するキーを保持します。
    • JSONBよりもJSONの方が取り込みが高速ですが、さらに処理を行うと、JSONBの処理速度が速くなります。

    たとえば、JSONログを取り込むだけで、クエリをまったく行わない場合は、JSONの方が適している可能性があります。このブログでは、PostgreSQLでのJSONサポートについて言及する場合、今後はJSONBについて言及します。

    PostgreSQLでのJSONBの使用:PostgreSQLでJSONデータを効果的に保存およびインデックス付けする方法Click To Tweet

    JSONBパターンとアンチパターン

    PostgreSQLがJSONBを強力にサポートしている場合、なぜ列が必要になったのですか? JSONB BLOBを使用してテーブルを作成し、以下のスキーマのようなすべての列を削除してみませんか。

    CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
    

    結局のところ、列はデータを操作するための最も効率的な手法です。 JSONBストレージには、従来の列と比較していくつかの欠点があります。

    • PostreSQLはJSONB列の列統計を保存しません

      PostgreSQLは、テーブルの各列の値の分布に関する統計を保持します。最も一般的な値(MCV)、NULLエントリ、分布のヒストグラムです。このデータに基づいて、PostgreSQLクエリプランナーはクエリに使用するプランについて賢明な決定を下します。この時点で、PostgreSQLはJSONB列またはキーの統計を保存しません。これにより、ネストされたループ結合とハッシュ結合などの選択が不適切になる場合があります。この詳細な例は、このブログ投稿–PostgreSQLスキーマでJSONBを回避する場合に提供されています。

    • JSONBストレージを使用すると、ストレージのフットプリントが大きくなります

      JSONBストレージは、JSONのキー名を重複排除しません。これにより、WiredTigerのMongoDB BSONや従来の列ストレージと比較して、ストレージのフットプリントが大幅に大きくなる可能性があります。約1,000万行のデータを格納する以下のJSONBモデル​​を使用して簡単なテストを実行しました。結果は次のとおりです。これは、JSONBのキーが圧縮なしでそのまま格納されたMongoDBMMAPV1ストレージモデルに似ています。長期的な修正の1つは、キー名を繰り返し保存するのではなく、キー名をテーブルレベルのディクショナリに移動し、このディクショナリを参照することです。それまでは、回避策として、よりわかりやすい名前ではなく、よりコンパクトな名前(unixスタイル)を使用することが考えられます。たとえば、特定のキーのインスタンスを数百万個保存する場合は、ストレージに関して「publisherName」ではなく「pb」という名前を付ける方が適切です。

    PostgreSQLでJSONBを活用する最も効率的な方法は、列とJSONBを組み合わせることです。キーがJSONBブロブに頻繁に表示される場合は、列として保存することをお勧めします。 JSONBを「キャッチオール」として使用して、より安定したフィールドに従来の列を活用しながら、スキーマの可変部分を処理します。

    JSONBデータ構造

    JSONBとMongoDBBSONはどちらも基本的にツリー構造であり、マルチレベルノードを使用して解析されたJSONBデータを保存します。 MongoDBBSONの構造は非常に似ています。

    画像ソース

    JSONB&TOAST

    ストレージに関するもう1つの重要な考慮事項は、JSONBがTOAST(特大属性ストレージ手法)とどのように相互作用するかです。通常、列のサイズがTOAST_TUPLE_THRESHOLD(デフォルトは2kb)を超えると、PostgreSQLはデータを圧縮して2kbに収めようとします。それが機能しない場合、データはオフラインストレージに移動されます。これは、データの「トースト」と呼ばれるものです。データがフェッチされると、逆のプロセス「deTOASTting」が発生する必要があります。 TOASTストレージ戦略を制御することもできます:

    • 拡張 –オフラインでの保存と圧縮を可能にします(pglzを使用)。これがデフォルトのオプションです。
    • 外部 –オフラインストレージは可能ですが、圧縮はできません。

    TOASTの圧縮または解凍が原因で遅延が発生している場合、1つのオプションは、列ストレージを「EXTENDED」に事前に設定することです。詳細については、このPostgreSQLドキュメントを参照してください。

    JSONBの演算子と関数

    PostgreSQLは、JSONBで動作するさまざまな演算子を提供します。ドキュメントから:

    オペレーター 説明
    -> JSON配列要素を取得します(ゼロからインデックス付けされ、負の整数は最後からカウントされます)
    -> キーでJSONオブジェクトフィールドを取得
    ->> JSON配列要素をテキストとして取得
    ->> JSONオブジェクトフィールドをテキストとして取得
    #> 指定されたパスでJSONオブジェクトを取得します
    #>> 指定されたパスにあるJSONオブジェクトをテキストとして取得
    @> 左側のJSON値には、最上位の右側のJSONパス/値エントリが含まれていますか?
    <@ 左側のJSONパス/値エントリは右側のJSON値の最上位に含まれていますか?
    文字列を実行します JSON値内にトップレベルキーとして存在しますか?
    ?| これらの配列の文字列のいずれかを実行します トップレベルのキーとして存在しますか?
    ?& これらすべての配列の文字列を実行します トップレベルのキーとして存在しますか?
    || 2つのjsonb値を新しいjsonb値に連結します
    - キーと値のペアまたは文字列を削除します 左のオペランドからの要素。キーと値のペアは、キー値に基づいて照合されます。
    - 複数のキーと値のペアまたは文字列を削除します 左のオペランドからの要素。キーと値のペアは、キー値に基づいて照合されます。
    - 指定されたインデックスを持つ配列要素を削除します(負の整数は最後から数えます)。トップレベルのコンテナが配列でない場合はエラーをスローします。
    #- 指定されたパスを持つフィールドまたは要素を削除します(JSON配列の場合、負の整数は最後から数えます)
    @? JSONパスは指定されたJSON値のアイテムを返しますか?
    @@ 指定されたJSON値のJSONパス述語チェックの結果を返します。結果の最初の項目のみが考慮されます。結果がブール値でない場合は、nullが返されます。

    PostgreSQLには、JSONBデータを操作するためのさまざまな作成関数と処理関数も用意されています。

    JSONBインデックス

    JSONBには、JSONデータにインデックスを付けるためのさまざまなオプションが用意されています。大まかに言えば、GIN、BTREE、HASHの3種類のインデックスを掘り下げていきます。すべてのインデックスタイプがすべての演算子クラスをサポートしているわけではないため、使用する予定の演算子とクエリのタイプに基づいてインデックスを設計するための計画が必要です。

    GINインデックス

    GINは「一般化転置インデックス」の略です。ドキュメントから:

    「GINは、インデックス付けされるアイテムが複合値であり、インデックスによって処理されるクエリが要素を検索する必要がある場合を処理するために設計されています複合アイテム内に表示される値。たとえば、アイテムはドキュメントであり、クエリは特定の単語を含むドキュメントの検索である可能性があります。」

    GINは2つの演算子クラスをサポートしています:

    • jsonb_ops(デフォルト)–?、?|、?&、@>、@@、@? [JSONB要素の各キーと値にインデックスを付けます]
    • jsonb_pathops – @>、@@、@? [JSONB要素の値のみにインデックスを付ける]
    CREATE INDEX datagin ON books USING gin (data);
    

    存在演算子(?、?|、?&)

    これらの演算子を使用して、JSONBに最上位のキーが存在するかどうかを確認できます。データJSONB列にGINインデックスを作成しましょう。たとえば、点字で利用できるすべての本を検索します。 JSONは次のようになります:

    "{"tags": {"nk594127": {"ik71786": "iv678771"}}, "braille": false, "keywords": ["abc", "kef", "keh"], "hardcover": true, "publisher": "EfgdxUdvB0", "criticrating": 1}
    
    demo=# select * from books where data ? 'braille';
    id | author | isbn | rating | data
    
    ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
    ------------------
    1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
    criticrating": 4}
    .....
    
    demo=# explain analyze select * from books where data ? 'braille';
    QUERY PLAN
    ---------------------------------------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158) (actual time=0.033..0.039 rows=15 loops=1)
    Recheck Cond: (data ? 'braille'::text)
    Heap Blocks: exact=2
    -> Bitmap Index Scan on datagin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.022..0.022 rows=15 loops=1)
    Index Cond: (data ? 'braille'::text)
    Planning Time: 0.102 ms
    Execution Time: 0.067 ms
    (7 rows)
    

    Explainの出力からわかるように、作成したGINインデックスが検索に使用されています。点字またはハードカバーの本を見つけたい場合はどうすればよいですか?

    demo=# explain analyze select * from books where data ?| array['braille','hardcover'];
    QUERY PLAN
    ---------------------------------------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.029..0.035 rows=15 loops=1)
    Recheck Cond: (data ?| '{braille,hardcover}'::text[])
    Heap Blocks: exact=2
    -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.023..0.023 rows=15 loops=1)
    Index Cond: (data ?| '{braille,hardcover}'::text[])
    Planning Time: 0.138 ms
    Execution Time: 0.057 ms
    (7 rows)
    

    GINインデックスは、「トップレベル」キーでのみ「存在」演算子をサポートします。キーがトップレベルにない場合、インデックスは使用されません。順次スキャンが行われます:

    demo=# select * from books where data->'tags' ? 'nk455671';
    id | author | isbn | rating | data
    
    ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
    ------------------
    1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
    criticrating": 4}
    685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0}
    (2 rows)
    
    demo=# explain analyze select * from books where data->'tags' ? 'nk455671';
    QUERY PLAN
    ----------------------------------------------------------------------------------------------------------
    Seq Scan on books (cost=0.00..38807.29 rows=1000 width=158) (actual time=0.018..270.641 rows=2 loops=1)
    Filter: ((data -> 'tags'::text) ? 'nk455671'::text)
    Rows Removed by Filter: 1000017
    Planning Time: 0.078 ms
    Execution Time: 270.728 ms
    (5 rows)
    

    ネストされたドキュメントに存在するかどうかを確認する方法は、「式インデックス」を使用することです。データのインデックスを作成しましょう->タグ:

    CREATE INDEX datatagsgin ON books USING gin (data->'tags');
    
    demo=# select * from books where data->'tags' ? 'nk455671';
    id | author | isbn | rating | data
    
    ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
    ------------------
    1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
    criticrating": 4}
    685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0}
    (2 rows)
    
    demo=# explain analyze select * from books where data->'tags' ? 'nk455671';
    QUERY PLAN
    ------------------------------------------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=12.75..1007.75 rows=1000 width=158) (actual time=0.031..0.035 rows=2 loops=1)
    Recheck Cond: ((data ->'tags'::text) ? 'nk455671'::text)
    Heap Blocks: exact=2
    -> Bitmap Index Scan on datatagsgin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.021..0.021 rows=2 loops=1)
    Index Cond: ((data ->'tags'::text) ? 'nk455671'::text)
    Planning Time: 0.098 ms
    Execution Time: 0.061 ms
    (7 rows)
    

    注:ここでの別の方法は、@>演算子を使用することです:

    select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;
    

    ただし、これは値がオブジェクトの場合にのみ機能します。そのため、値がオブジェクトなのかプリミティブ値なのかわからない場合は、誤った結果になる可能性があります。

    パス演算子@>、<@

    「パス」演算子は、JSONBデータのマルチレベルクエリに使用できます。 ?と同じように使いましょう。上記の演算子:

    select * from books where data @> '{"braille":true}'::jsonb;
    demo=# explain analyze select * from books where data @> '{"braille":true}'::jsonb;
    QUERY PLAN
    ---------------------------------------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.040..0.048 rows=6 loops=1)
    Recheck Cond: (data @> '{"braille": true}'::jsonb)
    Rows Removed by Index Recheck: 9
    Heap Blocks: exact=2
    -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.030..0.030 rows=15 loops=1)
    Index Cond: (data @> '{"braille": true}'::jsonb)
    Planning Time: 0.100 ms
    Execution Time: 0.076 ms
    (8 rows)
    

    パス演算子は、ネストされたオブジェクトまたはトップレベルオブジェクトのクエリをサポートします:

    demo=# select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb;
    id | author | isbn | rating | data
    -----+-----------------+------------+--------+-------------------------------------------------------------------------------------
    346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3}
    (1 row)
    
    demo=# explain analyze select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb;
    QUERY PLAN
    --------------------------------------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.491..0.492 rows=1 loops=1)
    Recheck Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb)
    Heap Blocks: exact=1
    -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.092..0.092 rows=1 loops=1)
    Index Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb)
    Planning Time: 0.090 ms
    Execution Time: 0.523 ms
    

    クエリはマルチレベルにすることもできます:

    demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;
    id | author | isbn | rating | data
    
    ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
    ------------------
    1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
    criticrating": 4}
    (1 row)
    

    GINインデックス「pathops」演算子クラス

    GINは、GINインデックスのサイズを縮小するための「pathops」オプションもサポートしています。 pathopsオプションを使用する場合、演算子のサポートは「@>」のみであるため、クエリに注意する必要があります。ドキュメントから:

    「jsonb_opsとjsonb_path_ops GINインデックスの技術的な違いは、前者はデータ内のキーと値ごとに独立したインデックスアイテムを作成するのに対し、後者はデータの各値」

    次のようにGINpathopsインデックスを作成できます:

    CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);
    

    100万冊の本の小さなデータセットでは、pathops GINインデックスが小さいことがわかります。データセットでテストして、節約額を理解する必要があります。

    public | dataginpathops | index | sgpostgres | books | 67 MB |
    public | datatagsgin | index | sgpostgres | books | 84 MB |
    

    pathopsインデックスを使用して以前からクエリを再実行しましょう:

    demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;
    id | author | isbn | rating | data
    
    ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------
    ------------------
    1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", "
    criticrating": 4}
    (1 row)
    
    demo=# explain select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;
    QUERY PLAN
    -----------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158)
    Recheck Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb)
    -> Bitmap Index Scan on dataginpathops (cost=0.00..12.50 rows=1000 width=0)
    Index Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb)
    (4 rows)
    

    ただし、前述のように、「pathops」オプションは、デフォルトの演算子クラスがサポートするすべてのシナリオをサポートしているわけではありません。 「pathops」GINインデックスを使用すると、これらすべてのクエリでGINインデックスを利用できなくなります。要約すると、インデックスは小さくなりますが、サポートされるユースケースはより限定されます。

    select * from books where data ? 'tags'; => Sequential scan
    select * from books where data @> '{"tags" :{}}'; => Sequential scan
    select * from books where data @> '{"tags" :{"k7888":{}}}' => Sequential scan
    

    Bツリーインデックス

    Bツリーインデックスは、リレーショナルデータベースで最も一般的なインデックスタイプです。ただし、JSONB列全体にBツリーインデックスを使用してインデックスを作成する場合、有用な演算子は「=」、<、<=、>、>=のみです。基本的に、これはオブジェクト全体の比較にのみ使用でき、ユースケースは非常に限られています。

    より一般的なシナリオは、Bツリーの「式インデックス」を使用することです。入門書については、ここを参照してください–式の索引。 Bツリー式インデックスは、一般的な比較演算子「=」、「<」、「>」、「> =」、「<=」をサポートできます。ご存知かもしれませんが、GINインデックスはこれらの演算子をサポートしていません。データ->批評>4のすべての本を取得する場合を考えてみましょう。したがって、次のようなクエリを作成します。

    demo=# select * from books where data->'criticrating' > 4;
    ERROR: operator does not exist: jsonb >= integer
    LINE 1: select * from books where data->'criticrating'  >= 4;
    ^
    HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
    

    「->」演算子はJSONBタイプを返すため、これは機能しません。したがって、次のようなものを使用する必要があります:

    demo=# select * from books where (data->'criticrating')::int4 > 4;
    

    PostgreSQL 11より前のバージョンを使用している場合は、さらに醜くなります。最初にテキストとしてクエリを実行してから、整数にキャストする必要があります:

    demo=# select * from books where (data->'criticrating')::int4 > 4;
    

    式インデックスの場合、インデックスはクエリ式と完全に一致する必要があります。したがって、インデックスは次のようになります。

    demo=# CREATE INDEX criticrating ON books USING BTREE (((data->'criticrating')::int4));
    CREATE INDEX
    
    demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3;
    QUERY PLAN
    ----------------------------------------------------------------------------------------------------------------------------------
    Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1)
    Index Cond: (((data -> 'criticrating'::text))::integer = 3)
    Planning Time: 0.103 ms
    Execution Time: 79.019 ms
    (4 rows)
    
    demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3;
    QUERY PLAN
    ----------------------------------------------------------------------------------------------------------------------------------
    Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1)
    Index Cond: (((data -> 'criticrating'::text))::integer = 3)
    Planning Time: 0.103 ms
    Execution Time: 79.019 ms
    (4 rows)
    1
    From above we can see that the BTREE index is being used as expected.
    

    ハッシュインデックス

    "="演算子のみに関心がある場合は、ハッシュインデックスが重要になります。たとえば、本で特定のタグを探している場合を考えてみましょう。インデックスを作成する要素は、最上位の要素にすることも、深くネストすることもできます。

    例:タグ->パブリッシャー=XlekfkLOtL

    CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
    

    ハッシュインデックスも、BツリーまたはGINインデックスよりもサイズが小さい傾向があります。もちろん、これは最終的にはデータセットによって異なります。

    demo=# select * from books where data->'publisher' = 'XlekfkLOtL'
    demo-# ;
    id | author | isbn | rating | data
    -----+-----------------+------------+--------+-------------------------------------------------------------------------------------
    346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3}
    (1 row)
    
    demo=# explain analyze select * from books where data->'publisher' = 'XlekfkLOtL';
    QUERY PLAN
    -----------------------------------------------------------------------------------------------------------------------
    Index Scan using publisherhash on books (cost=0.00..2.02 rows=1 width=158) (actual time=0.016..0.017 rows=1 loops=1)
    Index Cond: ((data -> 'publisher'::text) = 'XlekfkLOtL'::text)
    Planning Time: 0.080 ms
    Execution Time: 0.035 ms
    (4 rows)
    

    特別な言及:GINトリグラムインデックス

    PostgreSQLは、トリグラムインデックスを使用した文字列照合をサポートしています。トリグラムインデックスは、テキストをトリグラムに分割することで機能します。トライグラムは基本的に3文字のシーケンスに分割された単語です。詳細については、ドキュメントを参照してください。 GINインデックスは、JSONBのデータにインデックスを付けるために使用できる「gin_trgm_ops」クラスをサポートします。 You can choose to use expression indexes to build the trigram index on a particular column.

    CREATE EXTENSION pg_trgm;
    CREATE INDEX publisher ON books USING GIN ((data->'publisher') gin_trgm_ops);
    
    demo=# select * from books where data->'publisher' LIKE '%I0UB%';
     id |     author      |    isbn    | rating |                                      data
    ----+-----------------+------------+--------+---------------------------------------------------------------------------------
      4 | KiEk3xjqvTpmZeS | EYqXO9Nwmm |      0 | {"tags": {"nk3": {"ik1": "iv1"}}, "publisher": "MI0UBqZJDt", "criticrating": 1}
    (1 row)
    

    As you can see in the query above, we can search for any arbitrary string occurring at any potion. Unlike the B-tree indexes, we are not restricted to left anchored expressions.

    demo=# explain analyze select * from books where data->'publisher' LIKE '%I0UB%';
                                                         QUERY PLAN
    --------------------------------------------------------------------------------------------------------------------
     Bitmap Heap Scan on books  (cost=9.78..111.28 rows=100 width=158) (actual time=0.033..0.033 rows=1 loops=1)
       Recheck Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text)
       Heap Blocks: exact=1
       ->  Bitmap Index Scan on publisher  (cost=0.00..9.75 rows=100 width=0) (actual time=0.025..0.025 rows=1 loops=1)
             Index Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text)
     Planning Time: 0.213 ms
     Execution Time: 0.058 ms
    (7 rows)
    

    Special Mention:GIN Array Indexes

    JSONB has great built-in support for indexing arrays. Let's consider an example of indexing an array of strings using a GIN index in the case when our JSONB data contains a "keyword" element and we would like to find rows with particular keywords:

    {"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2}
    
    CREATE INDEX keywords ON books USING GIN ((data->'keywords') jsonb_path_ops);
    
    demo=# select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb;
       id    |     author      |    isbn    | rating |                                                               data
    ---------+-----------------+------------+--------+-----------------------------------------------------------------------------------------------------------------------------------
     1000003 | zEG406sLKQ2IU8O | viPdlu3DZm |      4 | {"tags": {"nk263020": {"ik203820": "iv817928"}}, "keywords": ["abc", "kef", "keh"], "publisher": "7NClevxuTM", "criticrating": 2}
     1000004 | GCe9NypHYKDH4rD | so6TQDYzZ3 |      4 | {"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2}
    (2 rows)
    
    demo=# explain analyze select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb;
                                                         QUERY PLAN
    ---------------------------------------------------------------------------------------------------------------------
     Bitmap Heap Scan on books  (cost=54.75..1049.75 rows=1000 width=158) (actual time=0.026..0.028 rows=2 loops=1)
       Recheck Cond: ((data -> 'keywords'::text) @> '["abc", "keh"]'::jsonb)
       Heap Blocks: exact=1
       ->  Bitmap Index Scan on keywords  (cost=0.00..54.50 rows=1000 width=0) (actual time=0.014..0.014 rows=2 loops=1)
             Index Cond: ((data -> 'keywords'::text) @&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; '["abc", "keh"]'::jsonb)
     Planning Time: 0.131 ms
     Execution Time: 0.063 ms
    (7 rows)
    

    The order of the items in the array on the right does not matter. For example, the following query would return the same result as the previous:

    demo=# explain analyze select * from books where data->'keywords' @> '["keh","abc"]'::jsonb;
    

    All elements in the right side array of the containment operator need to be present - basically like an "AND" operator. If you want "OR" behavior, you can construct it in the WHERE clause:

    demo=# explain analyze select * from books where (data->'keywords' @> '["abc"]'::jsonb OR data->'keywords' @> '["keh"]'::jsonb);
    

    More details on the behavior of the containment operators with arrays can be found in the documentation.

    SQL/JSON &JSONPath

    SQL standard added support for JSON  in SQL - SQL/JSON Standard-2016. With the PostgreSQL 12/13 releases, PostgreSQL has one of the best implementations of the SQL/JSON standard. For more details refer to the PostgreSQL 12 announcement.

    One of the core features of SQL/JSON is support for the JSONPath language to query JSONB data. JSONPath allows you to specify an expression (using a syntax similar to the property access notation in Javascript) to query your JSONB data. This makes it simple and intuitive, but is also very powerful to query your JSONB data. Think of  JSONPath as the logical equivalent of XPath for XML.

    .key Returns an object member with the specified key.
    [*] Wildcard array element accessor that returns all array elements.
    .* Wildcard member accessor that returns the values of all members located at the top level of the current object.
    .** Recursive wildcard member accessor that processes all levels of the JSON hierarchy of the current object and returns all the member values, regardless of their nesting level.

    Refer to JSONPath documentation for the full list of operators. JSONPath also supports a variety of filter expressions.

    JSONPath Functions

    PostgreSQL 12 provides several functions to use JSONPath to query your JSONB data.ドキュメントから:

    • jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON 値。
    • jsonb_path_match - Returns the result of JSONB path predicate check for the specified JSONB value. Only the first item of the result is taken into account. If the result is not Boolean, then null is returned.
    • jsonb_path_query - Gets all JSONB items returned by JSONB path for the specified JSONB value. There are also a couple of other variants of this function that handle arrays of objects.

    Let's start with a simple query - finding books by publisher:

    demo=# select * from books where data @@ '$.publisher == "ktjKEZ1tvq"';
    id | author | isbn | rating | data
    ---------+-----------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------
    1000001 | 4RNsovI2haTgU7l | GwSoX67gLS | 2 | {"tags": {"nk542369": {"ik55240": "iv305393"}}, "keywords": ["abc", "def", "geh"], "publisher": "ktjKEZ1tvq", "criticrating": 0}
    (1 row)
    
    demo=# explain analyze select * from books where data @@ '$.publisher == "ktjKEZ1tvq"';
    QUERY PLAN
    --------------------------------------------------------------------------------------------------------------------
    Bitmap Heap Scan on books (cost=21.75..1014.25 rows=1000 width=158) (actual time=0.123..0.124 rows=1 loops=1)
    Recheck Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath)
    Heap Blocks: exact=1
    -> Bitmap Index Scan on datagin (cost=0.00..21.50 rows=1000 width=0) (actual time=0.110..0.110 rows=1 loops=1)
    Index Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath)
    Planning Time: 0.137 ms
    Execution Time: 0.194 ms
    (7 rows)
    

    You can rewrite this expression as a JSONPath filter:

    demo=# select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")');
    

    You can also use very complex query expressions. For example, let's select books where print style =hardcover and price =100:

    select * from books where jsonb_path_exists(data, '$.prints[*] ?(@.style=="hc" &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; @.price == 100)');
    

    However, index support for JSONPath is very limited at this point - this makes it dangerous to use JSONPath in the where clause. JSONPath support for indexes will be improved in subsequent releases.

    demo=# explain analyze select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")');
    QUERY PLAN
    ------------------------------------------------------------------------------------------------------------
    Seq Scan on books (cost=0.00..36307.24 rows=333340 width=158) (actual time=0.019..480.268 rows=1 loops=1)
    Filter: jsonb_path_exists(data, '$."publisher"?(@ == "ktjKEZ1tvq")'::jsonpath, '{}'::jsonb, false)
    Rows Removed by Filter: 1000028
    Planning Time: 0.095 ms
    Execution Time: 480.348 ms
    (5 rows)
    

    Projecting Partial JSON

    Another great use case for JSONPath is projecting partial JSONB from the row that matches. Consider the following sample JSONB:

    demo=# select jsonb_pretty(data) from books where id = 1000029;
    jsonb_pretty
    -----------------------------------
    {
     "tags": {
     "nk678947": {
          "ik159670": "iv32358
     }
     },
     "prints": [
         {
             "price": 100,
             "style": "hc"
         },
         {
            "price": 50,
            "style": "pb"
         }
     ],
     "braille": false,
     "keywords": [
         "abc",
         "kef",
         "keh"
     ],
     "hardcover": true,
     "publisher": "ppc3YXL8kK",
     "criticrating": 3
    }
    

    Select only the publisher field:

    demo=# select jsonb_path_query(data, '$.publisher') from books where id = 1000029;
    jsonb_path_query
    ------------------
    "ppc3YXL8kK"
    (1 row)
    

    Select the prints field (which is an array of objects):

    demo=# select jsonb_path_query(data, '$.prints') from books where id = 1000029;
    jsonb_path_query
    ---------------------------------------------------------------
    [{"price": 100, "style": "hc"}, {"price": 50, "style": "pb"}]
    (1 row)
    

    Select the first element in the array prints:

    demo=# select jsonb_path_query(data, '$.prints[0]') from books where id = 1000029;
    jsonb_path_query
    -------------------------------
    {"price": 100, "style": "hc"}
    (1 row)
    

    Select the last element in the array prints:

    demo=# select jsonb_path_query(data, '$.prints[$.size()]') from books where id = 1000029;
    jsonb_path_query
    ------------------------------
    {"price": 50, "style": "pb"}
    (1 row)
    

    Select only the hardcover prints from the array:

    demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc")') from books where id = 1000029;
           jsonb_path_query
    -------------------------------
     {"price": 100, "style": "hc"}
    (1 row)
    

    We can also chain the filters:

    demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc") ?(@.price ==100)') from books where id = 1000029;
    jsonb_path_query
    -------------------------------
    {"price": 100, "style": "hc"}
    (1 row)
    

    In summary, PostgreSQL provides a powerful and versatile platform to store and process JSON data. There are several gotcha's that you need to be aware of, but we are optimistic that it will be fixed in future releases.

    More tips for you

    Which Is the Best PostgreSQL GUI?

    PostgreSQL graphical user interface (GUI) tools help these open source database users to manage, manipulate, and visualize their data. In this post, we discuss the top 5 GUI tools for administering your PostgreSQL deployments. Learn more

    Managing High Availability in PostgreSQL

    Managing high availability in your PostgreSQL hosting is very important to ensuring your clusters maintain exceptional uptime and strong operational performance so your data is always available to your application. Learn more

    PostgreSQL Connection Pooling:Part 1 – Pros &Cons

    In modern apps, clients open a lot of connections. Developers are discouraged from holding a database connection while other operations take place. “Open a connection as late as possible, close as soon as possible”. Learn more


    1. コレーションがわかりませんか? (Mysql、RDBMS、文字セット)

    2. DBMS_ASSERTを使用したOracleSQLインジェクションブロック

    3. パフォーマンスの神話:クラスター化インデックスと非クラスター化インデックス

    4. MySQLでピボットテーブルを作成する方法