SQL 替代 Postgre 中的 FOLLOWING / PRECEEDING 的解决方法SQL 8.4

SQL workaround to substitute FOLLOWING / PRECEEDING in PostgreSQL 8.4

我有一个使用 PostgreSQL 9.0 的 FOLLOWING / PRECEDING 语法计算基本移动平均线的查询。令我震惊的是,我发现我们的 pg 服务器运行在 8.4 上,并且在不久的将来没有升级的余地。

因此,我正在寻找对以下内容进行向后兼容查询的最简单方法:

SELECT time_series,
       avg_price AS daily_price,
       CASE WHEN row_number() OVER (ORDER BY time_series) > 7 
        THEN avg(avg_price) OVER (ORDER BY time_series DESC ROWS BETWEEN 0 FOLLOWING
                                                                     AND 6 FOLLOWING)
        ELSE NULL 
       END AS avg_price
FROM (
   SELECT to_char(closing_date, 'YYYY/MM/DD') AS time_series,
          SUM(price) / COUNT(itemname) AS avg_price
   FROM auction_prices 
   WHERE itemname = 'iphone6_16gb' AND price < 1000
   GROUP BY time_series
   ) sub

它是 table 的基本 7 天移动平均线,包含价格和时间戳列:

closing_date timestamp
price        numeric
itemname     text

对基础的要求是由于我对 SQL 的基础知识。

PostgreSQL 8.4.... 那个时代不是每个人都认为 Windows 95 很棒吗?无论如何...

我能想到的唯一选择是使用带有可滚动游标的存储过程并手动进行数学计算:

CREATE FUNCTION auction_prices(item text, price_limit real)
  RETURNS TABLE (closing_date timestamp, avg_day real, avg_7_day real) AS $$
DECLARE
  last_date  date;
  first_date date;
  cur        refcursor;
  rec        record;
  dt         date;
  today      date;
  today_avg  real;
  p          real;
  sum_p      real;
  n          integer;
BEGIN
  -- There may be days when an item was not traded under the price limit, so need a
  -- series of consecutive days to find all days. Find the end-points of that
  -- interval.
  SELECT max(closing_date), min(closing_date) INTO last_date, first_date
  FROM auction_prices
  WHERE itemname = item AND price < price_limit;

  -- Need at least some data, so quit if item was never traded under the price limit.
  IF NOT FOUND THEN
    RETURN;
  END IF;

  -- Create a scrollable cursor over the auction_prices daily average and the
  -- series of consecutive days. The LEFT JOIN means that you will get a NULL
  -- for avg_price on days without trading.
  OPEN cur SCROLL FOR
    SELECT days.dt, sub.avg_price
    FROM generate_series(last_date, first_date, interval '-1 day') AS days(dt)
    LEFT JOIN (
      SELECT sum(price) / count(itemname) AS avg_price
      FROM auction_prices 
      WHERE itemname = item AND price < price_limit
      GROUP BY closing_date
    ) sub ON sub.closing_date::date = days.dt::date;

  <<all_recs>>
  LOOP -- over the entire date series
    -- Get today's data (today = first day of 7-day period)
    FETCH cur INTO today, today_avg;
    EXIT all_recs WHEN NOT FOUND; -- No more data, so exit the loop
    IF today_avg IS NULL THEN
      n := 0;
      sum_p := 0.0;
    ELSE
      n := 1;
      sum_p := today_avg;
    END IF;

    -- Loop over the remaining 6 days
    FOR i IN 2 .. 7 LOOP
      FETCH cur INTO dt, p;
      EXIT all_recs WHEN NOT FOUND; -- No more data, so exit the loop
      IF p IS NOT NULL THEN
        sum_p := sum_p + p;
        n := n + 1;
      END IF;
    END LOOP;

    -- Save the data to the result set
    IF n > 0 THEN
      RETURN NEXT today, today_avg, sum_p / n;
    ELSE
      RETURN NEXT today, today_avg, NULL;
    END IF;

    -- Move the cursor back to the starting row of the next 7-day period
    MOVE RELATIVE -6 FROM cur;
  END LOOP all_recs;
  CLOSE cur;

  RETURN;
END; $$ LANGUAGE plpgsql STRICT;

一些注意事项:

  • 有些日期可能会出现商品不低于限价交易的情况。为了获得准确的移动平均线,您需要包括那些日子。生成一系列连续的日期,在此期间该项目确实以限价交易,您将获得准确的结果。
  • 光标需要可滚动,以便您可以向前看 6 天以获取计算所需的数据,然后向后移动 6 天以计算下一天的平均值。
  • 您无法计算最近 6 天的移动平均线。原因很简单,MOVE 命令需要移动固定数量的记录。不支持参数替换。从好的方面来说,您的移动平均线将始终为 7 天(其中可能并非所有人都进行过交易)。
  • 这个功能绝对不会很快,但应该可以。虽然不能保证,但我已经多年没有在 8.4 盒子上工作了。

此功能的使用非常简单。因为它返回一个 table 你可以在 FROM 子句中使用它就像任何其他 table (甚至 JOIN 到其他关系):

SELECT to_char(closing_date, 'YYYY/MM/DD') AS time_series, avg_day, avg_7_day
FROM auction_prices('iphone6_16gb', 1000);

Postgres 8.4 already has CTEs.
我建议使用它,计算 CTE 中的每日平均值,然后自加入过去一周的所有天数(存在或不存在)。最后,再次汇总每周平均值:

WITH cte AS (
   SELECT closing_date::date AS closing_day
        , sum(price)   AS day_sum
        , count(price) AS day_ct
   FROM   auction_prices
   WHERE  itemname = 'iphone6_16gb'
   AND    price <= 1000  -- including upper border
   GROUP  BY 1
   )   
SELECT d.closing_day
     , CASE WHEN d.day_ct > 1
            THEN d.day_sum / d.day_ct
            ELSE d.day_sum
       END AS avg_day         -- also avoids division-by-zero
     , CASE WHEN sum(w.day_ct) > 1
            THEN sum(w.day_sum) / sum(w.day_ct)
            ELSE sum(w.day_sum)
       END AS week_avg_proper  -- also avoids division-by-zero
FROM   cte d
JOIN   cte w ON w.closing_day BETWEEN d.closing_day - 6 AND d.closing_day
GROUP  BY d.closing_day, d.day_sum, d.day_ct
ORDER  BY 1;

SQL Fiddle.(运行 在 Postgres 9.3 上,但也应该在 8.4 中工作。)

备注

  • 我使用了不同的(正确的)算法来计算周平均值。请参阅我的 .

    中的注意事项
  • 这会计算基础 table 中 天的平均值,包括极端情况。但是几天没有任何一行。

  • 可以从 date 中减去 integerd.closing_day - 6。 (但不是来自 varchartimestamp!)

  • timestamp 列称为 closing_date 相当令人困惑 - 它不是 date,而是 timestamp。 对于具有 date 值的结果列,time_series?我改用 closing_day ...

  • 请注意我如何计算价格 count(price)不是 商品 COUNT(itemname) -如果任一列可以为 NULL,则将成为偷偷摸摸的错误的入口点。如果 都不能为 NULL,count(*) 会更好。

  • CASE 构造避免了被零除错误,只要您正在计算的列 可以 为 NULL,就会发生这种错误。我可以使用 COALESCE 来达到这个目的,但在使用它的同时,我也简化了这个案例,正好是 1 个价格。

        -- make a subset and rank it on date
WITH xxx AS (
        SELECT
        rank() OVER(ORDER BY closing_date) AS rnk
        , closing_date
        , price
        FROM auction_prices
        WHERE itemname = 'iphone6_16gb' AND price < 1000
        )
        -- select subset, + aggregate on self-join
SELECT this.*
        , (SELECT AVG(price) AS mean
                FROM xxx that
                WHERE that.rnk > this.rnk + 0 -- <<-- adjust window
                AND that.rnk < this.rnk + 7   -- <<-- here
                )
FROM xxx this
ORDER BY this.rnk
        ;
  • 注意:CTE 是为了方便起见(Postgres-8.4 确实有 CTE),但 CTE 可以用子查询或更优雅地用视图代替。
  • 代码假定时间序列没有间隙(:每个 {product*day} 一次观察。如果不是:加入日历 table(也可以包含排名。)
  • (另请注意,我没有涵盖边角案例。)