【问题标题】:How to speed up custom window function with arrays and loops in PostgreSQL?如何在 PostgreSQL 中使用数组和循环加速自定义窗口函数?
【发布时间】:2020-09-05 18:02:11
【问题描述】:

我目前正在学习 UDF,并在下面编写了 PostgreSQL UDF 来计算平均偏差 (MAD)。它是任何窗口的平均值和当前值之间的平均绝对差。 在 python pandas/numpy 中,要找到 MAD,我们可以这样写:

series_mad = abs(series - series.mean()).mean()

其中 series 是一组数字,而 series_mad 是表示该系列的 MAD 的单个数值。

我正在尝试使用 Windows 和 UDF 在 PostgreSQL 中编写此代码。到目前为止,这就是我所拥有的:

CREATE TYPE misc_tuple AS (
    arr_store numeric[],
    ma_period integer
);

CREATE OR REPLACE FUNCTION mad_func(prev misc_tuple, curr numeric, ma_period integer)
    RETURNS misc_tuple AS $$
    BEGIN
        IF curr is null THEN
            RETURN (null::numeric[], -1);
        ELSEIF prev.arr_store is null THEN
            RETURN (ARRAY[curr]::numeric[], ma_period);
        ELSE
            -- accumulate new values in array
            prev.arr_store := array_append(prev.arr_store, curr);
            RETURN prev;
        END IF;
    END;
    $$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION mad_final(prev misc_tuple)
    RETURNS numeric AS $$
    DECLARE
        total_len integer;
        count numeric;
        mad_val numeric;
        mean_val numeric;
    BEGIN
        count := 0;
        mad_val := 0;
        mean_val := 0;
        total_len := array_length(prev.arr_store, 1);
        -- first loop to find the mean of the series
        FOR i IN greatest(1,total_len-prev.ma_period+1)..total_len
        LOOP 
            mean_val := mean_val + prev.arr_store[i];
            count := count + 1;
        END LOOP;
        mean_val := mean_val/NULLIF(count,0);
        -- second loop to subtract mean from each value 
        FOR i IN greatest(1,total_len-prev.ma_period+1)..total_len
        LOOP 
            mad_val := mad_val + abs(prev.arr_store[i]-mean_val);
        END LOOP;
        RETURN mad_val/NULLIF(count, 0);
    END;
    $$ LANGUAGE plpgsql;

CREATE OR REPLACE AGGREGATE mad(numeric, integer) (
    SFUNC = mad_func,
    STYPE = misc_tuple,
    FINALFUNC = mad_final
);

这是我测试性能的方式:

-- find rolling 12-period MAD
SELECT x,
       mad(x, 12) OVER (ROWS 12-1 PRECEDING)
FROM generate_series(0,1000000) as g(x);

目前,在我的桌面(i5 4670、3.4 GHz、16 GB RAM)上大约需要 45-50 秒。我仍在学习 UDF,所以我不确定我还能对我的函数做些什么来让它更快。我还有其他一些类似的 UDF - 但不使用数组的 UDF 在相同的 1m 行上花费

我可以对此 UDF 进行任何更改以使其更快吗?

【问题讨论】:

  • 请以文本形式提供样本测试日期、无图片和预期结果。
  • @Belayer 我的帖子中没有图片。问题不在于查询的结果,而在于性能。
  • 我的要求是测试数据和预期结果。请求不是关于问题中的内容,而是问题中没有的内容。将其发布为文本。另外描述您要解决的问题;不仅仅是代码。
  • @Belayer 很抱歉,但我不确定我还能如何澄清。不需要测试数据,generate_series 已经为您完成了。我试图解决的问题是加快我在帖子中提到的查询。我还提到了 UDF 计算的内容以及我对代码为何缓慢的假设。因为我只是在学习 UDF,所以我不确定还有哪些其他技术可以用来加速我的代码并寻求建议——这就是问题所在。预期结果与我的代码输出的结果相同,但运行时间要快得多。

标签: sql postgresql user-defined-functions window-functions


【解决方案1】:

您的示例代码不起作用,您在类型定义中有一个额外的逗号,并且您在其中一个函数中使用了未定义的变量 cnt

您为什么将 12 既指定为聚合本身的参数,又指定为 ROWS PRECEDING 中的参数?这似乎是多余的。

您与 numpy 的比较似乎不太恰当,因为那不是滑动窗口函数。

我还有一些其他类似的 UDF - 但是不使用数组的 UDF,它们在相同的 1m 行上花费

它们是否也用作滑动窗口功能?也是用plpgsql写的?你能展示一个和它的用法吗?

pl/pgsql 通常不是一种非常有效的语言,尤其是在处理大型数组时。尽管在您的使用中,数组永远不会变得非常大,所以我认为这不会是一个特别大的问题。

提高效率的一种方法是用 C 而不是 pl/pgsql 编写代码,使用 INTERNAL 数据类型而不是 SQL 复合类型。

另一种改进这种特殊用法的方法(大量窗口,每个窗口都很小)可能是为此聚合实现MINVFUNC function and friends,这样它就不必每次都从头开始重新启动聚合行。

这是一个反函数示例,它根本不改变输出,但确实将运行时间缩短了大约一半:

CREATE OR REPLACE FUNCTION mad_invfunc(prev misc_tuple, curr numeric, ma_period integer)
    RETURNS misc_tuple AS $$
    BEGIN
            -- remove prev value
            prev.arr_store := prev.arr_store[2:];
            RETURN prev;
    END;
    $$ LANGUAGE plpgsql;
CREATE OR REPLACE AGGREGATE mad(numeric, integer) (
    SFUNC = mad_func,
    STYPE = misc_tuple,
    FINALFUNC = mad_final,
    MSFUNC = mad_func,
    MSTYPE = misc_tuple,
    MFINALFUNC = mad_final,
    MINVFUNC = mad_invfunc
);

如果我将类型从numeric 更改为double precision,它们会再次将运行时间缩短一半。因此,虽然数组上的循环可能效率不高,但仅使用 12 个成员的窗口时,它们并不是主要瓶颈。

【讨论】:

  • 对错误感到抱歉。我现在已经修好了。 - 我使用 12 作为参数的原因是因为它仅用于选择最后一个 period 值,然后在找出平均值时除以 period。 - 它并不完全是 numpy(再次抱歉),而是我上面提到的功能可以通过使用 series.rolling(5).apply(<mean fn>, raw=True) 在 pandas 中应用。
  • - 其他功能确实使用滑动窗口 - 我从 this 帖子中学习了如何实现简单的 SMA。它们是用 plpgsql 编写的,但没有循环,因此运行速度很快。编辑:我提到的其他函数使用了 MINVFUNC,但我不确定如何将它与数组一起使用。
  • 这里是 link 到 MAD 统计的 pandas 实现。
  • 我已经编辑了一个反函数的例子。
  • 您链接到的 pl/pgsql 定义的 average 函数比内置的 avg 函数慢大约 4 倍。对于这种情况,我希望 pl/pgsql 和 C 实现之间存在类似的差异。但是实现 C 语言的工作量很大,尤其是如果您以前没有对 PostgreSQL 进行过 C 语言扩展。而且它可能永远不会像 numpy 那样快,专注于管道来来回移动数据可能更有意义。
猜你喜欢
  • 1970-01-01
  • 2017-03-17
  • 1970-01-01
  • 1970-01-01
  • 2020-02-28
  • 2021-05-30
  • 2012-11-27
  • 2018-12-15
  • 1970-01-01
相关资源
最近更新 更多