【发布时间】:2022-07-04 03:14:57
【问题描述】:
我正在设计一个表,它有一个jsonb 列实现权限,格式如下:
[
{"role": 5, "perm": "view"},
{"role": 30, "perm": "edit"},
{"role": 52, "perm": "view"}
]
TL;DR
如何将这种jsonb 值转换为整数角色的SQL 数组?在本例中,它将是'{5,30,52}'::int[]。我有一些解决方案,但没有一个足够快。继续阅读...
每个登录用户都有一些角色(一个或多个)。这个想法是使用 int[] 上的重叠运算符 (&&) 过滤记录。
SELECT * FROM data WHERE extract_roles(access) && '{1,5,17}'::int[]
我正在寻找也可以在索引定义中使用的extract_roles 函数/表达式:
CREATE INDEX data_roles ON data USING gin ((extract_roles(access)))
Postgres 中的jsonb 似乎对构建和转换有广泛的支持,但对提取值的支持较少——在这种情况下是 SQL 数组。
我尝试了什么:
create or replace function extract_roles(access jsonb) returns int[]
language sql
strict
parallel safe
immutable
-- with the following bodies:
-- (0) 629ms
select translate(jsonb_path_query_array(access, '$.role')::text, '[]', '{}')::int[]
-- (1) 890ms
select array_agg(r::int) from jsonb_path_query(access, '$.role') r
-- (2) 866ms
select array_agg((t ->> 'role')::int) from jsonb_array_elements(access) as x(t)
-- (3) 706ms
select f1 from jsonb_populate_record(row('{}'::int[]), jsonb_build_object('f1', jsonb_path_query_array(access, '$.role'))) as x (f1 int[])
使用索引时,查询速度很快。但是这些表达式有两个问题:
- 部分功能只有
stable而不是immutable;这也适用于cast。 我可以将我的函数标记为immutable吗?索引定义需要不变性。 - 它们很慢;规划器在某些情况下不使用索引,然后查询会变得非常慢(以上时间是在具有 3M 条记录的表上):
explain (analyse)
select id, access
from data
where extract_roles(access) && '{-3,99}'::int[]
order by id
limit 100
使用以下计划(上述所有变体都相同;更喜欢扫描与主键关联的索引,获得排序结果并希望它很快找到其中的 100 个):
Limit (cost=1000.45..2624.21 rows=100 width=247) (actual time=40.668..629.193 rows=100 loops=1)
-> Gather Merge (cost=1000.45..476565.03 rows=29288 width=247) (actual time=40.667..629.162 rows=100 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Index Scan using data_pkey on data (cost=0.43..472184.44 rows=12203 width=247) (actual time=25.522..513.463 rows=35 loops=3)
Filter: (extract_roles(access) && '{-3,99}'::integer[])
Rows Removed by Filter: 84918
Planning Time: 0.182 ms
Execution Time: 629.245 ms
删除LIMIT 子句的速度非常快:
Gather Merge (cost=70570.65..73480.29 rows=24938 width=247) (actual time=63.263..75.710 rows=40094 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=69570.63..69601.80 rows=12469 width=247) (actual time=59.870..61.569 rows=13365 loops=3)
Sort Key: id
Sort Method: external merge Disk: 3744kB
Worker 0: Sort Method: external merge Disk: 3232kB
Worker 1: Sort Method: external merge Disk: 3160kB
-> Parallel Bitmap Heap Scan on data (cost=299.93..68722.36 rows=12469 width=247) (actual time=13.823..49.336 rows=13365 loops=3)
Recheck Cond: (extract_roles(access) && '{-3,99}'::integer[])
Heap Blocks: exact=9033
-> Bitmap Index Scan on data_roles (cost=0.00..292.44 rows=29926 width=0) (actual time=9.429..9.430 rows=40094 loops=1)
Index Cond: (extract_roles(access) && '{-3,99}'::integer[])
Planning Time: 0.234 ms
Execution Time: 77.719 ms
有没有更好更快的方法从jsonb 中提取int[]?因为我不能依赖计划器总是使用索引。使用extract_roles 函数的COST 会有所帮助(规划器开始使用LIMIT 1000 的索引),但即使是非常高的值也不会强制使用LIMIT 100 的索引。
评论:
如果没有,我可能会将信息存储在另一列 roles int[] 中,这速度很快,但需要额外的空间并需要额外的处理(可以使用 Postgres 12+ 上生成的列来解决,Azure 仍然不提供,或触发器,或在应用程序逻辑中)。
展望未来,Postgres 15 会有更好的支持吗?可能是JSON_QUERY,但我看不到任何立竿见影的改进,因为它的RETURNING 子句可能指的是整个结果而不是它的元素。
也许jsonb_populate_record 也可以考虑非复合类型(其签名允许),例如:
select jsonb_populate_record(null::int[], '[123,456]'::jsonb)
最接近的两个问题是:
- Extract integer array from jsonb within postgres 9.6
- Cast postgresql jsonb value as array of int and remove element from it
对建议标准化的反应:
标准化可能不可行。但让我们顺着思路走吧。
我假设额外的表看起来像这样:*_perm (id, role, perm)。 id 上会有一个索引,role 上会有另一个索引。
由于一个用户有多个角色,它可能会为同一个 id 连接多个记录,这会导致数据表中的记录相乘,并强制进行group by 聚合。
group by 不利于性能,因为它会阻止一些优化。我正在设计一个积木。因此,例如可以有两个数据表在起作用:
select pd.*, jsonb_agg(to_jsonb(pp))
from posts_data pd
join posts_perm pp on pd.id = pp.id
where exists(
select 1
from comments_data cd on cd.post_id = pd.id
join comments_perm cp on cp.id = cd.id
where cd.reputation > 100
and cp.role in (3,34,52)
-- no group by needed due to semi-join
)
and cp.role in (3,34,52)
group by pd.id
order by pd.title
limit 10
如果我没记错的话,这个查询需要在排序之前聚合所有记录。这里没有索引可以提供帮助。对于数百万条记录,这永远不会很快。此外,group by 的使用背后有一个重要的逻辑 - 它并不总是需要。
如果我们不需要返回权限而只关心它的存在呢?
select pd.*
from posts_data pd
where exists(
select 1
from posts_perm pp on pd.id = pp.id
where cp.role in (3,34,52)
)
and exists(
select 1
from comments_data cd on cd.post_id = pd.id
where exists(
select 1
from comments_perm cp on cp.id = cd.id
where cp.role in (3,34,52)
)
and cd.reputation > 100
)
order by pd.title
limit 10
然后我们不需要任何聚合 - 数据库将简单地发出 SEMI-JOIN。如果title上有索引,数据库可以考虑使用。我们甚至可以在投影中获取权限;像这样:
select pd.*, (select jsonb_agg(to_jsonb(pp)) from posts_perm pp on pd.id = pp.id) perm
...
仅针对少数 (10) 条记录发出嵌套循环连接。我将测试这种方法。
另一种选择是将数据保留在两个表中 - 数据表将仅存储int[] 的角色。然后我们保存一个 JOIN,最后只从权限表中获取。现在我们需要一个支持数组操作的索引——GIN。
select pd.*, (select jsonb_agg(to_jsonb(pp)) from posts_perm pp on pd.id = pp.id) perm
from posts_data pd
where pd.roles && '{3,34,52}'::int[]
and exists(
select 1
from comments_data cd on cd.post_id = pd.id
where cd.roles && '{3,34,52}'::int[]
and cd.reputation > 100
)
order by pd.title
limit 10
因为我们总是汇总返回记录的所有权限(它们的解释在应用程序中,我们返回所有这些都无关紧要),我们可以将 post_perms 存储为json。因为我们从不需要使用 SQL 中的值,所以将它们直接存储在数据表中似乎是合理的。
稍后我们将需要支持一些批量共享操作来更新许多记录的权限,但这比选择要少得多。因此,我们可以改用jsonb。
投影不再需要选择权限:
select pd.*
...
但现在roles 列是多余的 - 我们在同一个表中拥有相同的信息,只是采用 JSON 格式。如果我们可以编写一个只提取角色的函数,我们可以直接对其进行索引。
我们又回到了起点。但看起来extract_roles 函数永远不会很快,所以我们需要保留roles 列。
在同一个表中保留权限的另一个原因是可以使用 Bitmap And 组合多个索引并避免连接。
角色会有很大的偏差。有些将出现在几乎所有行上(管理员可以编辑所有内容),有些则很少见(John Doe 只能访问与他明确共享的这 3 条记录)。我不确定统计数据在 int[] 方法上的效果如何,但到目前为止,我的测试表明,当角色不频繁(高选择性)时使用 GIN 索引。
【问题讨论】:
-
为什么要使用 json,因为每次提取都会花费时间。
-
使用正确规范化的数据模型将非常容易(并且可能也更有效)
-
@a_horse_with_no_name - 我尝试探索规范化方法(参见编辑),但尚未运行任何查询。我的想法合理吗?
-
“它们很慢;规划器在某些情况下不使用索引,”您是否在创建表达式索引后分析表?
-
“在 Postgres 12+ 上使用生成的列,Azure 仍然没有提供这些列”我会假设现在没有超过 v11 的任何东西都是一些死胡同的遗留系统,我不想发展反对它了。他们的“灵活服务器”目前升级到 v13。
标签: sql postgresql jsonb