使用 PostgreSQL 用户定义函数实现类似窗口函数的行为?

Posted

技术标签:

【中文标题】使用 PostgreSQL 用户定义函数实现类似窗口函数的行为?【英文标题】:Achieving window function-like behavior using a PostgreSQL user defined function? 【发布时间】:2020-03-01 04:03:52 【问题描述】:

假设给定一个表observations_tbl 具有属性date(天)和value,我想生成新属性prev_day_value 以获取下表:

|---------------------|-------|----------------|
|     date            | value | prev_day_value |
|---------------------|-------|----------------|
| 01.01.2015 00:00:00 | 5     | 0              |
| 02.01.2015 00:00:00 | 4     | 5              |
| 03.01.2015 00:00:00 | 3     | 4              |
| 04.01.2015 00:00:00 | 2     | 3              |
|---------------------|-------|----------------|

我很清楚这样的输出通常可以使用WINDOW 函数获得。但是我如何通过 PostgreSQL 用户定义的函数来实现这一点?我想表明我处于必须使用函数的情况,如果不详细说明,很难解释为什么 - 这些是我的限制,如果有的话,这是一个技术挑战。

考虑这个模板查询:

SELECT *, lag(value,1) AS prev_day_value -- or lag(record,1) or lag(date,value,1) or lag(date,1) or lag(observations_tbl,1), etc.
FROM observations_tbl

我正在使用带有参数1 的函数lag 来查找在当前行之前1 的值 - 1 行的距离。我不在乎函数lag 可以有哪些其他参数(表名、其他属性)- 函数lag 看起来像什么来实现这样的功能?该函数可以是任何语言,SQLPL/pgSQL 甚至使用 PostgreSQL API/后端的C

我知道一个答案可以是在 lag 用户定义的函数中包装 WINDOW 查询。但我认为,如果我必须扫描整个表两次(一次在 lag 函数内,一次在函数外),那将是一项相当昂贵的操作。我在想,也许每条 PostgreSQL 记录都会有一个指向它以前可以直接访问的记录的指针?或者我可以以某种方式在这个特定的行/行号处打开一个游标,而不必扫描整个表?或者我的要求是不可能的?

【问题讨论】:

尝试使用用户定义的函数来复制内置函数的效率似乎——我应该说——具有挑战性。 你确定PostgreSQL没有办法解决这个要求吗? 。 .不,我不知道。您可以围绕内部定义编写一个包装器。但是这样做的需要有些不对劲。这似乎是一个 X-Y 问题,您关注的是错误的细节。 您可以将功能内联到您的存储过程中,直到找到更好的方法来定位您的数据:/ 如果您出于某些奇怪的原因“必须”使用函数,那么为什么不简单地将使用窗口函数的查询封装到 SQL 函数中呢? 【参考方案1】:

您的请求无法使用关系工具来解决(窗口函数不是 SQL 中的关系扩展)。在 C 语言中,您可以编写自己的函数滞后替代方案。你可以用 PL8 语言(javascript)做同样的工作。不幸的是,PL/pgSQL 不存在窗口函数的 API。您不能编写简单的 PL/pgSQL 函数来访问与处理不同的行。

一种可能的替代方法(但存在一些性能风险)是编写表函数。您可以控制所有已处理的数据集,并且可以简单地执行此操作。

CREATE OR REPLACE FUNCTION report()
RETURNS TABLE(d date, v int, prev_v int) $$
DECLARE r RECORD;
BEGIN
  prev_v := 0;
  FOR r IN SELECT date, value FROM observations_tbl t ORDER BY 1
  LOOP
    d := r.date; v := r.value;
    RETURN NEXT;
    prev_v := v;
  END LOOP;
END;
$$ LANGUAGE plpgsql;

没有其他可用的替代解决方案。在非常古老的年代,这些值是通过相关的自联接计算得出的,但这种解决方案的性能非常糟糕。

【讨论】:

窗口函数不是 SQL 中的关系扩展” 到底是什么意思? 关系代数基于集合。集合内字段的顺序并不重要。您可以对这些集合进行一些操作 - 并集、乘法、子集......窗口函数基于不同的基础 - 顺序很重要。 关于窗口函数不是 SQL 的关系扩展的论点是完全错误的 - 请参阅 ISO SQL:2003 和 ISO SQL:2008。您说得对,Codd 博士 1970 年的原始论文(参见:Communications of the ACM,第 13 卷,第 6 期,377-387)没有包含它们,它是关于存储和检索的。进一步的窗口函数都是 Post Retrieval,所以你的关系代数就完成了。您是否还争辩说 ORDER BY 不是有效的 SQL 或其他正常操作。 @PavelStehule 在您的示例中,您指定了返回类型。我想知道是否有办法在函数体中指定返回类型? @Zeruno - 返回类型应该只在接口和计划时间定义。 Postgres 是类型严格的系统。【参考方案2】:

什么Pavel posted,只是任务更少。应该更快:

CREATE OR REPLACE FUNCTION report()
  RETURNS TABLE(d date, v int, prev_v int) AS
$func$
BEGIN
   prev_v := 0;
   FOR d, v IN
      SELECT date, value FROM observations_tbl ORDER BY 1
   LOOP
      RETURN NEXT;
      prev_v := v;
   END LOOP;
END
$func$  LANGUAGE plpgsql;

如果它实际上将表上的多次扫描替换为单个扫描,则一般的想法是可以支付的。喜欢这里:

GROUP BY and aggregate sequential numeric values

【讨论】:

以上是关于使用 PostgreSQL 用户定义函数实现类似窗口函数的行为?的主要内容,如果未能解决你的问题,请参考以下文章

MySQL用户自定义函数生成主键

Postgresql 用户定义的聚合函数计数

ORACLE ---- 和PostgreSQL继承表类似的实现方式

在JS中自定义函数并实现类似 $.messager.confirm(title, msg, fn)功能,请附代码。

函数如何将数组或设置值作为 PostgreSQL 中用户定义函数的参数 (IN)

PostgreSQL之SQL函数介绍及实践