如何根据常量 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_nameprocessed.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