如何根据常量 table 的行在 Postgres 中实现查询参数?
How do I implement a query parameter in Postgres based on a constant table's row?
我有以下查询:
-- Really fast
-- Explain Analyze: https://explain.depesz.com/s/lsq8
with start_time as (
select '2022-06-02T17:45:43Z':: timestamp with time zone as time
)
-- 200x slower
-- Explain Analyze: https://explain.depesz.com/s/CavD
with start_time as (
select last_update as time
from processed.last_update
where table_name = 'token_supply'
)
select ident as ma_id
, sum(quantity) as quantity
, sum(quantity) filter (where quantity > 0) as quantity_minted
from public.ma_tx_mint
where exists (
select id
from public.tx
where tx.id = ma_tx_mint.tx_id
and exists (
select id
from public.block
cross join start_time
where block.id = tx.block_id
and block.time >= start_time.time
)
)
group by ident
我正在尝试查询在指定时间后添加到 table 的记录。如果时间像第一个 start_time
那样被硬编码,则查询将在 0.2 秒内运行。在我动态检索时间的第二个 start_time
的情况下,查询运行了 40 秒。
如何让 Postgres 以相同的方式处理这两种情况,并根据另一个 table 的行动态查询 ma_tx_mint
table?
版本: x86_64-pc-linux-gnu 上的 PostgreSQL 13.6,由 Debian clang 版本 12.0.1 编译,64 位
表格:
create table public.ma_tx_mint (
id bigint
, quantity numeric
, tx_id bigint
, ident bigint
, primary key(id)
);
create table public.tx (
id bigint
, block_id bigint
, primary key(id)
);
create table public.block (
id bigint
, time timestamp with time zone
, primary key(id)
);
create table processed.last_update (
table_name varchar
, last_update timestamp with time zone
, primary key(table_name)
);
Explain Analyze
:
快:https://explain.depesz.com/s/lsq8
慢:https://explain.depesz.com/s/CavD
问题
Postgres 的列统计信息包括 histogram bounds。您的恒定时间戳(fast 变体)似乎接近最新的几行,因此 Postgres 知道 期望来自 [=66 的很少的合格行=] block
。这个估计结果没问题:
Index Scan using idx_block_time on block (cost=0.43..14.29 rows=163 width=8)
(actual time=0.825..1.146 rows=891 loops=1)
随着子选择获取一个未知的时间戳(慢 变体),Postgres 不知道 期望什么并计划时间戳在中间的某个地方。不幸的是,您的 table block
似乎有大约 750 万行,因此假设一个未知的过滤器 Postgres 预计所有行中大约有 1/3 符合条件,即 ~ 250 万:
Index Scan using idx_block_time on block (cost=0.43..127,268.65 rows=2,491,327 width=16)
(actual time=1.261..1.723 rows=653 loops=3)
因此 Postgres 计划数百万行并使用顺序扫描,这对于实际符合条件的少数行来说是一个糟糕的选择。
可能的解决方案
如果您知道时间戳的下限,您可以将其添加为(逻辑上多余的)额外的谓词来引导 Postgres 更合适 table 计划:
SELECT ident AS ma_id
, sum(quantity) AS quantity
, sum(quantity) FILTER (WHERE quantity > 0) AS quantity_minted
FROM public.ma_tx_mint m
WHERE EXISTS (
SELECT FROM public.tx
WHERE tx.id = m.tx_id
AND EXISTS (
SELECT FROM public.block b
WHERE b.id = tx.block_id
AND b.time >= (SELECT last_update FROM processed.last_update WHERE table_name = 'token_supply')
AND b.time >= '2022-06-01 00:00+0' -- !!! some known minimum bound
)
)
GROUP BY 1;
此外,由于 table_name
是 processed.last_update
的主键,我们知道 suquery 仅 returns 一行,我们可以使用简单的标量子查询。应该已经快一点了。
但要点是添加的最小界限。如果这足够有选择性,Postgres 就会知道像您的快速计划一样切换到索引扫描。
放在一边
将 timestamp
常量转换为 timestamptz
通常是个坏主意:
'2022-06-02T17:45:43Z'::timestamptz
这将采用当前会话的时区,这可能与预期不符。而是明确:
'2022-06-02T17:45:43Z'::timestamp AT TIME ZONE 'UTC'
'2022-06-02T17:45:43Z+0'::timestamptz
.. 或您实际想要使用的任何时区。参见:
- Ignoring time zones altogether in Rails and PostgreSQL
我有以下查询:
-- Really fast
-- Explain Analyze: https://explain.depesz.com/s/lsq8
with start_time as (
select '2022-06-02T17:45:43Z':: timestamp with time zone as time
)
-- 200x slower
-- Explain Analyze: https://explain.depesz.com/s/CavD
with start_time as (
select last_update as time
from processed.last_update
where table_name = 'token_supply'
)
select ident as ma_id
, sum(quantity) as quantity
, sum(quantity) filter (where quantity > 0) as quantity_minted
from public.ma_tx_mint
where exists (
select id
from public.tx
where tx.id = ma_tx_mint.tx_id
and exists (
select id
from public.block
cross join start_time
where block.id = tx.block_id
and block.time >= start_time.time
)
)
group by ident
我正在尝试查询在指定时间后添加到 table 的记录。如果时间像第一个 start_time
那样被硬编码,则查询将在 0.2 秒内运行。在我动态检索时间的第二个 start_time
的情况下,查询运行了 40 秒。
如何让 Postgres 以相同的方式处理这两种情况,并根据另一个 table 的行动态查询 ma_tx_mint
table?
版本: x86_64-pc-linux-gnu 上的 PostgreSQL 13.6,由 Debian clang 版本 12.0.1 编译,64 位
表格:
create table public.ma_tx_mint (
id bigint
, quantity numeric
, tx_id bigint
, ident bigint
, primary key(id)
);
create table public.tx (
id bigint
, block_id bigint
, primary key(id)
);
create table public.block (
id bigint
, time timestamp with time zone
, primary key(id)
);
create table processed.last_update (
table_name varchar
, last_update timestamp with time zone
, primary key(table_name)
);
Explain Analyze
:
快:https://explain.depesz.com/s/lsq8
慢:https://explain.depesz.com/s/CavD
问题
Postgres 的列统计信息包括 histogram bounds。您的恒定时间戳(fast 变体)似乎接近最新的几行,因此 Postgres 知道 期望来自 [=66 的很少的合格行=] block
。这个估计结果没问题:
Index Scan using idx_block_time on block (cost=0.43..14.29 rows=163 width=8) (actual time=0.825..1.146 rows=891 loops=1)
随着子选择获取一个未知的时间戳(慢 变体),Postgres 不知道 期望什么并计划时间戳在中间的某个地方。不幸的是,您的 table block
似乎有大约 750 万行,因此假设一个未知的过滤器 Postgres 预计所有行中大约有 1/3 符合条件,即 ~ 250 万:
Index Scan using idx_block_time on block (cost=0.43..127,268.65 rows=2,491,327 width=16) (actual time=1.261..1.723 rows=653 loops=3)
因此 Postgres 计划数百万行并使用顺序扫描,这对于实际符合条件的少数行来说是一个糟糕的选择。
可能的解决方案
如果您知道时间戳的下限,您可以将其添加为(逻辑上多余的)额外的谓词来引导 Postgres 更合适 table 计划:
SELECT ident AS ma_id
, sum(quantity) AS quantity
, sum(quantity) FILTER (WHERE quantity > 0) AS quantity_minted
FROM public.ma_tx_mint m
WHERE EXISTS (
SELECT FROM public.tx
WHERE tx.id = m.tx_id
AND EXISTS (
SELECT FROM public.block b
WHERE b.id = tx.block_id
AND b.time >= (SELECT last_update FROM processed.last_update WHERE table_name = 'token_supply')
AND b.time >= '2022-06-01 00:00+0' -- !!! some known minimum bound
)
)
GROUP BY 1;
此外,由于 table_name
是 processed.last_update
的主键,我们知道 suquery 仅 returns 一行,我们可以使用简单的标量子查询。应该已经快一点了。
但要点是添加的最小界限。如果这足够有选择性,Postgres 就会知道像您的快速计划一样切换到索引扫描。
放在一边
将 timestamp
常量转换为 timestamptz
通常是个坏主意:
'2022-06-02T17:45:43Z'::timestamptz
这将采用当前会话的时区,这可能与预期不符。而是明确:
'2022-06-02T17:45:43Z'::timestamp AT TIME ZONE 'UTC'
'2022-06-02T17:45:43Z+0'::timestamptz
.. 或您实际想要使用的任何时区。参见:
- Ignoring time zones altogether in Rails and PostgreSQL