按 JSON 数组中的匹配数查询和排序
Query and order by number of matches in JSON array
在 Postgres 9.4 和 Rails 的 jsonb
列中使用 JSON 数组,我可以设置一个 return 包含 [=28= 的所有行的范围]any 数组中的元素传递给范围方法 - 像这样:
scope :tagged, ->(tags) {
where(["data->'tags' ?| ARRAY[:tags]", { tags: tags }])
}
我还想根据数组中匹配元素的数量对结果进行排序。
我很感激我可能需要走出 ActiveRecord 的范围才能做到这一点,所以普通的 Postgres SQL 答案也很有帮助,但是如果它可以包含在 ActiveRecord 中那么它可以加分可链接范围。
根据要求,这里有一个示例 table。 (实际模式要复杂得多,但这就是我所关心的。)
id | data
----+-----------------------------------
1 | {"tags": ["foo", "bar", "baz"]}
2 | {"tags": ["bish", "bash", "baz"]}
3 |
4 | {"tags": ["foo", "foo", "foo"]}
用例是根据标签查找相关内容。更多匹配标签更相关,因此结果应按匹配数量排序。在 Ruby 中,我有一个像这样的简单方法:
Page.tagged(['foo', 'bish', 'bash', 'baz']).all
应该return的页面顺序如下:2, 1, 4
.
您的数组仅包含 ,嵌套文档会更复杂。
查询
在 LATERAL
连接中使用 jsonb_array_elements_text()
取消嵌套找到的行的 JSON 数组并计算匹配项:
SELECT *
FROM (
SELECT *
FROM tbl
WHERE data->'tags' ?| ARRAY['foo', 'bar']
) t
, LATERAL (
SELECT count(*) AS ct
FROM jsonb_array_elements_text(t.data->'tags') a(elem)
WHERE elem = ANY (ARRAY['foo', 'bar']) -- same array parameter
) ct
ORDER BY ct.ct DESC; -- more expressions to break ties?
替代INSTERSECT
。这是我们可以利用此基本 SQL 功能的罕见情况之一:
SELECT *
FROM (
SELECT *
FROM tbl
WHERE data->'tags' ?| '{foo, bar}'::text[] -- alt. syntax w. array
) t
, LATERAL (
SELECT count(*) AS ct
FROM (
SELECT * FROM jsonb_array_elements_text(t.data->'tags')
INTERSECT ALL
SELECT * FROM unnest('{foo, bar}'::text[]) -- same array literal
) i
) ct
ORDER BY ct.ct DESC;
注意一个细微差别:这个消耗每个匹配的元素,所以它不计算data->'tags'
中不匹配的重复项就像第一个变体一样。 有关详细信息,请参见下面的演示。
还演示了另一种传递数组参数的方法:作为数组文字:'{foo, bar}'
。对于 一些 客户来说,这可能更容易处理:
或者您可以创建一个服务器端搜索函数,使用 VARIADIC
参数并传递可变数量的纯 text
值:
相关:
索引
一定要有一个功能性 GIN 索引 来支持 jsonb
existence operator ?|
:
CREATE INDEX tbl_dat_gin ON tbl USING gin (data->'tags');
- Index for finding an element in a JSON array
- What's the proper index for querying structures in arrays in Postgres jsonb?
重复的细微差别
根据 进行澄清。比如说,我们有一个 JSON 数组,其中 两个 重复标签(总共 4 个):
jsonb '{"tags": ["foo", "bar", "foo", "bar"]}'
并使用 SQL 数组参数进行搜索,其中包括 两个 标签,其中一个 重复(共 3 个):
'{foo, bar, foo}'::text[]
考虑这个演示的结果:
SELECT *
FROM (SELECT jsonb '{"tags":["foo", "bar", "foo", "bar"]}') t(数据)
, 横向 (
SELECT <b>count(*)</b> 作为 ct
从 jsonb_array_elements_text(t.data->'tags') e
WHERE e = ANY ('{foo, bar, foo}'::text[])
) 克拉
, 横向 (
SELECT 计数(*) AS ct_intsct_all
从 (
SELECT * 来自 jsonb_array_elements_text(t.data->'tags')
<b>全部相交</b>
SELECT * FROM unnest('{foo, bar, foo}'::text[])
) 一世
) ct_intsct_all
, 横向 (
SELECT <b>count(DISTINCT e)</b> AS ct_dist
从 jsonb_array_elements_text(t.data->'tags') e
WHERE e = ANY ('{foo, bar, foo}'::text[])
) ct_dist
, 横向 (
SELECT 计数(*) AS ct_intsct
从 (
SELECT * 来自 jsonb_array_elements_text(t.data->'tags')
<b>相交</b>
SELECT * FROM unnest('{foo, bar, foo}'::text[])
) 一世
) ct_intsct;
结果:
data | ct | ct_intsct_all | ct_dist | ct_intsct
-----------------------------------------+----+---------------+---------+----------
'{"tags": ["foo", "bar", "foo", "bar"]}' | 4 | 3 | 2 | 2
将 JSON 数组中的元素与数组参数中的元素进行比较:
- 4 标签匹配任何搜索元素:
ct
.
- 3 tags in the set intersect(可以逐元素匹配):
ct_intsct_all
.
- 2 distinct 可以识别匹配标签:
ct_dist
or ct_intsct
.
如果您没有受骗或者不想排除它们,请使用前两种技术中的一种。其他两个有点慢(除了不同的结果),因为他们必须检查欺骗。
我在 Ruby 中发布了我的解决方案的详细信息,以防它对解决相同问题的任何人有用。
最后我决定范围不合适,因为该方法将 return 一个对象数组(不是可链接的 ActiveRecord::Relation
),所以我写了一个 class 方法,并提供了一种通过块将链式范围传递给它的方法:
def self.with_any_tags(tags, &block)
composed_scope = (
block_given? ? yield : all
).where(["data->'tags' ?| ARRAY[:tags]", { tags: tags }])
t = Arel::Table.new('t', ActiveRecord::Base)
ct = Arel::Table.new('ct', ActiveRecord::Base)
arr_sql = Arel.sql "ARRAY[#{ tags.map { |t| Arel::Nodes::Quoted.new(t).to_sql }.join(', ') }]"
any_tags_func = Arel::Nodes::NamedFunction.new('ANY', [arr_sql])
lateral = ct
.project(Arel.sql('e').count(true).as('ct'))
.from(Arel.sql "jsonb_array_elements_text(t.data->'tags') e")
.where(Arel::Nodes::Equality.new Arel.sql('e'), any_tags_func)
query = t
.project(t[Arel.star])
.from(composed_scope.as('t'))
.join(Arel.sql ", LATERAL (#{ lateral.to_sql }) ct")
.order(ct[:ct].desc)
find_by_sql query.to_sql
end
可以这样使用:
Page.with_any_tags(['foo', 'bar'])
# SELECT "t".*
# FROM (
# SELECT "pages".* FROM "pages"
# WHERE data->'tags' ?| ARRAY['foo','bar']
# ) t,
# LATERAL (
# SELECT COUNT(DISTINCT e) AS ct
# FROM jsonb_array_elements_text(t.data->'tags') e
# WHERE e = ANY(ARRAY['foo', 'bar'])
# ) ct
# ORDER BY "ct"."ct" DESC
Page.with_any_tags(['foo', 'bar']) do
Page.published
end
# SELECT "t".*
# FROM (
# SELECT "pages".* FROM "pages"
# WHERE pages.published_at <= '2015-07-19 15:11:59.997134'
# AND pages.deleted_at IS NULL
# AND data->'tags' ?| ARRAY['foo','bar']
# ) t,
# LATERAL (
# SELECT COUNT(DISTINCT e) AS ct
# FROM jsonb_array_elements_text(t.data->'tags') e
# WHERE e = ANY(ARRAY['foo', 'bar'])
# ) ct
# ORDER BY "ct"."ct" DESC
在 Postgres 9.4 和 Rails 的 jsonb
列中使用 JSON 数组,我可以设置一个 return 包含 [=28= 的所有行的范围]any 数组中的元素传递给范围方法 - 像这样:
scope :tagged, ->(tags) {
where(["data->'tags' ?| ARRAY[:tags]", { tags: tags }])
}
我还想根据数组中匹配元素的数量对结果进行排序。
我很感激我可能需要走出 ActiveRecord 的范围才能做到这一点,所以普通的 Postgres SQL 答案也很有帮助,但是如果它可以包含在 ActiveRecord 中那么它可以加分可链接范围。
根据要求,这里有一个示例 table。 (实际模式要复杂得多,但这就是我所关心的。)
id | data
----+-----------------------------------
1 | {"tags": ["foo", "bar", "baz"]}
2 | {"tags": ["bish", "bash", "baz"]}
3 |
4 | {"tags": ["foo", "foo", "foo"]}
用例是根据标签查找相关内容。更多匹配标签更相关,因此结果应按匹配数量排序。在 Ruby 中,我有一个像这样的简单方法:
Page.tagged(['foo', 'bish', 'bash', 'baz']).all
应该return的页面顺序如下:2, 1, 4
.
您的数组仅包含
查询
在 LATERAL
连接中使用 jsonb_array_elements_text()
取消嵌套找到的行的 JSON 数组并计算匹配项:
SELECT *
FROM (
SELECT *
FROM tbl
WHERE data->'tags' ?| ARRAY['foo', 'bar']
) t
, LATERAL (
SELECT count(*) AS ct
FROM jsonb_array_elements_text(t.data->'tags') a(elem)
WHERE elem = ANY (ARRAY['foo', 'bar']) -- same array parameter
) ct
ORDER BY ct.ct DESC; -- more expressions to break ties?
替代INSTERSECT
。这是我们可以利用此基本 SQL 功能的罕见情况之一:
SELECT *
FROM (
SELECT *
FROM tbl
WHERE data->'tags' ?| '{foo, bar}'::text[] -- alt. syntax w. array
) t
, LATERAL (
SELECT count(*) AS ct
FROM (
SELECT * FROM jsonb_array_elements_text(t.data->'tags')
INTERSECT ALL
SELECT * FROM unnest('{foo, bar}'::text[]) -- same array literal
) i
) ct
ORDER BY ct.ct DESC;
注意一个细微差别:这个消耗每个匹配的元素,所以它不计算data->'tags'
中不匹配的重复项就像第一个变体一样。 有关详细信息,请参见下面的演示。
还演示了另一种传递数组参数的方法:作为数组文字:'{foo, bar}'
。对于 一些 客户来说,这可能更容易处理:
或者您可以创建一个服务器端搜索函数,使用 VARIADIC
参数并传递可变数量的纯 text
值:
相关:
索引
一定要有一个功能性 GIN 索引 来支持 jsonb
existence operator ?|
:
CREATE INDEX tbl_dat_gin ON tbl USING gin (data->'tags');
- Index for finding an element in a JSON array
- What's the proper index for querying structures in arrays in Postgres jsonb?
重复的细微差别
根据
jsonb '{"tags": ["foo", "bar", "foo", "bar"]}'
并使用 SQL 数组参数进行搜索,其中包括 两个 标签,其中一个 重复(共 3 个):
'{foo, bar, foo}'::text[]
考虑这个演示的结果:
SELECT *
FROM (SELECT jsonb '{"tags":["foo", "bar", "foo", "bar"]}') t(数据)
, 横向 (
SELECT <b>count(*)</b> 作为 ct
从 jsonb_array_elements_text(t.data->'tags') e
WHERE e = ANY ('{foo, bar, foo}'::text[])
) 克拉
, 横向 (
SELECT 计数(*) AS ct_intsct_all
从 (
SELECT * 来自 jsonb_array_elements_text(t.data->'tags')
<b>全部相交</b>
SELECT * FROM unnest('{foo, bar, foo}'::text[])
) 一世
) ct_intsct_all
, 横向 (
SELECT <b>count(DISTINCT e)</b> AS ct_dist
从 jsonb_array_elements_text(t.data->'tags') e
WHERE e = ANY ('{foo, bar, foo}'::text[])
) ct_dist
, 横向 (
SELECT 计数(*) AS ct_intsct
从 (
SELECT * 来自 jsonb_array_elements_text(t.data->'tags')
<b>相交</b>
SELECT * FROM unnest('{foo, bar, foo}'::text[])
) 一世
) ct_intsct;
结果:
data | ct | ct_intsct_all | ct_dist | ct_intsct
-----------------------------------------+----+---------------+---------+----------
'{"tags": ["foo", "bar", "foo", "bar"]}' | 4 | 3 | 2 | 2
将 JSON 数组中的元素与数组参数中的元素进行比较:
- 4 标签匹配任何搜索元素:
ct
. - 3 tags in the set intersect(可以逐元素匹配):
ct_intsct_all
. - 2 distinct 可以识别匹配标签:
ct_dist
orct_intsct
.
如果您没有受骗或者不想排除它们,请使用前两种技术中的一种。其他两个有点慢(除了不同的结果),因为他们必须检查欺骗。
我在 Ruby 中发布了我的解决方案的详细信息,以防它对解决相同问题的任何人有用。
最后我决定范围不合适,因为该方法将 return 一个对象数组(不是可链接的 ActiveRecord::Relation
),所以我写了一个 class 方法,并提供了一种通过块将链式范围传递给它的方法:
def self.with_any_tags(tags, &block)
composed_scope = (
block_given? ? yield : all
).where(["data->'tags' ?| ARRAY[:tags]", { tags: tags }])
t = Arel::Table.new('t', ActiveRecord::Base)
ct = Arel::Table.new('ct', ActiveRecord::Base)
arr_sql = Arel.sql "ARRAY[#{ tags.map { |t| Arel::Nodes::Quoted.new(t).to_sql }.join(', ') }]"
any_tags_func = Arel::Nodes::NamedFunction.new('ANY', [arr_sql])
lateral = ct
.project(Arel.sql('e').count(true).as('ct'))
.from(Arel.sql "jsonb_array_elements_text(t.data->'tags') e")
.where(Arel::Nodes::Equality.new Arel.sql('e'), any_tags_func)
query = t
.project(t[Arel.star])
.from(composed_scope.as('t'))
.join(Arel.sql ", LATERAL (#{ lateral.to_sql }) ct")
.order(ct[:ct].desc)
find_by_sql query.to_sql
end
可以这样使用:
Page.with_any_tags(['foo', 'bar'])
# SELECT "t".*
# FROM (
# SELECT "pages".* FROM "pages"
# WHERE data->'tags' ?| ARRAY['foo','bar']
# ) t,
# LATERAL (
# SELECT COUNT(DISTINCT e) AS ct
# FROM jsonb_array_elements_text(t.data->'tags') e
# WHERE e = ANY(ARRAY['foo', 'bar'])
# ) ct
# ORDER BY "ct"."ct" DESC
Page.with_any_tags(['foo', 'bar']) do
Page.published
end
# SELECT "t".*
# FROM (
# SELECT "pages".* FROM "pages"
# WHERE pages.published_at <= '2015-07-19 15:11:59.997134'
# AND pages.deleted_at IS NULL
# AND data->'tags' ?| ARRAY['foo','bar']
# ) t,
# LATERAL (
# SELECT COUNT(DISTINCT e) AS ct
# FROM jsonb_array_elements_text(t.data->'tags') e
# WHERE e = ANY(ARRAY['foo', 'bar'])
# ) ct
# ORDER BY "ct"."ct" DESC