使用非空约束优化 Postgres JSONB 查询
Optimizing Postgres JSONB query with not null constraint
我有一个包含 170 万条记录的 Postgres 9.4.4 数据库,以下信息存储在名为 accounts
的 table 中名为 data
的 JSONB 列中:
data: {
"lastUpdatedTime": "2016-12-26T12:09:43.901Z",
"UID": "2c5bb7fd-1a00-4988-8d92-ffaa52ebc20d",
"data": {
"country": "UK",
"verified_at": "2017-01-01T23:49:10.217Z"
}
}
无法更改数据格式,因为这是遗留信息。
我需要获取国家为UK
、verified_at
值不为空且lastUpdatedTime
值大于某个给定值的所有帐户。
到目前为止,我有以下查询:
SELECT * FROM "accounts"
WHERE (data @> '{ "data": { "country": "UK" } }')
AND (data->'data' ? 'verified_at')
AND ((data->'data' ->> 'verified_at') is not null)
AND (data ->>'lastUpdatedTime' > '2016-02-28T05:49:08.511846')
ORDER BY data ->>'lastUpdatedTime' LIMIT 100 OFFSET 0;
以及以下索引:
"accounts_idxgin" gin (data)
"accounts_idxgin_on_data" gin ((data -> 'data'::text))
我已经设法将查询时间缩短到大约 1000 到 4000 毫秒
这是查询的分析:
Bitmap Heap Scan on accounts (cost=41.31..6934.50 rows=9 width=1719)
(actual time=7.273..1067.657 rows=23190 loops=1)
Recheck Cond: ((data -> 'data'::text) ? 'verified_at'::text)
Filter: ((((data -> 'data'::text) ->> 'verified_at'::text) IS NOT NULL)
AND ((data ->> 'lastUpdatedTime'::text) > '2016-02-01 05:49:08.511846'::text)
AND (((data -> 'data'::text) ->> 'country'::text) = 'UK'::text))
Rows Removed by Filter: 4
Heap Blocks: exact=16039
-> Bitmap Index Scan on accounts_idxgin_on_data (cost=0.00..41.30 rows=1773 width=0)
(actual time=4.618..4.618 rows=23194 loops=1)
Index Cond: ((data -> 'data'::text) ? 'verified_at'::text)
Planning time: 0.448 ms
Execution time: 1069.344 ms
(9 rows)
我有以下问题
- 我能做些什么来进一步加快这个查询的速度吗?
- 使用 JSONB 加速
field is not null
查询的正确方法是什么?我最终使用带有 (data->'data' ? 'verified_at')
的存在运算符来过滤掉大量不匹配的记录,因为我的大部分数据都没有 verified_at
作为顶级键。这提高了查询速度,但我想知道是否有优化此类查询的通用方法。
- 为了在
(data->'data' ? 'verified_at')
中使用存在运算符,我需要在 ((data -> 'data'::text))
上添加另一个索引。我已经在 gin (data)
上有一个索引,但是存在运算符没有使用它。这是为什么?我以为存在和包含运算符会使用这个索引。
使用路径访问运算符可以更快地访问较低级别的对象:
SELECT * FROM "accounts"
WHERE data #>> '{data, country}' = 'UK'
AND data #>> '{data, verified_at}' IS NOT NULL
AND data ->> 'lastUpdatedTime' > '2016-02-28T05:49:08.511846'
ORDER BY data ->> 'lastUpdatedTime' LIMIT 100 OFFSET 0;
索引仅适用于顶级键。因此,使用列 data
上的索引支持 data @> [[key]]
之类的查询。但是,对于 data -> 'data' ? 'verified_at'
上的查询,您需要 data->'data'
.
上的索引
还有两点:
- 我认为没有必要测试
verified_at
的存在。如果它不存在,它只是作为 NULL 出现,所以它会被相同的测试捕获。
- 如果 JSON 值的格式正确且一致,则比较
timestamp
值的字符串表示可能有效。为了安全起见,投射到 timestamp
。
经过更多尝试后,我通过创建以下部分索引设法将查询时间从大约 1000 毫秒减少到 350 毫秒:
CREATE INDEX index_accounts_partial_on_verified_at
ON accounts ((data->'data'->'verified_at'))
WHERE (data->'data'->>'verified_at') IS NOT NULL
AND (data->'data' ? 'verified_at')
AND (data->'data'->>'country' = 'UK');
我能够硬编码此索引中的某些值,例如 country=UK
,因为我只需要考虑此查询的 UK
个帐户。我还能够删除 ((data->'data'))
上的 258MB 索引,并将其替换为仅 1360 kB 的部分索引!
对于任何感兴趣的人,我从 here
中找到了构建部分 JSONB 索引的详细信息
3:不是真的。本例为explicitly mentioned in the docs。
当您在列 data
上有索引时,它仅在您查询 table 时使用,例如 data @> '...'
或 data ? '...'
。当表达式 (data -> 'data')
上有索引时,这些查询可以利用它:(data -> 'data') @> '...'
或 (data -> 'data') ? '...'
.
2:通常的 jsonb
索引在 (jsonb_col -> '<key>') is [not] null
查询期间根本没有帮助。不幸的是,您也不能使用 jsonb_col @> '{"<key>":null}'
,因为 JSON 对象可能完全缺少密钥。也根本不可能反向使用索引(对于 is not null
)。不过可能有窍门...
1:不多。可能会有一些改进,但不要指望巨大的性能优势。所以他们开始了:
您可以使用 jsonb_path_ops
运算符 class 而不是(默认)jsonb_ops
。这应该意味着性能上有一点改进,但他们不能使用存在运算符 (?
)。但无论如何我们都不需要它。
您有一个索引不友好的 boolean
类型表达式,这会减慢您的速度。值得庆幸的是,如果您只对 true
值感兴趣,可以在此处使用 partial index。
因此,您的索引应如下所示:
create index accounts_idxgin_on_data
on accounts using gin ((data -> 'data') jsonb_path_ops)
where (data -> 'data' ->> 'verified_at') is not null;
有了这个索引,你可以使用下面的查询:
select *
from accounts
where (data -> 'data') @> '{"country":"UK"}'
and (data -> 'data' ->> 'verified_at') is not null
and (data ->> 'lastUpdatedTime') > '2016-02-28T05:49:08.511Z'
order by data ->>'lastUpdatedTime';
注意:为了进行正确的timestamp
比较,您应该使用(data ->> 'lastUpdatedTime')::timestamptz > '2016-02-28T05:49:08.511Z'
.
我有一个包含 170 万条记录的 Postgres 9.4.4 数据库,以下信息存储在名为 accounts
的 table 中名为 data
的 JSONB 列中:
data: {
"lastUpdatedTime": "2016-12-26T12:09:43.901Z",
"UID": "2c5bb7fd-1a00-4988-8d92-ffaa52ebc20d",
"data": {
"country": "UK",
"verified_at": "2017-01-01T23:49:10.217Z"
}
}
无法更改数据格式,因为这是遗留信息。
我需要获取国家为UK
、verified_at
值不为空且lastUpdatedTime
值大于某个给定值的所有帐户。
到目前为止,我有以下查询:
SELECT * FROM "accounts"
WHERE (data @> '{ "data": { "country": "UK" } }')
AND (data->'data' ? 'verified_at')
AND ((data->'data' ->> 'verified_at') is not null)
AND (data ->>'lastUpdatedTime' > '2016-02-28T05:49:08.511846')
ORDER BY data ->>'lastUpdatedTime' LIMIT 100 OFFSET 0;
以及以下索引:
"accounts_idxgin" gin (data)
"accounts_idxgin_on_data" gin ((data -> 'data'::text))
我已经设法将查询时间缩短到大约 1000 到 4000 毫秒
这是查询的分析:
Bitmap Heap Scan on accounts (cost=41.31..6934.50 rows=9 width=1719)
(actual time=7.273..1067.657 rows=23190 loops=1)
Recheck Cond: ((data -> 'data'::text) ? 'verified_at'::text)
Filter: ((((data -> 'data'::text) ->> 'verified_at'::text) IS NOT NULL)
AND ((data ->> 'lastUpdatedTime'::text) > '2016-02-01 05:49:08.511846'::text)
AND (((data -> 'data'::text) ->> 'country'::text) = 'UK'::text))
Rows Removed by Filter: 4
Heap Blocks: exact=16039
-> Bitmap Index Scan on accounts_idxgin_on_data (cost=0.00..41.30 rows=1773 width=0)
(actual time=4.618..4.618 rows=23194 loops=1)
Index Cond: ((data -> 'data'::text) ? 'verified_at'::text)
Planning time: 0.448 ms
Execution time: 1069.344 ms
(9 rows)
我有以下问题
- 我能做些什么来进一步加快这个查询的速度吗?
- 使用 JSONB 加速
field is not null
查询的正确方法是什么?我最终使用带有(data->'data' ? 'verified_at')
的存在运算符来过滤掉大量不匹配的记录,因为我的大部分数据都没有verified_at
作为顶级键。这提高了查询速度,但我想知道是否有优化此类查询的通用方法。 - 为了在
(data->'data' ? 'verified_at')
中使用存在运算符,我需要在((data -> 'data'::text))
上添加另一个索引。我已经在gin (data)
上有一个索引,但是存在运算符没有使用它。这是为什么?我以为存在和包含运算符会使用这个索引。
使用路径访问运算符可以更快地访问较低级别的对象:
SELECT * FROM "accounts"
WHERE data #>> '{data, country}' = 'UK'
AND data #>> '{data, verified_at}' IS NOT NULL
AND data ->> 'lastUpdatedTime' > '2016-02-28T05:49:08.511846'
ORDER BY data ->> 'lastUpdatedTime' LIMIT 100 OFFSET 0;
索引仅适用于顶级键。因此,使用列 data
上的索引支持 data @> [[key]]
之类的查询。但是,对于 data -> 'data' ? 'verified_at'
上的查询,您需要 data->'data'
.
还有两点:
- 我认为没有必要测试
verified_at
的存在。如果它不存在,它只是作为 NULL 出现,所以它会被相同的测试捕获。 - 如果 JSON 值的格式正确且一致,则比较
timestamp
值的字符串表示可能有效。为了安全起见,投射到timestamp
。
经过更多尝试后,我通过创建以下部分索引设法将查询时间从大约 1000 毫秒减少到 350 毫秒:
CREATE INDEX index_accounts_partial_on_verified_at
ON accounts ((data->'data'->'verified_at'))
WHERE (data->'data'->>'verified_at') IS NOT NULL
AND (data->'data' ? 'verified_at')
AND (data->'data'->>'country' = 'UK');
我能够硬编码此索引中的某些值,例如 country=UK
,因为我只需要考虑此查询的 UK
个帐户。我还能够删除 ((data->'data'))
上的 258MB 索引,并将其替换为仅 1360 kB 的部分索引!
对于任何感兴趣的人,我从 here
中找到了构建部分 JSONB 索引的详细信息3:不是真的。本例为explicitly mentioned in the docs。
当您在列 data
上有索引时,它仅在您查询 table 时使用,例如 data @> '...'
或 data ? '...'
。当表达式 (data -> 'data')
上有索引时,这些查询可以利用它:(data -> 'data') @> '...'
或 (data -> 'data') ? '...'
.
2:通常的 jsonb
索引在 (jsonb_col -> '<key>') is [not] null
查询期间根本没有帮助。不幸的是,您也不能使用 jsonb_col @> '{"<key>":null}'
,因为 JSON 对象可能完全缺少密钥。也根本不可能反向使用索引(对于 is not null
)。不过可能有窍门...
1:不多。可能会有一些改进,但不要指望巨大的性能优势。所以他们开始了:
您可以使用 jsonb_path_ops
运算符 class 而不是(默认)jsonb_ops
。这应该意味着性能上有一点改进,但他们不能使用存在运算符 (?
)。但无论如何我们都不需要它。
您有一个索引不友好的 boolean
类型表达式,这会减慢您的速度。值得庆幸的是,如果您只对 true
值感兴趣,可以在此处使用 partial index。
因此,您的索引应如下所示:
create index accounts_idxgin_on_data
on accounts using gin ((data -> 'data') jsonb_path_ops)
where (data -> 'data' ->> 'verified_at') is not null;
有了这个索引,你可以使用下面的查询:
select *
from accounts
where (data -> 'data') @> '{"country":"UK"}'
and (data -> 'data' ->> 'verified_at') is not null
and (data ->> 'lastUpdatedTime') > '2016-02-28T05:49:08.511Z'
order by data ->>'lastUpdatedTime';
注意:为了进行正确的timestamp
比较,您应该使用(data ->> 'lastUpdatedTime')::timestamptz > '2016-02-28T05:49:08.511Z'
.