From Oracle 12, you can use MATCH_RECOGNIZE:
SELECT cat, month, COUNT(*) FROM ( SELECT t.*, TRUNC( "DATE", 'MM' ) AS month FROM table_name t ) MATCH_RECOGNIZE( PARTITION BY cat, month ORDER BY "DATE", version ONE ROW PER MATCH AFTER MATCH SKIP TO LAST change_code PATTERN ( strt change_code ) DEFINE change_code AS change_code.some_code <> strt.some_code ) GROUP BY cat, month
Which, for the sample data:
CREATE TABLE table_name ( CAT, NR, "DATE", VERSION, SOME_CODE ) AS SELECT 'ABC', 123, TIMESTAMP '2009-02-19 00:00:00 UTC', 1, 'OPP' FROM DUAL UNION ALL SELECT 'ABC', 456, TIMESTAMP '2009-03-18 00:00:00 UTC', 1, 'ZUM' FROM DUAL UNION ALL SELECT 'ABC', 444, TIMESTAMP '2009-03-18 00:00:00 UTC', 1, 'ZUM' FROM DUAL UNION ALL SELECT 'ABC', 444, TIMESTAMP '2009-03-18 00:00:00 UTC', 2, 'MUZ' FROM DUAL UNION ALL SELECT 'ABC', 456, TIMESTAMP '2009-04-18 00:00:00 UTC', 2, 'XXX' FROM DUAL UNION ALL SELECT 'ABC', 456, TIMESTAMP '2009-04-18 00:00:00 UTC', 3, 'XXX' FROM DUAL UNION ALL SELECT 'ABC', 456, TIMESTAMP '2009-04-18 00:00:00 UTC', 4, 'UIO' FROM DUAL UNION ALL SELECT 'ABC', 456, TIMESTAMP '2009-05-18 00:00:00 UTC', 5, 'RQA' FROM DUAL UNION ALL SELECT 'DEF', 637, TIMESTAMP '2018-02-16 00:00:00 UTC', 1, 'FAW' FROM DUAL UNION ALL SELECT 'DEF', 789, TIMESTAMP '2018-02-17 00:00:00 UTC', 1, 'WER' FROM DUAL UNION ALL SELECT 'GHI', 248, TIMESTAMP '2018-02-17 00:00:00 UTC', 1, 'QWE' FROM DUAL UNION ALL SELECT 'GHI', 248, TIMESTAMP '2019-02-17 00:00:00 UTC', 2, 'PPP' FROM DUAL UNION ALL SELECT 'GHI', 357, TIMESTAMP '2020-02-16 00:00:00 UTC', 1, 'FFF' FROM DUAL UNION ALL SELECT 'GHI', 420, TIMESTAMP '2020-02-16 00:00:00 UTC', 1, 'QDS' FROM DUAL UNION ALL SELECT 'GHI', 357, TIMESTAMP '2020-02-16 00:00:00 UTC', 2, 'GGG' FROM DUAL UNION ALL SELECT 'GHI', 357, TIMESTAMP '2020-02-16 00:00:00 UTC', 3, 'LLL' FROM DUAL UNION ALL SELECT 'GHI', 357, TIMESTAMP '2020-02-16 00:00:00 UTC', 4, 'LLL' FROM DUAL UNION ALL SELECT 'GHI', 357, TIMESTAMP '2020-08-16 00:00:00 UTC', 4, 'FFF' FROM DUAL UNION ALL SELECT 'GHI', 357, TIMESTAMP '2020-10-16 00:00:00 UTC', 5, 'ZZZ' FROM DUAL
Outputs:
CAT | MONTH | COUNT(*) :-- | :-------- | -------: ABC | 01-MAR-09 | 1 ABC | 01-APR-09 | 1 DEF | 01-FEB-18 | 1 GHI | 01-FEB-20 | 3
If you want to see the changes then you can use:
SELECT * FROM ( SELECT t.*, TRUNC( "DATE", 'MM' ) AS month FROM table_name t ) MATCH_RECOGNIZE( PARTITION BY cat, month ORDER BY "DATE", version MEASURES MATCH_NUMBER() AS mn, FIRST( some_code ) AS change_from, LAST( some_code ) AS change_to ONE ROW PER MATCH AFTER MATCH SKIP TO LAST change_code PATTERN ( strt change_code ) DEFINE change_code AS change_code.some_code <> strt.some_code )
Which outputs:
CAT | MONTH | MN | CHANGE_FROM | CHANGE_TO :-- | :-------- | -: | :---------- | :-------- ABC | 01-MAR-09 | 1 | ZUM | MUZ ABC | 01-APR-09 | 1 | XXX | UIO DEF | 01-FEB-18 | 1 | FAW | WER GHI | 01-FEB-20 | 1 | FFF | QDS GHI | 01-FEB-20 | 2 | QDS | GGG GHI | 01-FEB-20 | 3 | GGG | LLL
db<>fiddle here
If your requirement for "within a month" is that you want changes where there is at most one month's difference between the previous row to the changed row, even if the rows are in two different calendar months, (rather than just the changes that happen in the same calendar month) then you can use:
SELECT cat, TRUNC( change_date, 'MM' ) AS month, COUNT(*) FROM table_name MATCH_RECOGNIZE( PARTITION BY cat ORDER BY "DATE", version MEASURES LAST( "DATE" ) AS change_date ONE ROW PER MATCH AFTER MATCH SKIP TO LAST change_code PATTERN ( strt change_code ) DEFINE change_code AS ( change_code.some_code <> strt.some_code AND MONTHS_BETWEEN( change_code."DATE", strt."DATE" ) <= 1 ) ) GROUP BY cat, TRUNC( change_date, 'MM' )
Which outputs:
CAT | MONTH | COUNT(*) :-- | :-------- | -------: ABC | 01-MAR-09 | 2 ABC | 01-MAY-09 | 1 ABC | 01-APR-09 | 2 DEF | 01-FEB-18 | 1 GHI | 01-FEB-20 | 3
db<>fiddle here
ABCtoDEFand then back toABC? Is that one or two changes?CAT, sameDATEand sameSOME_CODE. How do you order such rows? "By date" doesn't suffice, since they have exactly the same timestamp. And, of course, different order may result in different counts of "changes". 2. If the value changes from'ZUM'on February 26 to'ZAM'on March 2, is that a "change" you count for March? In almost any sane business use case the answer should be "yes" - but your explanation is confusing enough that MT0 assumed "no" for his answer. Please clarify.