Postgres SQL UDF 优化(不是内联?)

问题描述 投票:0回答:1

我有一个问题,我需要将表示结束日期的时间戳四舍五入到当前月末或上个月末,具体取决于这些时间戳相对于

NOW()
的位置(假设时间戳与
NOW()
在同一个月) 。我基本上只是将时间戳四舍五入到特定月份的末尾,并使用一些关于选择哪个月份的逻辑。我使用两个辅助函数来使我的日期数学变得更容易,一个将日期转换为月初,另一个将日期转换为该月的最后一天:

CREATE FUNCTION first_of_month(val date) RETURNS date
    IMMUTABLE
    STRICT
    PARALLEL SAFE
    LANGUAGE sql
RETURN (DATE_TRUNC('MONTH'::text, (val)::timestamp WITH TIME ZONE))::date;

CREATE FUNCTION last_of_month(val date) RETURNS date
    IMMUTABLE
    STRICT
    PARALLEL SAFE
    LANGUAGE sql
RETURN ((DATE_TRUNC('MONTH'::text, (val)::timestamp WITH TIME ZONE) + '1 mon -1 days'::interval))::date;

我在操作中调用的主要函数如下:

CREATE FUNCTION rounded_end(end_ts timestamp WITH TIME ZONE, now_ts timestamp WITH TIME ZONE) RETURNS date
    IMMUTABLE
    STRICT
    PARALLEL SAFE
    LANGUAGE sql
RETURN CASE
           WHEN ((first_of_month((now_ts)::date) = first_of_month((end_ts)::date)) AND (end_ts >= now_ts))
               THEN last_of_month((end_ts)::date)
           ELSE (first_of_month((end_ts)::date) - 1) END;

所有这些函数都是用 SQL 编写的,并标记为

IMMUTABLE
,目的是允许它们内联,根据 this wiki 中的信息。

但是,根据我是调用

rounded_end
还是手动内联它,我看到的性能结果截然不同。

例如,这个使用

rounded_end
的示例调用在本地需要 8-9 秒:

WITH timestamps AS (SELECT GENERATE_SERIES(timestamp '2014-01-10 20:00:00' +
                                           RANDOM() * (timestamp '2014-01-20 20:00:00' -
                                                       timestamp '2014-01-10 10:00:00'),
                                           timestamp '2025-01-10 20:00:00' +
                                           RANDOM() * (timestamp '2025-01-20 20:00:00' -
                                                       timestamp '2025-01-10 10:00:00'),
                                           '10 minutes') AS ts)
SELECT rounded_end(ts, NOW())
FROM timestamps;

虽然此示例调用手动内联

rounded_end
的主体,但运行时间不到 2 秒:

WITH timestamps AS (SELECT GENERATE_SERIES(timestamp '2014-01-10 20:00:00' +
                                           RANDOM() * (timestamp '2014-01-20 20:00:00' -
                                                       timestamp '2014-01-10 10:00:00'),
                                           timestamp '2025-01-10 20:00:00' +
                                           RANDOM() * (timestamp '2025-01-20 20:00:00' -
                                                       timestamp '2025-01-10 10:00:00'),
                                           '10 minutes') AS ts)
SELECT CASE
           WHEN ((first_of_month((NOW())::date) = first_of_month((ts)::date)) AND (ts >= NOW()))
               THEN last_of_month((ts)::date)
           ELSE (first_of_month((ts)::date) - 1) END
FROM timestamps;

带有重现和

ANALYZE
计时的 DB Fiddle 位于:https://dbfiddle.uk/CtQxpa3S。我正在 Postgres 15 上运行。

  • 什么给予?我可以在
    ANALYZE
    的结果中看到我的函数没有被内联,尽管调用堆栈中的所有依赖项都应该是
    IMMUTABLE
    ,除非我遗漏了某些内容。是什么阻止了内联?
  • 对于
    first_of_month
    last_of_month
    ,如果这是性能优化,我会非常乐意预先计算数据涵盖的时间范围内所有可能日期的映射到这些结果的样子。 Postgres 是否有执行此操作的策略,基本上构建 2014 年至 2025 年之间所有日期的哈希图以及它们各自的第一个月和最后一个月是什么?我知道我可以编写一个新表并执行
    JOIN
    ,但我想知道是否有比可用的
    JOIN
    更便宜的解决方案。

非常感谢您的帮助!

postgresql postgresql-performance postgresql-15
1个回答
0
投票

那是因为

date_trunc()
有几个变体,而您感兴趣的 3 个变体中只有 1 个是
immutable
:

\dfS+ date_trunc
姓名 结果数据类型 参数数据类型 波动性 并行 描述
日期_截断 间隔 文字、间隔 不变 安全 将间隔截断为指定单位
日期_截断 带时区的时间戳 文本、带时区的时间戳 稳定 安全 将带有时区的时间戳截断为指定单位
日期_截断 带时区的时间戳 文本、带时区的时间戳、文本 稳定 安全 将指定时区的带时区的时间戳截断为指定单位
日期_截断 无时区的时间戳 文本、无时区的时间戳 不可变 安全 将时间戳截断为指定单位

我的猜测是,当您“手动内联”时,规划器/优化器可能会看到您正在传递一个

date
并在
timestamp with timezone
调用中将其强制转换为
date_trunc()
,因此它会忽略这一点并使用不可变的
 timestamp withOUT timezone
变体。

由于您一路上要强制转换为时区不敏感的

::date
,因此您可以在辅助函数中强制转换为
timestamp withOUT timezone
并保持对规划者显而易见的不变性。虽然非内联版本需要 2121 毫秒,但执行时间现在下降到接近 276 毫秒,手动内联查询显示:演示

CREATE OR REPLACE FUNCTION first_of_month(val date) RETURNS date
    IMMUTABLE STRICT PARALLEL SAFE LANGUAGE sql
RETURN (DATE_TRUNC('MONTH'::text, (val)::timestamp WITHout TIME ZONE))::date;

CREATE OR REPLACE FUNCTION last_of_month(val date) RETURNS date
    IMMUTABLE STRICT PARALLEL SAFE LANGUAGE sql
RETURN ((DATE_TRUNC('MONTH'::text, (val)::timestamp WITHout TIME ZONE) + '1 mon -1 days'::interval))::date;

EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS)
WITH timestamps AS (
  SELECT GENERATE_SERIES(timestamp '2014-01-10 20:00:00' +
                         RANDOM() * (timestamp '2014-01-20 20:00:00' -
                                     timestamp '2014-01-10 10:00:00'),
                         timestamp '2015-01-10 20:00:00' +
                         RANDOM() * (timestamp '2025-01-20 20:00:00' -
                                     timestamp '2025-01-10 10:00:00'),
                        '1 hour') AS ts)
SELECT rounded_end(ts, NOW())
FROM timestamps;
查询计划
CTE 扫描时间戳(成本=5.03..280.03行=1000宽度=4)(实际时间=0.150..242.478行=8665循环=1)
  输出:rounded_end((timestamps.ts)::带时区的时间戳,now())
  CTE 时间戳
    -> ProjectSet(成本=0.00..5.03行=1000宽度=8)(实际时间=0.005..1.867行=8665循环=1)
          输出:generate_series(('2014-01-10 20:00:00'::没有时区的时间戳+(random() * '10天10:00:00'::interval)), ('2015-01 -10 20:00:00'::无时区时间戳 + (random() * '10 天 10:00:00'::interval)), '01:00:00'::interval)
          ->结果(成本=0.00..0.01行=1宽度=0)(实际时间=0.000..0.001行=1循环=1)
规划时间:0.100毫秒
执行时间:265.984 ms
© www.soinside.com 2019 - 2024. All rights reserved.