Search code examples
sqloracle-databasegroup-bycontiguouspartition-by

How to group by contiguous data only in Oracle


I'm trying to display how many cages for each rate and the start and end date. This is going into an invoice where we bill for each cage per day and these cages can have different rates.

It isn't a simple GROUP BY and getting the MIN and MAX dates since the number of cages can go down or up and then back to the same number again so I need to only look at contiguous data.

I searched for a solution and found this answer. I modified it a little bit to suit my needs and came up with this:

WITH cte(rate_name, cages, use_date) AS (
  SELECT 'I1',      8, DATE'2017-11-04' FROM DUAL UNION ALL
  SELECT 'I1',      8, DATE'2017-11-05' FROM DUAL UNION ALL
  SELECT 'I1',      7, DATE'2017-11-07' FROM DUAL UNION ALL
  SELECT 'I1',      7, DATE'2017-11-10' FROM DUAL UNION ALL
  SELECT 'I1',      7, DATE'2017-11-11' FROM DUAL UNION ALL
  SELECT 'I1',      8, DATE'2017-11-12' FROM DUAL UNION ALL
  SELECT 'I1',      8, DATE'2017-11-13' FROM DUAL UNION ALL
  SELECT 'I1',      8, DATE'2017-11-14' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-01' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-02' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-03' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-04' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-05' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 1, DATE'2017-11-06' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 1, DATE'2017-11-07' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 1, DATE'2017-11-08' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-09' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-10' FROM DUAL UNION ALL
  SELECT 'I1 - BR', 5, DATE'2017-11-11' FROM DUAL
)

SELECT
  a.rate_name,
  a.cages,
  MIN(a.use_date) AS startdate,
  MAX(a.use_date) AS enddate
FROM (
  SELECT
    cte.use_date,
    cte.rate_name,
    cte.cages,
    ROW_NUMBER() OVER (ORDER BY cte.rate_name, cte.use_date) - ROW_NUMBER() OVER (PARTITION BY cte.rate_name, cte.cages ORDER BY cte.use_date) AS grp
  FROM cte
) a
GROUP BY a.rate_name, a.cages, a.grp
ORDER BY a.rate_name ASC, 3;

I ran this query within PL/SQL and it seems to work perfectly for what I'm trying to do. When I tried plugging in the solution in the software tool that we're using it turns out it doesn't support ROW_NUMBER(), OVER, and PARTITION BY.

Is there a way I could achieve the same results without having to use those built-in functionalities?

I've started looking at implementing the ROW_NUMBER() manually and found this approach. It seems to work when I tested it but I haven't really plugged it in. I'm now a bit stuck with implementing PARTITION BY and I just feel a little lost and don't know whether I'm heading in the right direction here.

EDIT

I just noticed that the query returns an incorrect result.

For I1 with 7 cages there should be 2 rows returned. The first row starts and ends on 2017-11-07 while the second row start on 2017-11-10 and ends on 2017-11-11.


Solution

  • Well, this is a little awkward, but it's the first thing I thought of. I'm sure it can be cleaned up. I had to make a second-level CTE to get it to work without any functions or anything. Probably would've been easier with connect by or lag, but I'm guessing your software tool can't handle those either.

    WITH cte(rate_name, cages, use_date) AS (
      SELECT 'I1',      8, DATE'2017-11-04' FROM DUAL UNION ALL
      SELECT 'I1',      8, DATE'2017-11-05' FROM DUAL UNION ALL
      SELECT 'I1',      7, DATE'2017-11-07' FROM DUAL UNION ALL
      SELECT 'I1',      7, DATE'2017-11-10' FROM DUAL UNION ALL
      SELECT 'I1',      7, DATE'2017-11-11' FROM DUAL UNION ALL
      SELECT 'I1',      8, DATE'2017-11-12' FROM DUAL UNION ALL
      SELECT 'I1',      8, DATE'2017-11-13' FROM DUAL UNION ALL
      SELECT 'I1',      8, DATE'2017-11-14' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-01' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-02' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-03' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-04' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-05' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 1, DATE'2017-11-06' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 1, DATE'2017-11-07' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 1, DATE'2017-11-08' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-09' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-10' FROM DUAL UNION ALL
      SELECT 'I1 - BR', 5, DATE'2017-11-11' FROM DUAL
    ),
    recur as (
      SELECT
        c1.use_date,
        c1.rate_name,
        c1.cages, 
        case when c3.rate_name is null then c1.use_date else null end as start_date,
        case when c2.rate_name is null then c1.use_date else null end as end_date
      FROM cte c1
      -- next day
      left join cte c2 on c2.rate_name = c1.rate_name and c2.use_date = c1.use_date +1 and c2.cages = c1.cages
      -- prev day
      left join cte c3 on c3.rate_name = c1.rate_name and c3.use_date = c1.use_date -1 and c3.cages = c1.cages
    )
    select rate_name, cages, start_date, 
        (select min(e.end_date) from recur e
            where e.rate_name = s.rate_name
              and e.end_date >= s.start_date) as end_date
    from recur s
    where start_date is not null
    order by rate_name, start_date;