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 BYwithHAVING子句:

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

基本上会做我需要的,这只有在索引上的作者0Arthur C. Clarke. 如果他与人合写了这本书并且他会排在第二位(索引 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.authors是 JSONB 数据类型)。然后我可以做

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 %';

将给出作者姓名以“亚瑟”开头的所有书籍。我仍然感谢对这种方法的评论或更新。

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;

要点

  • 使它更简单。不需要plpgsql。
  • 让它STABLE
  • 不要省略别名的关键字AS
  • 利用jsonb_agg()

  • 如果您只想将id列作为键添加到您的data中,有更简单的方法:

    1. 使用jsonb_set()Postgres 9.5中的新功能:

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

    这使用相同的键添加对象或更新现有对象的值 - 相当于 SQL 中的 UPSERT。您还可以将操作限制为 UPDATE only,请参阅手册。
    我在内部子查询中使用它来添加一个

    1. 连接两个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()您在上次更新中添加的部分匹配项。

任何一种方法都很昂贵,因为您是在从三个完整的表构建 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'.
   );

在构建结果之前过滤至少有一位匹配作者的书籍。

请注意我如何使用ORDER BYas 修饰符对jsob_agg()聚合函数而不是子查询来对结果进行排序,就像前面的示例一样。这通常更慢但更短。对于一个小的结果集来说已经足够了。考虑:

如果您的表很大并且您需要快速查询,请使用索引!对于这个特定的查询,像这样的函数 trigram GIN 索引应该可以为大表创造奇迹:

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

详细解释/说明:

于 2016-04-08T01:08:46.187 回答
1

推荐一个关于 postgresql 中 JSon的不错的教程。如果您以这种方式创建数据:

CREATE TABLE json_test (
  id serial primary key,
  data jsonb
);
INSERT INTO json_test (data) VALUES 
  ('{"id":1,"title":"2001: A Space Odyssey","pages":432,"authors":[{"id":1,"fullname":"Arthur C. Clarke"}]}'),
  ('{"id":2,"title":"The City And The City","pages":300,"authors":[{"id":2,"fullname":"China Miéville"}]}'),
  ('{"id":3,"title":"Unknown Book","pages":143,"authors":null}');

您可以使用特定的 id 进行选择

SELECT * FROM json_test
WHERE data @> '{"id":2}';

或者在子数组中查找特定名称:

SELECT * FROM json_test
WHERE data -> 'authors' @> '[{"fullname": "Arthur C. Clarke"}]'

或查找超过 200 页的书:

SELECT * FROM json_test
WHERE (data -> 'pages')::text::int > 200
于 2016-04-07T14:37:09.557 回答