I have a table that stores the hours reported by users for activities completed, by reporting year and month. I have a function in an old Oracle database, that I need to replicate in SQL Server, that shows the current value for consecutive 0 hour reports. i.e. when someone hasn't reported hours for 3 months in a row, the number would be 3, if they have reported for a given month, the counter would then be reset back to 0, so as to only count the consecutive reports with 0 hours.
The table is always pre-filled with the expected amount of reporting periods for the user. i.e. if the user is expected to report hours for 6 months, we create the 6 rows in this table, with NULL in the HOURS_REPORTED field.
Here is a sample of the PLSQL.
FUNCTION FN_NIL_RPT_CNT ( PI_USER_ID IN NUMBER )
RETURN NUMBER
IS
LV_NIL_CNT NUMBER := 0;
CURSOR CR_DTLS IS
SELECT YEAR
,MONTH
,HOURS_REPORTED
FROM HOURS
WHERE STATUS = 'CURRENT'
AND USER_ID = PI_USER_ID
ORDER BY TO_DATE(MONTH||YEAR, 'mmyyyy');
BEGIN
FOR LR_HR IN CR_DTLS
LOOP
EXIT WHEN TO_DATE(LR_HR.MONTH||LR_HR.YEAR,'mmyyyy') >= TRUNC(SYSDATE,'MM');
IF LR_HR.HOURS_REPORTED IS NOT NULL
THEN
IF LR_HR.HOURS_REPORTED = 0
THEN
LV_NIL_CNT := LV_NIL_CNT + 1;
ELSE
LV_NIL_CNT := 0;
END IF;
ELSE
NULL; -- ignore
END IF;
END LOOP;
END;
RETURN LV_NIL_CNT;
END FN_NIL_RPT_CNT;
So if the cursor contained the following, the expected result would be 2 because hours were reported in 2019/10, which would then reset the counter to 0, and the subsequent 2 months had 0 hours reported.
My suggestion is to make use of straight sql, and to lose the cursor logic. Anyday its going to be more efficient, faster and less expensive
Eg: the following query can get you the last_consecutive_count
CREATE TABLE dbo.t
(
USER_ID INT
,YEAR INT
,MONTH INT
,HOURS_REPORTED INT
);
insert into dbo.t
VALUES
( 254, '2017', '12', 0 ),
( 254, '2018', '01', 8 ),
( 254, '2018', '02', 11 ),
( 254, '2018', '03', 16 ),
( 254, '2018', '04', 12 ),
( 254, '2018', '05', 16 ),
( 254, '2018', '06', 20 ),
( 254, '2018', '07', 14 ),
( 254, '2018', '08', 12 ),
( 254, '2018', '09', 11 ),
( 254, '2018', '10', 22 ),
( 254, '2018', '11', 10 ),
( 254, '2018', '12', 6 ),
( 254, '2019', '01', 11 ),
( 254, '2019', '02', 12 ),
( 254, '2019', '03', 5 ),
( 254, '2019', '04', 12 ),
( 254, '2019', '05', 10 ),
( 254, '2019', '06', 9 ),
( 254, '2019', '07', 10 ),
( 254, '2019', '08', 9 ),
( 254, '2019', '09', 0 ),
( 254, '2019', '10', 4 ),
( 254, '2019', '11', 8 ),
( 254, '2019', '12', 0 ),
( 254, '2020', '01', 0 ),
( 254, '2020', '02', 5 ),
( 254, '2020', '03', 7 )
WITH data
AS (SELECT *
,CAST(CONCAT(YEAR, '-', MONTH, '-01') AS DATE) AS dt
,ROW_NUMBER() OVER (ORDER BY YEAR desc, MONTH desc) AS rnk
,ROW_NUMBER() OVER (partition by case when hours_reported=0 then 1 end ORDER BY YEAR desc, MONTH desc) AS rnk2
,DATEADD(MONTH, -ROW_NUMBER() OVER (ORDER BY YEAR, MONTH), CAST(CONCAT(YEAR, '-', MONTH, '-01') AS DATE)) AS grp
FROM dbo.t
WHERE USER_ID = 254 /* you can parameterize this in your sql server function*/
)
,last_val
as(SELECT *
,case when hours_reported<>0 then rnk else rnk-rnk2 end as val1
,rank() over(order by case when hours_reported<>0 then rnk else rnk-rnk2 end) as rnk_min
FROM data
)
select count(*) as last_consec_cnt
from last_val
where rnk_min=1
+----------------------+
| last_consecutive_cnt |
+----------------------+
| 1 |
+----------------------+
You can put this in a function and pass the user_id to the table..
Here is a db fiddle link. https://dbfiddle.uk/?rdbms=sqlserver_2016&fiddle=9ac60e62a99e2465cc8e979ae9f29f7c