AdrianBR AdrianBR - 3 months ago 18
SQL Question

group consecutive time intervals in sql

Assuming a data structure of the type

stock_name, action, start_date, end_date
google, growing, 1, 2
google, growing, 2, 3
google, falling, 3, 4
google, growing, 4, 5
yahoo, growing, 1, 2


How can I aggregate it to merge consecutive time intervals?

The output would look like:

stock_name, action, start_date, end_date
google, growing, 1, 3
google, falling, 3, 4
google, growing, 4, 5
yahoo, growing, 1, 2


I thought of using rank window function to number the consecutive with a constant and then grouping by that and action/name, but I cannot quite get it to work, something as below:

stock_name, action, start_date, end_date, rank
google, growing, 1, 2, 1
google, growing, 2, 3, 1
google, falling, 3, 4, 1
google, growing, 4, 5, 2
yahoo, growing, 1, 2, 1


If this were Mysql, I would easily solve it with variables, but this is not possible in postgres.

There could be any number of consecutive intervals, so self joining a predetermined nr of times is not an option.

Elegance(performance, readability) of solution matters.

Answer

You can use variables just fine in PL/pgSQL.

I would solve this with a table function.

Assuming the table is called stock, my code would look like this:

CREATE OR REPLACE FUNCTION combine_periods() RETURNS SETOF stock
   LANGUAGE plpgsql STABLE AS
$$DECLARE
   s stock;
   period stock;
BEGIN
   FOR s IN
      SELECT stock_name, action, start_date, end_date
      FROM stock
      ORDER BY stock_name, action, start_date
   LOOP
      /* is this a new period? */
      IF period IS NOT NULL AND
         (period.stock_name <> s.stock_name
            OR period.action <> s.action
            OR period.end_date <> s.start_date)
      THEN
         /* new period, output last period */
         RETURN NEXT period;
         period := NULL;
      ELSE
         IF period IS NOT NULL
         THEN
            /* period continues, update end_date */
            period.end_date := s.end_date;
         END IF;
      END IF;

      /* remember the beginning of a new period */
      IF period IS NULL
      THEN
         period := s;
      END IF;
   END LOOP;

   /* output the last period */
   IF period IS NOT NULL
   THEN
      RETURN NEXT period;
   END IF;

   RETURN;
END;$$;

And I would call it like this:

test=> SELECT * FROM combine_periods();
┌────────────┬─────────┬────────────┬──────────┐
│ stock_name │ action  │ start_date │ end_date │
├────────────┼─────────┼────────────┼──────────┤
│ google     │ falling │          3 │        4 │
│ google     │ growing │          1 │        3 │
│ google     │ growing │          4 │        5 │
│ yahoo      │ growing │          1 │        2 │
└────────────┴─────────┴────────────┴──────────┘
(4 rows)