PostgreSQL JSONB查询未用索引优化及结果不符问题
PostgreSQL JSONB GIN索引优化:匹配数组中任意元素的查询
表结构与数据
refs表包含JSONB类型列,结构及数据如下:
| id | json_data(jsonb) |
|---|---|
| 1 | { "CorrelationId": "1111", "References": [ { "Id": "001", "Type": "RefType" }, { "Id": "002", "Type": "RefType" } ] } |
| 2 | { "CorrelationId": "2222", "References": [ { "Id": "001", "Type": "RefType" }, { "Id": "003", "Type": "RefType" } ] } |
问题描述
需要返回包含任意一个指定RefType对象的行(即同时返回id=1和id=2的行),但遇到以下问题:
- 以下查询逻辑正确,但未使用已创建的GIN索引,导致全表扫描,性能极差:
select * from refs where json_data -> 'References' @> any(array(select jsonb_build_array(refNo) from jsonb_array_elements(cast('[{"Id":"001","Type":"RefType"},{"Id":"002","Type":"RefType"}]' as jsonb)) refNo))
- 已创建的GIN索引:
CREATE INDEX idx ON refs USING gin ((json_data -> 'References') jsonb_path_ops);
- 以下查询能正常使用GIN索引,但逻辑不符合需求(仅返回同时包含两个指定RefType对象的行,即只返回id=1,无法返回id=2):
select * from refs where json_data -> 'References' @> '[{"Id":"001","Type":"RefType"},{"Id":"002","Type":"RefType"}]'
执行计划分析
原查询执行计划
原查询执行计划显示进行了并行全表扫描,未使用GIN索引,执行时间长达23秒:
QUERY PLAN | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Gather (cost=1001.25..1135454.32 rows=2490762 width=486) (actual time=23804.717..23807.660 rows=0 loops=1) | Output: Workers Planned: 2 | Params Evaluated: $0 | Workers Launched: 2 | Buffers: shared hit=4220 read=609812 dirtied=75 | I/O Timings: shared/local read=61795.046 | InitPlan 1 (returns $0) | -> Function Scan on pg_catalog.jsonb_array_elements refno (cost=0.00..1.25 rows=100 width=32) (actual time=0.018..0.020 rows=2 loops=1) | Output: jsonb_build_array(refno.value) | Function Call: jsonb_array_elements('[{"Id": "0002222", "Type": "RefType"}, {"Id": "0002333", "Type": "RefType"}]'::jsonb) | -> Parallel Seq Scan on public.refs (cost=0.00..885376.86 rows=1037818 width=486) (actual time=23793.829..23793.830 rows=0 loops=3) | Output: Filter: ((refs.json_data -> 'References'::text) @> ANY ($0)) | Rows Removed by Filter: 8352299 | Buffers: shared hit=4220 read=609812 dirtied=75 | I/O Timings: shared/local read=61795.046 | Worker 0: actual time=23793.628..23793.629 rows=0 loops=1 | Buffers: shared hit=2002 read=202638 dirtied=6 | I/O Timings: shared/local read=20565.263 | Worker 1: actual time=23784.348..23784.349 rows=0 loops=1 | Buffers: shared hit=1059 read=202832 dirtied=69 | I/O Timings: shared/local read=20593.289 | Settings: effective_cache_size = '2640112kB', search_path = 'public, public, "$user"' | Query Identifier: 4447059927147613398 | Planning: | Buffers: shared hit=1 | Planning Time: 0.286 ms | Execution Time: 23807.719 ms |
修改后的查询执行计划
根据Laurenz Albe修改后的查询依然采用全表扫描+嵌套循环,执行时间更长(42秒),仍未利用索引:
QUERY PLAN | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Gather (cost=1000.00..33199657.22 rows=26046052 width=518) (actual time=42826.145..42840.404 rows=1 loops=1) | Output: refs.id, refs.json_data Workers Planned: 2 | Workers Launched: 2 | Buffers: shared hit=3048 read=610912 dirtied=2466 | I/O Timings: shared/local read=25028.306 | -> Nested Loop (cost=0.00..30594052.02 rows=10852522 width=518) (actual time=42817.831..42821.316 rows=0 loops=3) | Output: refs.id, refs.json_data Join Filter: ((refs.json_data -> 'References'::text) @> jsonb_build_array(refNo.value)) Rows Removed by Join Filter: 8352160 | Buffers: shared hit=3048 read=610912 dirtied=2466 | I/O Timings: shared/local read=25028.306 | Worker 0: actual time=42814.320..42814.322 rows=0 loops=1 | Buffers: shared hit=1038 read=205719 dirtied=789 | I/O Timings: shared/local read=8403.926 | Worker 1: actual time=42813.762..42813.765 rows=0 loops=1 | Buffers: shared hit=1024 read=201674 dirtied=982 | I/O Timings: shared/local read=8412.231 | -> Parallel Seq Scan on public.refs (cost=0.00..722485.22 rows=10852522 width=486) (actual time=0.962..19531.915 rows=8352160 loops=3) | Output: refs.id, refs.json_data Buffers: shared hit=3048 read=610912 dirtied=2466 | I/O Timings: shared/local read=25028.306 | Worker 0: actual time=0.817..19559.124 rows=8448451 loops=1 | Buffers: shared hit=1038 read=205719 dirtied=789 | I/O Timings: shared/local read=8403.926 | Worker 1: actual time=0.834..19706.338 rows=8261642 loops=1 | Buffers: shared hit=1024 read=201674 dirtied=982 | I/O Timings: shared/local read=8412.231 | -> Function Scan on pg_catalog.jsonb_array_elements refno (cost=0.00..1.00 rows=100 width=32) (actual time=0.000..0.000 rows=1 loops=25056480) | Output: refno.value | Function Call: jsonb_array_elements('[{"Id": "0001", "Type": "RefType"}]'::jsonb) | Worker 0: actual time=0.000..0.000 rows=1 loops=8448451 | Worker 1: actual time=0.000..0.000 rows=1 loops=8261642 | Settings: effective_cache_size = '2640112kB', search_path = 'public, public, "$user"' | Query Identifier: -5824741316160729696 | Planning: | Buffers: shared hit=1 | Planning Time: 0.358 ms | Execution Time: 42840.510 ms |
解决方案:让查询命中GIN索引
要实现“匹配任意指定RefType对象”且利用GIN索引,可采用以下两种方式:
方式1:使用OR连接独立条件
将每个待匹配的RefType对象作为单独的@>条件,用OR连接,PostgreSQL会自动合并多个索引扫描的结果:
SELECT * FROM refs WHERE json_data -> 'References' @> '[{"Id":"001","Type":"RefType"}]' OR json_data -> 'References' @> '[{"Id":"002","Type":"RefType"}]';
方式2:使用EXISTS子查询
适合动态生成匹配条件的场景,同样能命中GIN索引:
SELECT r.* FROM refs r WHERE EXISTS ( SELECT 1 FROM jsonb_array_elements(r.json_data -> 'References') ref WHERE ref IN ( '{"Id":"001","Type":"RefType"}'::jsonb, '{"Id":"002","Type":"RefType"}'::jsonb ) );
原查询无法使用索引的原因
原查询中ANY(array(...))的结构,无法被PostgreSQL优化器关联到GIN索引的jsonb_path_ops算子,因此只能执行全表扫描。而OR连接的独立条件或EXISTS子查询的形式,能让优化器识别到可通过GIN索引快速定位目标行。
内容的提问来源于stack exchange,提问作者user2686282
相关产品推荐
相关产品推荐

