2

我的应用程序向白水划船者报告降雨和流量信息。Postgres 是我的数据存储,用于以 15 分钟为间隔的仪表读数。随着时间的推移,这些表变得相当大,并且 Postgres 10 中范围分区的可用性激励我离开我的共享托管服务并在 Linode 从头开始​​构建服务器。在我将读数分成 2 周的块后,我对这些大表的查询变得更快。几个月后,我检查了查询计划,非常惊讶地发现在查询中使用 now() 会导致 PG 扫描我分区表上的所有索引。有没有搞错?!?!分区数据的目的不是为了避免这种情况吗?

这是我的设置:我的分区表

CREATE TABLE public.precip
(
    gauge_id smallint,
    inches numeric(8, 2),
    reading_time timestamp with time zone
) PARTITION BY RANGE (reading_time)

我每两周创建一次分区,所以到目前为止我有大约 50 个分区表。我的分区之一:

CREATE TABLE public.precip_y2017w48 PARTITION OF public.precip
    FOR VALUES FROM ('2017-12-03 00:00:00-05') TO ('2017-12-17 00:00:00-05');

然后每个分区在 gauge_id 和 reading_time 上建立索引

我有很多疑问,比如

WHERE gauge_id = xxx
  AND precip.reading_time > (now() - '01:00:00'::interval)
  AND precip.reading_time < now()

正如我所提到的,postgres 会为每个“子”表扫描 reading_time 上的所有索引,而不是仅查询在查询范围内具有时间戳的子表。如果我输入文字值(例如 precip.reading_time > '2018-03-01 01:23:00')而不是 now(),它只会扫描相应子表的索引。我已经阅读了一些内容,并且我知道 now() 是易变的,并且规划者不会知道查询执行时的值是什么。我还读到查询计划很昂贵,因此 postgres 缓存计划。我可以理解为什么 PG 被编程来做到这一点。但是,我读到的一个反驳论点是,重新计划的查询可能比最终忽略分区的查询便宜得多。我同意——在我的情况下可能就是这种情况。

作为一种变通方法,我创建了这个函数:

CREATE OR REPLACE FUNCTION public.hours_ago2(i integer)
RETURNS timestamp with time zone
LANGUAGE 'plpgsql'
COST 100
IMMUTABLE 
ROWS 0
AS $BODY$
    DECLARE X timestamp with time zone;

    BEGIN
            X:= now() + cast(i || ' hours' as interval);
            RETURN X;
    END;
$BODY$;

注意 IMMUTABLE 语句。现在,当发出类似的查询时

select * from stream where gauge_id = 2142 and reading_time > hours_ago2(-3) and reading_time < hours_ago2(0)

PG 只搜索存储该时间范围内数据的分区表。这是我一开始设置分区时的目标。嘘。但这安全吗?查询计划器是否会缓存 hours_ago2(-3) 的结果并在接下来的几个小时内一遍又一遍地使用它?缓存几分钟就好了。同样,我的应用程序报告降雨和流量信息;它不处理金融交易或任何其他“关键”类型的数据处理。我已经测试过像 select hours_ago2(-3) 这样的简单语句,它每次都会返回新值。所以看起来很安全。但真的是这样吗?

4

2 回答 2

2

这是不安全的,因为在计划时您不知道该语句是否会在同一个事务中执行。

如果您处于查询计划被缓存的情况,这将返回错误的结果。查询计划被缓存用于命名的预准备语句和 PL/pgSQL 函数中的语句,因此您最终可能会在数据库会话期间得到一个过时的值。

例如:

CREATE TABLE times(id integer PRIMARY KEY, d timestamptz NOT NULL);

PREPARE x AS SELECT * FROM times WHERE d > hours_ago2(1);

该函数在计划时进行评估,结果是执行计划中的一个常量(对于不可变函数来说很好)。

EXPLAIN (COSTS off) EXECUTE x;
                                QUERY PLAN                                 
---------------------------------------------------------------------------
 Seq Scan on times
   Filter: (d > '2018-03-12 14:25:17.380057+01'::timestamp with time zone)
(2 rows)

SELECT pg_sleep(100);

EXPLAIN (COSTS off) EXECUTE x;
                                QUERY PLAN                                 
---------------------------------------------------------------------------
 Seq Scan on times
   Filter: (d > '2018-03-12 14:25:17.380057+01'::timestamp with time zone)
(2 rows)

第二个查询肯定不会返回您想要的结果。

我认为您应该首先评估now()(或更好地在客户端使用等效函数),执行日期算术并将结果作为参数提供给查询。在 PL/pgSQL 函数内部,使用动态 SQL。

于 2018-03-12T12:34:48.247 回答
1

将查询更改为使用'now'::timestamptz而不是now(). 此外,区间数学timestamptz不是一成不变的。

将您的查询更改为:

WHERE gauge_id = xxx
  AND precip.reading_time > ((('now'::timestamptz AT TIME ZONE 'UTC')  - '01:00:00'::interval) AT TIME ZONE 'UTC')
  AND precip.reading_time < 'now'::timestamptz
于 2021-03-03T23:48:57.217 回答