Search code examples
sqlpostgresqlsubquerywindow-functions

Simplify a chain of computations


I would like to make a query similar to this:

SELECT
  time,
  close - LAG(close) OVER (ORDER BY time) AS "diff"
  CASE WHEN diff > 0 THEN diff ELSE 0 END AS gain,
  CASE WHEN diff < 0 THEN diff ELSE 0 END AS loss,
  AVG(gain) OVER (ORDER BY time ROWS 40 PRECEDING) as avg_gain,
  AVG(loss) OVER (ORDER BY time ROWS 40 PRECEDING) AS avg_loss
  avg_gain / avg_loss AS rs,
  100 - (100 / NULLIF(1+rst.rs, 0)) as rsi
FROM candles_5min
WHERE symbol = 'AAPL';

But from what I can tell, SQL doesn't allow references to columns created within the same SELECT. So I have to do something like:

SELECT rst.time, 100 - (100 / NULLIF((1+rst.rs), 0)) as rsi
FROM (SELECT
avgs.time,
avgs.avg_gain / NULLIF(avgs.avg_loss, 0) AS rs
FROM (SELECT glt.time, AVG(glt.gain) OVER (ORDER BY time ROWS 40 PRECEDING) as avg_gain,
    AVG(glt.loss) OVER (ORDER BY time ROWS 40 PRECEDING) AS avg_loss
FROM (SELECT
    dt.time,
    CASE WHEN dt.diff > 0 THEN dt.diff ELSE 0 END AS gain,
    CASE WHEN diff < 0 THEN diff ELSE 0 END AS loss
    FROM (SELECT
    time,
    close - LAG(close) OVER (ORDER BY time) AS "diff"
    FROM candles_5min
    WHERE symbol = 'AAPL') AS dt) AS glt) AS avgs) AS rst

Is there any way to simplify a query like this one? I'm using PostgreSQL.


Solution

  • There is a window function based on another window function, which cannot be nested in the same SELECT list. So you need a minimum of two query levels. But that minimum seems feasible:

    SELECT time
         , 100 - (100 / NULLIF((1 + AVG(GREATEST(diff, 0)) OVER w
                              / NULLIF(AVG(LEAST(diff, 0)) OVER w, 0)), 0)) AS rsi
    FROM  (
       SELECT time
            , close - LAG(close) OVER (ORDER BY time) AS diff
       FROM   candles_5min
       WHERE  symbol = 'AAPL'
       ) dt
    WINDOW w AS (ORDER BY time ROWS 40 PRECEDING);
    

    fiddle

    You can replace the lengthy CASE expressions with dedicated (100 % equivalent) GREATEST() and LEAST().

    You can use a WINDOW clause to avoid spelling out the same frame for multiple window functions in the same SELECT list. Results in the same query plan, so no effect on performance. The manual:

    ... the WINDOW clause saves typing when the same window definition is needed for more than one window function.

    Overall, I expect my rewrite to be only slightly faster. But you asked for a simplified query, not performance, so that's ok.