4

複数のテーブルから階層的な JSON 結果を作成しています。これらは単なる例ですが、このデモンストレーションの目的を理解するのに十分なはずです。

CREATE TABLE book (
    id INTEGER PRIMARY KEY NOT NULL,
    data JSONB
);
CREATE TABLE author (
    id INTEGER PRIMARY KEY NOT NULL,
    data JSONB
);
CREATE TABLE book_author (
    id INTEGER PRIMARY KEY NOT NULL,
    author_id INTEGER,
    book_id INTEGER
);
CREATE UNIQUE INDEX pk_unique ON book_author (author_id, book_id);

テストデータ:

INSERT INTO book (id, data) VALUES
  (1, '{"pages": 432, "title": "2001: A Space Odyssey"}')
, (2, '{"pages": 300, "title": "The City And The City"}')
, (3, '{"pages": 143, "title": "Unknown Book"}');

INSERT INTO author (id, data) VALUES
  (1, '{"age": 90, "name": "Arthur C. Clarke"}')
, (2, '{"age": 43, "name": "China Miéville"}');

INSERT INTO book_author (id, author_id, book_id) VALUES
  (1, 1, 1)
, (2, 1, 2);

次の関数を作成しました。

CREATE OR REPLACE FUNCTION public.book_get()
  RETURNS json AS
$BODY$
DECLARE
    result json;
BEGIN
      SELECT to_json(array_agg(_b)) INTO result
      FROM (
        SELECT
          book.id id,
          book.data->>'title' title,
          book.data->>'pages' pages,
          (
            SELECT to_json(array_agg(_a))
            FROM (
              SELECT
                author.id id,
                author.data->>'name' "name",
                author.data->>'age' age
              FROM
                author, book_author ba
              WHERE
                ba.author_id = author.id AND 
                  ba.book_id = book.id
              ORDER BY id
            ) _a
          ) authors
        FROM
          book
        ORDER BY id ASC
      ) _b;
        
    RETURN result;
END;
$BODY$ LANGUAGE plpgsql VOLATILE;

関数の実行book_get

SELECT book_get();

次の結果が生成されます

[
   {
      "id":1,
      "title":"2001: A Space Odyssey",
      "pages":432,
      "authors":[
         {
            "id":1,
            "name":"Arthur C. Clarke",
            "age":90
         }
      ]
   },
   {
      "id":2,
      "title":"The City And The City",
      "pages":300,
      "authors":[
         {
            "id":2,
            "name":"China Miéville",
            "age":43
         }
      ]
   },
   {
      "id":3,
      "title":"Unknown Book",
      "pages":143,
      "authors":null
   }
]

WHEREこれで、句を使用してデータをフィルタリングできるようになりました。

SELECT to_json(array_agg(_b)) INTO result
FROM (
 ...
) _b
-- give me the book with id 1
WHERE _b.id = 1;
-- or give me all titles with the occurrence of 'City' anywhere
WHERE _b.title LIKE '%City%';
-- or has more than 200 pages
WHERE _b.pages > 200;

でフィルタリングできるようにするにはどうすればよいauthorsですか? たとえば、 に相当するものWHERE _b.authors.'name' = 'Arthur C. Clarke'

authorsどのタイプになるか全くわかりません。それとも?それはまだレコード(配列)ですか?もうJSONですか?にアクセスできるからだと思いますidが、アクセスすることはそれほど問題ではありませんか?titlepages_b.authors

アクセス_b.authorsすると私にERROR: missing FROM-clause entry for table "authors"

JSON 演算子でアクセスすると、_b.authors->>..または_b->authors->>..

operator does not exist: record -> json
Hint: No operator matches the given name and argument type(s). You might need to add explicit type casts.

句で使用GROUP BYしたことを覚えています:HAVING

GROUP BY _b.authors
HAVING _b.authors->>'name' = 'Arthur C. Clarke';

しかし、それは私にエラーを与えます:

エラー: タイプ json の等価演算子を識別できませんでした

もう少し明確にするために:

      SELECT to_json(array_agg(_b)) INTO result
      FROM (
        ...
      ) _b
      WHERE _b.authors->0->>'name' = 'Arthur C. Clarke';

基本的に必要なことを行います。これは、インデックスの作成者が である場合にのみ一致し0ますArthur C. Clarke。彼が本を共同執筆し、彼が 2 位 (インデックス 1) だった場合、一致はありませんでした。だから私が見つけようとしているのは、_b.authorsたまたま著者で満たされた JSON 配列であるスキャンの正しい構文です。どんな試みも受け入れません。私が理解している限りでは@>#>のみがサポートされていJSONBます。_b.authorsでは、値に対して任意の列を選択する際に正しい構文を取得するにはどうすればよいですか。

更新 2

ドキュメントをもう一度読んでください... Postgresのドキュメントから、関数に関してJSONとJSONBに違いがあるという部分を取得できなかったようです。データ型のみに関するものだと思いました。where 句で etc のような演算子を使用すると、と置き換えるto_jsonとうまくいくようです。to_jsonb@>

アップデート 3

@ErwinBrandstetter: 理にかなっています。LATERAL は私にはまだ知られていませんでしたが、存在することを知ってよかったです。私は JSON/JSONB の関数と演算子に慣れてきました。今ではとても理にかなっています。私には明確ではないのはLIKE、たとえばWHERE節での出現を見つけることです。

jsonb_array_elements配列内のオブジェクトのネストを解除するために使用する必要がある場合(最後のWHERE節では、の内容がb.authorsJSONB データ型であるため)。私はそれからすることができました

SELECT * FROM jsonb_array_elements('[
  {"age": 90, "name": "the Arthur C. Clarke"},
  {"age": 43, "name": "China Miéville"},
  {"age": null, "name": "Erwin the Brandstetter"}
]'::jsonb) author
WHERE 
  author->>'name' LIKE '%the%';

望ましい結果が得られます。

1: {"age": 90, "name": "the Arthur C. Clarke"}
2: {"age": null, "name": "Erwin the Brandstetter"}

しかし、私の例の最後の(最後の)WHERE節でこれを達成するためのアプローチは何ですか? WHERE結果の完全なセットをフィルタリングし、サブ選択の途中で部分的にフィルタリングしたくないため、最後の句を指摘します。したがって、一般的には、著者のミドルネームが「C」である本を最終的な結果セットから除外したいと考えています。または名前「アーサー」。

更新 4

FROMもちろん節で。すべての可能性を見つけたら、最後にパフォーマンスの微調整を行う必要がありますが、これが私が思いついたものです。

SELECT json_agg(_b) INTO result
FROM (
...
) _b,
jsonb_array_elements(_b.authors) AS arrauthors
WHERE arrauthors->>'name' LIKE 'Arthur %';

'Arthur' で始まる著者名を持つすべての本を提供します。このアプローチへのコメントや更新に感謝します。

4

2 に答える 2

3

著者をフィルタリングできるようにするにはどうすればよいですか? たとえば、 に相当するものWHERE _b.authors.'name' = 'Arthur C. Clarke'

jsonbおよび「含む」演算子を使用した質問の更新で正しい軌道に乗っています@>最適なアプローチは、正確に何をどのようにフィルタリングするかによって異なります。

基本機能

あなたの基本的な機能はより簡単にすることができます:

CREATE OR REPLACE FUNCTION public.book_get()
  RETURNS jsonb AS
$func$
SELECT jsonb_agg(books)
FROM  (
   SELECT b.data || jsonb_build_object('id', b.id, 'authors', a.authors) AS books
   FROM   book b
   LEFT   JOIN (  -- LEFT JOIN to include books without authors
      SELECT book_id, jsonb_agg(data_plus) AS authors
      FROM  (
         SELECT ba.book_id, jsonb_set(a.data, '{id}', to_jsonb(a.id)) AS data_plus
         FROM   book_author ba
         JOIN   author a ON a.id = ba.author_id
         ORDER  BY ba.book_id, ba.author_id
         ) a0
      GROUP  BY 1
      ) a ON a.book_id = b.id
   ORDER  BY b.id
   ) b0
$func$ LANGUAGE sql STABLE;

主なポイント

  • SQL をよりシンプルにします。plpgsql は必要ありません。
  • それを作るSTABLE
  • エイリアスのキーワードASを省略しないでください。
  • 使用するjsonb_agg()

  • idに列をキーとして追加したいだけの場合dataは、より簡単な方法があります。

    1. jsonb_set()Postgres 9.5の新機能:

      jsonb_set(data, '{id}', to_jsonb(id))
      

    これにより、オブジェクトが追加されるか、既存のオブジェクトの値が同じキーで更新されます。これは、SQL の UPSERT に相当します。操作を UPDATEのみに制限することもできます。マニュアルを参照してください。
    これを内側のサブクエリで使用して、単一のキーを追加します。

    1. 2 つのjsonb値を連結します。

      b.data || jsonb_build_object('id', b.id, 'authors', a.authors) 
      

    ここでも、左の値の同じレベルにある既存のキーは、右の値のキーに置き換えられます。でオブジェクトを構築しますjsonb_build_object()。この関連する回答の詳細:

    これを外側のサブクエリで使用すると、複数のキーを簡単に追加できます。(そして、両方のオプションを示すために。

元のクエリはすべての値を に変換しましたがtext、これはおそらく意図したものではありません。このクエリは、すべてのjsonb値に対して元のデータ型を保持します。

テスト結果

作成者の存在について関数の結果をテストするには、次のようにします。

SELECT public.book_get() @> '[{"authors": [{"name":"Arthur C. Clarke"}]}]';

パターンの JSON 構造と一致しています。そして、それは完全一致に対してのみ機能します。またはjsonb_array_elements()、部分一致の最後の更新で追加したように使用できます。

3 つのテーブル全体から JSON ドキュメントを構築したにテストするため、いずれの方法もコストがかかります。

最初にフィルタ

特定の著者 (おそらく他にもある!) を持つ本を実際にフィルタリングするには、基礎となるクエリを調整します。あなたは本をフィルタリングするように頼みます...

ミドルネーム「C」の著者がいます。または名前「アーサー」。

SELECT jsonb_agg(b.data || jsonb_build_object('id', b.id, 'authors', a.authors) ORDER BY b.id) AS books
FROM   book b
     , LATERAL (  -- CROSS JOIN since we filter before the join
   SELECT jsonb_agg(jsonb_set(a.data, '{id}', to_jsonb(a.id)) ORDER BY a.id) AS authors
   FROM   book_author ba 
   JOIN   author a ON a.id = ba.author_id
   WHERE  ba.book_id = b.id
   ) a
WHERE  EXISTS (
   SELECT 1                                 -- one of the authors matches
   FROM   book_author ba
   JOIN   author a ON a.id = ba.author_id
   WHERE  ba.book_id = b.id
   AND   (a.data->>'name' LIKE '% C. %' OR  -- middle name 'C.'
          a.data->>'name' LIKE 'Arthur %')  -- or a first name 'Arthur'.
   );

結果を作成する前に、一致する著者が少なくとも 1 人いる書籍をフィルタリングします。

前の例のように結果をソートするために、サブクエリの代わりに集計関数ORDER BYの修飾子として使用する方法に注意してください。jsob_agg()これは通常は遅くなりますが、短くなります。そして、小さな結果セットには十分です。検討:

テーブルが大きく、クエリを高速に実行する必要がある場合は、インデックスを使用してください。この特定のクエリの場合、このような関数 trigram GIN インデックスは、大きなテーブルに対して驚異的に機能するはずです。

CREATE INDEX author_special_idx ON author USING gin ((data->>'name') gin_trgm_ops);

詳細な説明/手順:

于 2016-04-08T01:08:46.187 に答える