在子选择中过滤 jsonb 结果
Filter jsonb results in subselect
我正在从多个表构建一个分层 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
、title
和 pages
访问 _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.
我记得在 HAVING
子句中使用了 GROUP BY
:
GROUP BY _b.authors
HAVING _b.authors->>'name' = 'Arthur C. Clarke';
但它给了我错误:
ERROR: could not identify an equality operator for type json
为了更清楚一点:
SELECT to_json(array_agg(_b)) INTO result
FROM (
...
) _b
WHERE _b.authors->0->>'name' = 'Arthur C. Clarke';
基本上会满足我的需要,这仅在索引 0
上的作者是 Arthur C. Clarke
时才匹配。如果他与人合着了这本书并且他将排在第二位(索引 1),那么就不会出现匹配。所以我试图找到的是扫描 _b.authors
的正确语法,它恰好是一个充满作者的 JSON 数组。它只是不接受任何尝试。据我了解,@>
和 #>
仅支持 JSONB
。那么我如何在 selecting _b.authors
的任何列上针对值获得正确的语法。
更新 2
好的,再读一遍文档……我似乎没有从 Postgres 文档中了解到 JSON 和 JSONB 在函数方面存在差异,我认为是只涉及数据类型。将 to_json
替换为 to_jsonb
似乎可以通过在 where 子句中使用 @>
等运算符来解决问题。
更新 3
@ErwinBrandstetter:有道理。我还不知道 LATERAL,很高兴知道它存在。我掌握了 JSON/JSONB 的函数和运算符,现在对我来说很有意义。我不清楚的是在 WHERE
子句中找到 LIKE
的出现。
如果我需要使用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
子句是因为我想过滤完整的结果集,而不是在子 select 中间的某个地方部分过滤。所以总的来说,我想过滤掉最终结果集中作者的中间名 'C.' 或名字 'Arthur'.
的书籍
更新 4
当然在 FROM
子句中。当我想出所有的可能性时,我将不得不在最后进行性能调整,但这就是我想出的。
SELECT json_agg(_b) INTO result
FROM (
...
) _b,
jsonb_array_elements(_b.authors) AS arrauthors
WHERE arrauthors->>'name' LIKE 'Arthur %';
将给出作者姓名以 'Arthur' 开头的所有书籍。我仍然感谢对此方法的评论或更新。
推荐一个不错的tutorial on JSOn in postgresql。如果您以这种方式创建数据:
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}');
您可以 select 使用特定的 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
How would I make it possible to filter on authors? E.g. something
equivalent to WHERE _b.authors.'name' = 'Arthur C. Clarke'
.
您在使用 jsonb
和“包含”运算符 @>
更新问题时走在了正确的轨道上。最佳方法取决于您要过滤的内容和方式完全。
基本功能
你的基本功能可以更简单:
CREATE OR REPLACE FUNCTION public.book_get()
RETURNS jsonb
LANGUAGE sql STABLE 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$;
要点
- 使其SQL更简单。不需要plpgsql。
- 做到
STABLE
.
- 不要为 列 别名省略关键字
AS
。
- 使用
jsonb_agg()
如果您只想将 id
列添加为 data
的键,则有更简单的方法:
随着 Postgres 中新的 jsonb_set()
9.5:
jsonb_set(data, '{id}', to_jsonb(id))
这将添加对象或更新具有相同键的现有对象的值 - 相当于 SQL 中的 UPSERT。您还可以将操作限制为仅更新 ,请参阅手册。
我在内部子查询中使用它来添加一个 单 键。
Concatenate 两个 jsonb
值:
b.data || jsonb_build_object('id', b.id, 'authors', a.authors)
同样,左侧值中同一级别的现有键被右侧值中的键替换。我用 jsonb_build_object()
构建对象。参见:
- Return multiple columns of the same row as JSON array of objects
我在外部子查询中使用它,添加 多个 键更简单。 (并演示这两个选项。
您的原始查询将所有值都转换为 text
,这可能不是预期的。此查询保留所有 jsonb
值的原始数据类型。
测试结果
要测试函数的结果 是否存在作者:
SELECT public.book_get() @> '[{"authors": [{"name":"Arthur C. Clarke"}]}]';
您已匹配模式中的 JSON 结构。它只适用于完全匹配。
或者您可以使用 jsonb_array_elements()
就像您在上次更新中添加的那样进行部分匹配。
这两种方法都昂贵,因为您在之后构建了一个JSON来自三个完整表格的文档。
先过滤
要实际过滤 有(可能还有其他!)给定作者的书籍,请调整您的基础查询。您要求过滤...
have an author with a middle name 'C.' or a first name 'Arthur'.
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
<b>WHERE EXISTS (
SELECT 1</b> -- one of the authors matches
<b> 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</b> -- middle name 'C.'
<b> a.data->>'name' LIKE 'Arthur %')</b> -- or a first name 'Arthur'.
);
在生成结果之前,过滤至少有一位匹配作者的图书。
请注意我是如何使用 ORDER BY
作为 jsob_agg()
聚合函数的修饰符而不是子查询来对结果进行排序的,就像在前面的示例中一样。这通常 较慢 但更短。对于一个小的结果集来说已经足够好了。考虑:
- Joining arrays within group by clause
如果您的表很大并且需要快速查询,请使用 indexes!对于这个特定的查询,像这样的函数 trigram GIN 索引应该对大表产生奇迹:
CREATE INDEX author_special_idx ON author USING gin ((data->>'name') gin_trgm_ops);
详细解释/说明:
- Index for finding an element in a JSON array
我正在从多个表构建一个分层 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
、title
和 pages
访问 _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.
我记得在 HAVING
子句中使用了 GROUP BY
:
GROUP BY _b.authors
HAVING _b.authors->>'name' = 'Arthur C. Clarke';
但它给了我错误:
ERROR: could not identify an equality operator for type json
为了更清楚一点:
SELECT to_json(array_agg(_b)) INTO result
FROM (
...
) _b
WHERE _b.authors->0->>'name' = 'Arthur C. Clarke';
基本上会满足我的需要,这仅在索引 0
上的作者是 Arthur C. Clarke
时才匹配。如果他与人合着了这本书并且他将排在第二位(索引 1),那么就不会出现匹配。所以我试图找到的是扫描 _b.authors
的正确语法,它恰好是一个充满作者的 JSON 数组。它只是不接受任何尝试。据我了解,@>
和 #>
仅支持 JSONB
。那么我如何在 selecting _b.authors
的任何列上针对值获得正确的语法。
更新 2
好的,再读一遍文档……我似乎没有从 Postgres 文档中了解到 JSON 和 JSONB 在函数方面存在差异,我认为是只涉及数据类型。将 to_json
替换为 to_jsonb
似乎可以通过在 where 子句中使用 @>
等运算符来解决问题。
更新 3
@ErwinBrandstetter:有道理。我还不知道 LATERAL,很高兴知道它存在。我掌握了 JSON/JSONB 的函数和运算符,现在对我来说很有意义。我不清楚的是在 WHERE
子句中找到 LIKE
的出现。
如果我需要使用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
子句是因为我想过滤完整的结果集,而不是在子 select 中间的某个地方部分过滤。所以总的来说,我想过滤掉最终结果集中作者的中间名 'C.' 或名字 'Arthur'.
更新 4
当然在 FROM
子句中。当我想出所有的可能性时,我将不得不在最后进行性能调整,但这就是我想出的。
SELECT json_agg(_b) INTO result
FROM (
...
) _b,
jsonb_array_elements(_b.authors) AS arrauthors
WHERE arrauthors->>'name' LIKE 'Arthur %';
将给出作者姓名以 'Arthur' 开头的所有书籍。我仍然感谢对此方法的评论或更新。
推荐一个不错的tutorial on JSOn in postgresql。如果您以这种方式创建数据:
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}');
您可以 select 使用特定的 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
How would I make it possible to filter on authors? E.g. something equivalent to
WHERE _b.authors.'name' = 'Arthur C. Clarke'
.
您在使用 jsonb
和“包含”运算符 @>
更新问题时走在了正确的轨道上。最佳方法取决于您要过滤的内容和方式完全。
基本功能
你的基本功能可以更简单:
CREATE OR REPLACE FUNCTION public.book_get()
RETURNS jsonb
LANGUAGE sql STABLE 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$;
要点
- 使其SQL更简单。不需要plpgsql。
- 做到
STABLE
. - 不要为 列 别名省略关键字
AS
。 - 使用
jsonb_agg()
如果您只想将 id
列添加为 data
的键,则有更简单的方法:
随着 Postgres 中新的
jsonb_set()
9.5:jsonb_set(data, '{id}', to_jsonb(id))
这将添加对象或更新具有相同键的现有对象的值 - 相当于 SQL 中的 UPSERT。您还可以将操作限制为仅更新 ,请参阅手册。
我在内部子查询中使用它来添加一个 单 键。Concatenate 两个
jsonb
值:b.data || jsonb_build_object('id', b.id, 'authors', a.authors)
同样,左侧值中同一级别的现有键被右侧值中的键替换。我用 jsonb_build_object()
构建对象。参见:
- Return multiple columns of the same row as JSON array of objects
我在外部子查询中使用它,添加 多个 键更简单。 (并演示这两个选项。
您的原始查询将所有值都转换为 text
,这可能不是预期的。此查询保留所有 jsonb
值的原始数据类型。
测试结果
要测试函数的结果 是否存在作者:
SELECT public.book_get() @> '[{"authors": [{"name":"Arthur C. Clarke"}]}]';
您已匹配模式中的 JSON 结构。它只适用于完全匹配。
或者您可以使用 jsonb_array_elements()
就像您在上次更新中添加的那样进行部分匹配。
这两种方法都昂贵,因为您在之后构建了一个JSON来自三个完整表格的文档。
先过滤
要实际过滤 有(可能还有其他!)给定作者的书籍,请调整您的基础查询。您要求过滤...
have an author with a middle name 'C.' or a first name 'Arthur'.
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
<b>WHERE EXISTS (
SELECT 1</b> -- one of the authors matches
<b> 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</b> -- middle name 'C.'
<b> a.data->>'name' LIKE 'Arthur %')</b> -- or a first name 'Arthur'.
);
在生成结果之前,过滤至少有一位匹配作者的图书。
请注意我是如何使用 ORDER BY
作为 jsob_agg()
聚合函数的修饰符而不是子查询来对结果进行排序的,就像在前面的示例中一样。这通常 较慢 但更短。对于一个小的结果集来说已经足够好了。考虑:
- Joining arrays within group by clause
如果您的表很大并且需要快速查询,请使用 indexes!对于这个特定的查询,像这样的函数 trigram GIN 索引应该对大表产生奇迹:
CREATE INDEX author_special_idx ON author USING gin ((data->>'name') gin_trgm_ops);
详细解释/说明:
- Index for finding an element in a JSON array