Merge lines over timespan in SCD2 table

374 views Asked by At

I'm having the following table sourced from a SCD2 table. From this source table, I have selected only a few columns, which reults in several lines looking exactly similar. I want to remove the unnecessary lines, those that contain the same data, and have the ValidFrom column showing the first value and ValidTo column showing the last value within "the timespan group".

Source data:

| Item     | Color      | ValidFrom     | ValidTo    |
| -------- | ---------- | ------------- | ---------- |
| Ball     | Red        | 2020-01-01    | 2020-03-24 |
| Ball     | Blue       | 2020-03-25    | 2020-04-12 |
| Ball     | Blue       | 2020-04-13    | 2020-05-07 |
| Ball     | Blue       | 2020-05-08    | 2020-11-14 |
| Ball     | Red        | 2020-11-15    | 9999-12-31 |
| Doll     | Yellow     | 2020-01-01    | 2020-03-24 |
| Doll     | Green      | 2020-03-25    | 2020-04-12 |
| Doll     | Green      | 2020-04-13    | 2020-05-07 |
| Doll     | Green      | 2020-05-08    | 2020-11-14 |
| Doll     | Pink       | 2020-11-15    | 9999-12-31 | 

What I want to accomplish is this:

| Item     | Color      | ValidFrom     | ValidTo    |
| -------- | ---------- | ------------- | ---------- |
| Ball     | Red        | 2020-01-01    | 2020-03-24 |
| Ball     | Blue       | 2020-03-25    | 2020-11-14 |
| Ball     | Red        | 2020-11-15    | 9999-12-31 |
| Doll     | Yellow     | 2020-01-01    | 2020-03-24 |
| Doll     | Green      | 2020-03-25    | 2020-11-14 |
| Doll     | Pink       | 2020-11-15    | 9999-12-31 | 

Note that the Item Ball at first has the color Red, then Blue and then goes back to Red. That makes things a bit more complicated, from what I have learned.

Thanks for your help.

4

There are 4 answers

1
Gordon Linoff On BEST ANSWER

Your data is very regular. You seem to just want to combine adjacent, tiled, records that have no overlaps or gaps. However the following handles gaps and more general overlaps:

select item, color, min(validfrom), max(validto)
from (select t.*,
             sum(case when prev_validto >= dateadd(day, -1, validfrom)
                      then 0 else 1
                 end) over (partition by item order by validfrom) as grp
      from (select t.*,
                   lag(validto) over (partition by item, color order by validfrom) as prev_validto
            from t
            ) t
     ) t
group by item, color, grp;

You are looking for islands of rows in the original data where the "islands" have the same item, color, and adjacent dates. This determines where islands start by looking at the previous row for the same item and color. If there is no such row or the row ends before the current row begins, then the current row is the beginning of an island.

The grp is then the cumulative sum of "island beginnings", and that can be used for aggregating and getting the final results.

Your specific data is quite constrained -- perfectly tiled with one row ending the day before the next begins. You can do something very similar using left join:

select item, color, min(validfrom), max(validto)
from (select t.*,
             sum(case when tprev.color is null then 1 else 0
                 end) over (partition by t.item order by t.validfrom) as grp
      from t left join
           t tprev
           on tprev.item = t.item and
              tprev.color = t.color and
              tprev.validto = dateadd(day, -1, t.validfrom)
     ) t
group by item, color, grp
order by item, min(validfrom);

Here is a db<>fiddle illustrating both methods

1
GMB On

This is indeed a gaps-and-islands problem, where islands are adjacent records having the same item and color.

Here, I would recommend using the difference between row numbers to define the groups. This involves just one level of nesting, as opposed to two when using lag(), so it should be the most efficient option:

select item, color, min(validfrom) as validfrom, max(validto) as validto
from (
    select t.*,
        row_number() over(order by validfrom) as rn1,
        row_number() over(partition by item, color order by validfrom) as rn2
    from mytable t
) t
group by item, color, rn1 - rn2
1
Popeye On

This is island and gap problem.

You can use the analytical function as follows:

Select item, color,
       min(validfrom) as validfrom,
       Max(validto) as validto
  From
(Select t.*,
       Sum(case when lged between validfrom and validto then 0 else 1 end) 
           over (partition by item, color order by validfrom) as sm
  From
(Select t.*,
       Lag(validto) over (partition by item, color order by validfrom) as lged
  From t) t) t
Group by item, color, sm
1
SteveC On

Since there are no gaps or overlaps between rows maybe this query is sufficient

select item, color, 
       min(validfrom) as ValidFrom,
       max(validto) as ValidTo
from tTable
group by item, color
order by item, ValidFrom;