10

I'm writing a function in PostgreSQL 9.2.

For a table of stock prices and dates, I want to calculate the percentage change from the previous day for each entry. For the earliest day of data, there won't be a previous day, so that entry can simply be Nil.

I'm aware that the WITH statement is probably not supposed to be above the IF statement. This is just how I've thought about it logically so far:

CREATE FUNCTION percentage_change_func(asset_histories)
  RETURNS numeric LANGUAGE sql IMMUTABLE AS
$func$
DECLARE
   r asset_histories%rowtype
BEGIN   
   WITH twodaysdata AS (SELECT date,price,asset_symbol FROM asset_histories 
           WHERE asset_symbol = $1.asset_symbol 
           AND asset_histories.date <= $1.date 
           ORDER BY date DESC LIMIT 2), 
         numberofrecords AS (SELECT count(*) FROM twodaysdata) 

   IF numberofrecords = 2 THEN
        RETURN r.price / (r+1).price - 1  <---How do I reference r + 1??/
   ELSE
        RETURN NIL
   ENDIF
END
$func$;

How to reference data from the next row properly?

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
Terence Chow
  • 10,755
  • 24
  • 78
  • 141

2 Answers2

15

I want to calculate the percentage change from the previous day for each entry

Study the basics first. Read the excellent manual about CREATE FUNCTION, PL/pgSQL and SQL functions.

Major points why the example is nonsense

  • You cannot hand in an identifier like you do. Identifiers cannot be parameterized in plain SQL. You'd need dynamic SQL for that.
    Of course, not actually needed for your objective. There is only one table involved. Would be nonsense to parameterize it.

  • Don't use type names as identifiers. I use _date instead of date as parameter name and renamed your table column to asset_date. ALTER your table definition accordingly.

  • A function fetching data from a table is never IMMUTABLE. Don't declare it as such. Read the manual.

  • You are mixing SQL syntax with PL/pgSQL elements in nonsensical ways. WITH is part of an SELECT statement and cannot be mixed with PL/pgSQL control structures like LOOP or IF.

Sanitized function

A proper function could look like this (one of many ways):

CREATE FUNCTION percentage_change_func(_asset_symbol text)
  RETURNS TABLE(asset_date date, price numeric, pct_change numeric)
  LANGUAGE plpgsql STABLE AS
$func$
DECLARE
   last_price numeric;
BEGIN
   FOR asset_date, price IN
      SELECT a.asset_date, a.price  -- table-qualify to disambiguate!
      FROM   asset_histories a
      WHERE  a.asset_symbol = _asset_symbol 
      ORDER  BY a.asset_date  -- traverse ascending
   LOOP
      pct_change := price / last_price;  -- NULL if last_price is NULL
      RETURN NEXT;
      last_price := price;
   END LOOP;
END
$func$;

Performance shouldn't be so bad, but still pointless complication.

Proper solution: plain query

The simplest (and probably fastest) way would be with the window function lag():

SELECT asset_date, price
     , price / lag(price) OVER (ORDER BY asset_date) AS pct_change
FROM   asset_histories
WHERE  asset_symbol = _asset_symbol 
ORDER  BY asset_date;

Standard deviation

As per your later comment, you want to calculate statistical numbers like standard deviation.
Postgres has dedicated aggregate functions for statistics.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
  • 2
    @Ervin, thanks for showing the usage of lag(), was not aware of it – Akash Mar 30 '13 at 08:27
  • @Akash: Window functions are pretty versatile tools. – Erwin Brandstetter Mar 30 '13 at 08:30
  • @ErwinBrandstetter So window functions don't work in a view calling an aggregate? eg. Create or Replace View tmp AS Select timestmp, sum(price) AS this_wk_price, lag(this_wk_price,1) from... group by ... order by ... ; – Fast Engy Aug 11 '15 at 00:01
  • @FastEngy: Not sure how you would infer that from my answer. It's possible to combine window and aggregate functions in the same SELECT. Please ask an actual *question* providing the necessary context for your question. – Erwin Brandstetter Aug 11 '15 at 09:38
  • Thanks @ErwinBrandstetter- nothing to do with your answer just building more info as haven't used lag before due to our server version being older. One of my tests ran into an issue but getting it sorted. – Fast Engy Aug 12 '15 at 22:34
0

Simple things like just calculating the per_change, can be done within a view instead, this would also result in faster results

create view view_stock_details AS ( SELECT 
    date, 
    price, 
    symbol, 
    pervious_day_close, 
    (price-pervious_day_close)/pervious_day_close*100 as per_change 
FROM (
    SELECT
        date,
        price,
        symbol,
        ( SELECT price FROM asset_histories t WHERE t.symbol = outers.symbol AND t.date < outers.date limit 1 ) as pervious_day_close
    FROM 
        asset_histories as outers
    ); 

To view the stock details, you can then use

SELECT 
    *
FROM
    view_stock_details
WHERE
    date = '2012-01-03' 
    AND symbol = 'A'
Akash
  • 4,956
  • 11
  • 42
  • 70
  • The real purpose of getting percent change is to find the standard deviation of percent changes over the last 20 records. I plan on displaying standard deviation in a graph. Therefore I used `create function` so that I could subsequently write queries calculating off that new column. Would it be better to write a view for std dev using a subquery like yours which calcates percent_changes? It seems like that would be awfully complicated. Also, with my needs in mind, what would you say is the fastest way for getting data to the user? A function that has precalculated the value or a view? – Terence Chow Mar 30 '13 at 07:05
  • 2
    @Chowza: Obviously, this should be in the question. Don't make people work for you, just to reveal your actual intentions later. – Erwin Brandstetter Mar 30 '13 at 08:19
  • @ErwinBrandstetter Sorry about doing one thing and then another. I was thinking 1 step at a time and didn't realize there was a way to do both steps in one. I'll make sure i'm more clear in the future and thanks for all your help recently! – Terence Chow Mar 30 '13 at 19:55