get list of student with attendance min15days in a month and come for continuous 4 months in a year - sql

I need a query to get the list of students attended there class for atleast 15 days in a month for continuous 4 months.
table maybe like
studentid monthyear attendance
1 Apr2018 16
1 May2018 23
1 Jun2018 18
1 Jul2018 16
1 Aug2018 25
2 Apr2018 2
2 May2018 15
and so on...
Db fiddle

Try this query:
select #rn := 0;
select studentid from (
select studentid, month(dt) - (#rn := #rn + 1) grp from (
select * ,
str_to_date(concat('01 ', insert(monthyear, 4, 0, ' ')), '%d %M %Y') dt
from tbl
where attendance >= 15 --only those records, where attenadnce is at least 15
) a where year(dt) = 2018 --particular year
order by studentid,dt
) a group by studentid,grp having count(*) >= 4
Demo - I exapnded your data with some more cases :)
The idea is simple - if student has attended for some consecutive months, consecutive months would increment by one, just like row number, so I used difference between months and row numbers - for consecutive months, the difference should be constant, so it's enought to group by that difference and take those groups, where count is >= 4 :)
UPDATE
For SQL Server:
select studentid from (
select studentid, month(dt) - row_number() over (order by studentid, dt) grp from (
select * ,
cast(concat('01 ', stuff(monthyear, 4, 0, ' ')) as date) dt
from tbl
where attendance >= 15 --only those records, where attenadnce is at least 15
) a where year(dt) = 2018 --particular year
) a group by studentid, grp having count(*) >= 4
SQL Server demo

In general, a simple selft join that would catch the difference of months would suffice
In this case, a conversion of the column monthyear is required in the join command itself
The query, without the conversion :
SELECT t1.studentid, count(*) as cnt
FROM
table t1
INNER JOIN table t2 ON t1.studentid = t2.studentid AND
t2.attendance >= 15
AND t1.monthyear BETWEEN t2.monthyear AND (t2.monthyear - 3)
WHERE
t1.attendance >= 15
GROUP BY
studentid
HAVING
count(*) >=4
The conversion is as follows:
STR_TO_DATE(
CONCAT(SUBSTR(t1.monthyear,1, LENGTH(t1.monthyear) - 4),' ', RIGHT(t1.monthyear, 4), %M %Y)
so the query should be:
SELECT t1.studentid, count(*) as cnt
FROM
table t1
INNER JOIN table t2 ON t1.studentid = t2.studentid AND
t2.attendance >= 15
AND STR_TO_DATE(
CONCAT(SUBSTR(t1.monthyear,1, LENGTH(t1.monthyear) - 4),' ', RIGHT(t1.monthyear, 4), %M %Y) BETWEEN STR_TO_DATE(
CONCAT(SUBSTR(t2.monthyear,1, LENGTH(t2.monthyear) - 4),' ', RIGHT(t2.monthyear, 4), %M %Y) AND DATE_SUB(STR_TO_DATE(
CONCAT(SUBSTR(t2.monthyear,1, LENGTH(t2.monthyear) - 4),' ', RIGHT(t2.monthyear, 4), %M %Y), INTERVAL 3 MONTH)
WHERE
t1.attendance >= 15
GROUP BY
studentid
HAVING
count(*) >=4

I think this is the simplest method:
select distinct studentid
from (select t.*, cast(monthyear as date) as my,
lag(cast(monthyear as date), 3) over (partition by studentid order by cast(monthyear as date)) as prev_my
from tbl t
where attendance >= 15
) t
where prev_my = dateadd(month, -3, my);
Here is a db<>fiddle.
The logic is pretty simple:
Only consider rows that satisfy the attendance criterion.
Use LAG() to look at the 3rd record in past.
If all months meet the attendance criterion, then this will be exactly 3 months before.
The select distinct is because you want students, not the specific periods.

Related

Improve query to be less repetitive

Is there a way to improve this query? I see two problems here -
Repetitive code
Hard coded strings
The first CTE calculates count based on 18 months. The second CTE calculates count based on 12 months.
with month_18 as (
select proc_cd, count(*) as month_18 from
(
select distinct patient, proc_cd from
service
where proc_cd = '35'
and month_id >= (select month_id from annual)
and month_id <= '202009' --This month should be 18 months from the month above
and length(patient) > 1
) a
group by proc_cd
),
month_12 as
(
select proc_cd, count(*) as month_12 from
(
select distinct patient_id, proc_cd from
service
where proc_cd = '35'
and month_id >= '201910'
and month_id <= '202009' --This month should be 12 months from the month above
and length(patient) > 1
) a
group by proc_cd
)
select a.*, b.month_12 from
month_18 a
join month_12 b
on a.proc_cd = b.proc_cd
If I understand correctly, you can use conditional aggregation:
select proc_cd,
count(distinct patient) filter (where month_id >= (select month_id from annual) and month_id <= '202009') as month_18,
count(distinct patient) filter (where month_id >= '201910' and month_id <= '202009')
from service
where proc_cd = 35 and
length(patient) > 1
group by proc_cd;
If you have to deal with date arithmetic on the month ids, you can convert to a date, do the arithmetic and convert back to a string:
select to_char(to_date(month_id, 'YYYYMM') - interval '12 month', 'YYYYMM')
from (values ('202009')) v(month_id);

Detect if a month is missing and insert them automatically with a select statement (MSSQL)

I am trying to write a select statement which detects if a month is not existent and automatically inserts that month with a value 0. It should insert all missing months from the first entry to the last entry.
Example:
My table looks like this:
After the statement it should look like this:
You need a recursive CTE to get all the years in the table (and the missing ones if any) and another one to get all the month numbers 1-12.
A CROSS join of these CTEs will be joined with a LEFT join to the table and finally filtered so that rows prior to the first year/month and later of the last year/month are left out:
WITH
limits AS (
SELECT MIN(year) min_year, -- min year in the table
MAX(year) max_year, -- max year in the table
MIN(DATEFROMPARTS(year, monthnum, 1)) min_date, -- min date in the table
MAX(DATEFROMPARTS(year, monthnum, 1)) max_date -- max date in the table
FROM tablename
),
years(year) AS ( -- recursive CTE to get all the years of the table (and the missing ones if any)
SELECT min_year FROM limits
UNION ALL
SELECT year + 1
FROM years
WHERE year < (SELECT max_year FROM limits)
),
months(monthnum) AS ( -- recursive CTE to get all the month numbers 1-12
SELECT 1
UNION ALL
SELECT monthnum + 1
FROM months
WHERE monthnum < 12
)
SELECT y.year, m.monthnum,
DATENAME(MONTH, DATEFROMPARTS(y.year, m.monthnum, 1)) month,
COALESCE(value, 0) value
FROM months m CROSS JOIN years y
LEFT JOIN tablename t
ON t.year = y.year AND t.monthnum = m.monthnum
WHERE DATEFROMPARTS(y.year, m.monthnum, 1)
BETWEEN (SELECT min_date FROM limits) AND (SELECT max_date FROM limits)
ORDER BY y.year, m.monthnum
See the demo.
You should not be storing date components in two separate columns; instead, you should have just one column, with a proper date-like datatype.
One approach is to use a recursive query to generate all starts of month between the earliest and latest date in the table, then brin the table with a left join.
In SQL Server:
with cte as (
select min(datefromparts(year, monthnum, 1)) as dt,
max(datefromparts(year, monthnum, 1)) as dt_max
from mytable
union all
select dateadd(month, 1, dt)
from cte
where dt < dt_max
)
select c.dt, coalesce(t.value, 0) as value
from cte c
left join mytable t on datefromparts(t.year, t.month, 1) = c.dt
If your data spreads over more that 100 months, you need to add option(maxrecursion 0) at the end of the query.
You can extract the date components in the final select if you like:
select
year(c.dt) as yr,
month(c.dt) as monthnum,
datename(month, c.dt) as monthname,
coalesce(t.value, 0) as value
from ...

SQL Server : how to get previous year data and create a row in output

I have a delivery table as below:
I want the count of delivery age wise. Vehicles delivered in 2010 will be counted as age 0 in 2010, age 1 in 2011 and age 2 in 2012 and so on for the years.
Please help me to know how to do this in SQL query. I am new to this forum so don't have permission to add images.
Assuming that you have a delivery in each year, I see this as a JOIN and aggregation:
with years as (
select distinct year(deliverydate) as yyyy
from vehicle
)
select y.yyyy, (y.yyyy - year(v.deliverydate)) as age , count(*)
from vehicle v join
years y
on y.yyyy >= year(v.deliverydate)
group by y.yyyy, (y.yyyy - year(v.deliverydate))
order by y.yyyy, (y.yyyy - year(v.deliverydate));
If you don't have a delivery in each year, you can explicitly list them:
select y.yyyy, (y.yyyy - year(v.deliverydate)) as age , count(*)
from vehicle v join
(values (2010), (2011), (2012), (2013)) y(yyyy)
on y.yyyy >= year(v.deliverydate)
group by y.yyyy, (y.yyyy - year(v.deliverydate))
order by y.yyyy, (y.yyyy - year(v.deliverydate));
Here is a db<>fiddle.
here is a way to do this
select year(deliverydate)
,(year(deliverydate)-2010) as age_of_vehicle
,count(vehicleid)
from table
group by year(deliverydate)
SELECT DATEDIFF(year, '2011/08/25', getdate()) AS DateDiff
This will give you the age of vehicle and use group by clause to get value base on years as desired
I am sure there are better ways to do this (recursive cte), but the following also gives the correct result:
;with cte as
(
select YEAR(CONVERT(VARCHAR(10), CONVERT(date, DeliveryDate, 105), 23)) [Year], 0 Age
from vehicle v
)
select * into #temp
from cte
union all
select t1.[Year]+ (t2.[Year] - t1.[Year]), (t2.[Year] - t1.[Year]) as Age
from cte t1
join (select distinct [Year], Age from cte) t2 on t2.[Year] > t1.[Year]
select [Year], Age AgeofVehicle, COUNT(1) Deliveries
from #temp
group by [Year], Age
order by [Year], Age
Please see the db<>fiddle here.

Fill missing months in a SELECT query

I'm trying to fill missing months in a SELECT query.
It looks like this :
SELECT sl.loonperiode_dt, (sum(slr.uren)) code_220
FROM HR.soc_loonbrief_regels slr,
HR.soc_loonbrieven sl,
HR.werknemers w,
HR.v_kontrakten vk
WHERE sl.loonperiode_dt BETWEEN '01012018' AND '01122018'
AND slr.loon_code_id IN (394)
AND slr.loonbrief_id = sl.loonbrief_id
AND w.werknemer_id = sl.werknemer_id
AND w.werknemer_id = vk.werknemer_id
AND vk.functie_id IN (121, 122, 128)
AND sl.loonperiode_dt BETWEEN hist_start_dt AND last_day(nvl(hist_eind_dt, sl.loonperiode_dt))
AND w.afdeling_id like '961'
GROUP BY sl.loonperiode_dt
ORDER BY sl.loonperiode_dt
It outputs this table :
31/01/18 234
30/04/18 245,8
31/05/18 714,6
31/07/18 288,04
31/08/18 281
30/11/18 515,12
I obviously would like it to be like that :
31/01/18 234
28/02/18 0
31/03/18 0
30/04/18 245,8
31/05/18 714,6
30/06/18 0
31/07/18 288,04
31/08/18 281
30/09/18 0
31/10/18 0
30/11/18 515,12
31/12/18 0
I have a calendar table 'CONV_HC.calendar' with dates in a column named 'DAT'.
I have seen many questions and answers about this, but I can't figure out how to apply the LEFT JOIN method or any other one to my current problem.
Thanks a lot in advance,
You could have a already done table with months and "join" with it, group by the date, or you can create one with subquery or using a with statement, something like
WITH Months (month) AS (
SELECT 1 AS Month FROM DUAL
UNION ALL
SELECT MONTH + 1
FROM Months
WHERE MONTH < 12
)
SELECT *
FROM Months
LEFT JOIN SomeTable
ON SomeTable.month = Months.MONTH
--ON Extract(MONTH FROM SomeTable.date) = Months.MONTH
edit
A better example:
--Just to simulate some table data
WITH SomeData AS (
SELECT TO_DATE('01/01/2019', 'MM/DD/YYYY') AS Dat, 5 AS Value FROM dual
UNION ALL
SELECT TO_DATE('01/05/2019', 'MM/DD/YYYY') AS Dat, 7 AS Value FROM dual
UNION ALL
SELECT TO_DATE('03/03/2019', 'MM/DD/YYYY') AS Dat, 2 AS Value FROM dual
UNION ALL
SELECT TO_DATE('11/05/2019', 'MM/DD/YYYY') AS Dat, 9 AS Value FROM dual
)
, Months (StartDate, MaxYear) AS (
SELECT CAST(TO_DATE('01/01/2019', 'MM/DD/YYYY') AS DATE) AS StartDate, 2019 AS MaxYear FROM DUAL
UNION ALL
SELECT CAST(ADD_MONTHS(StartDate, 1) AS DATE), MaxYear
FROM Months
WHERE EXTRACT(YEAR FROM ADD_MONTHS(StartDate, 1)) <= MaxYear
)
SELECT
Months.StartDate AS Dat
, SUM(SomeData.Value) AS SumValue
FROM Months
LEFT JOIN SomeData
ON Extract(MONTH FROM SomeData.Dat) = Extract(MONTH FROM Months.StartDate)
GROUP BY
Months.StartDate
edit
You won't find a just copy past solution, you need to get the idea from it and change to your context.
let's try this. You can "add" the missing months in an APP, or you can JOIN it with a already done table, doesn't need to be a real table, you can make one. The with statement is an example of it. So lets get all month, at the last day for 2019:
--Geting the last day of every month for 2019
WITH Months (CurrentMonth, MaxYear) AS (
SELECT CAST(TO_DATE('01/01/2019', 'MM/DD/YYYY') AS DATE) AS CurrentMonth, 2019 AS MaxYear FROM DUAL
UNION ALL
SELECT CAST(ADD_MONTHS(CurrentMonth, 1) AS DATE), MaxYear
FROM Months
WHERE EXTRACT(YEAR FROM ADD_MONTHS(CurrentMonth, 1)) <= MaxYear
)
SELECT LAST_DAY(Months.CurrentMonth) AS LastDay
FROM Months
Ok, now we have all months avaliable for the join. In your query, you already have the sum done so lets skip the sum and just use your data. Just add another with query.
--Geting the last day of every month for 2018
WITH Months (CurrentMonth, MaxYear) AS (
SELECT CAST(TO_DATE('01/01/2018', 'MM/DD/YYYY') AS DATE) AS CurrentMonth, 2018 AS MaxYear FROM DUAL
UNION ALL
SELECT CAST(ADD_MONTHS(CurrentMonth, 1) AS DATE), MaxYear
FROM Months
WHERE EXTRACT(YEAR FROM ADD_MONTHS(CurrentMonth, 1)) <= MaxYear
)
, YourData as (
SELECT sl.loonperiode_dt, (sum(slr.uren)) code_220
FROM HR.soc_loonbrief_regels slr,
HR.soc_loonbrieven sl,
HR.werknemers w,
HR.v_kontrakten vk
WHERE sl.loonperiode_dt BETWEEN '01012018' AND '01122018'
AND slr.loon_code_id IN (394)
AND slr.loonbrief_id = sl.loonbrief_id
AND w.werknemer_id = sl.werknemer_id
AND w.werknemer_id = vk.werknemer_id
AND vk.functie_id IN (121, 122, 128)
AND sl.loonperiode_dt BETWEEN hist_start_dt AND last_day(nvl(hist_eind_dt, sl.loonperiode_dt))
AND w.afdeling_id like '961'
GROUP BY sl.loonperiode_dt
--ORDER BY sl.loonperiode_dt
)
SELECT
LAST_DAY(Months.CurrentMonth) AS LastDay
, COALESCE(YourData.code_220, 0) AS code_220
FROM Months
Left Join YourData
on Extract(MONTH FROM Months.CurrentMonth) = Extract(MONTH FROM YourData.loonperiode_dt)
--If you have more years: AND Extract(YEAR FROM Months.CurrentMonth) = Extract(YEAR FROM YourData.loonperiode_dt)
ORDER BY LastDay ASC

How to find the missing rows?

I have a table as shown in the image.
The column MONTH_NO should be having months from 1 to 12 for every year. For some years, we missed to load data for some months. I need a query which will fetch the years which doesn't have all the 12 months along with the missing month number.
Please help.
For example -
with mth
as (select level as month_no
from dual
connect by level <= 12),
yrs as (select distinct year from rag_month_dim)
select m.year, m.month_no
from (select year, month_no
from yrs, mth) m,
rag_month_dim r
where m.year = r.year(+)
and m.month_no = r.month_no(+)
group by m.year, m.month_no
having max(r.month_no) is null
order by year, month_no
Try it like this:
post this into an empty query window and adapt to your needs.
MyData contains a "full" year 2013, Sept is missing in 2014 and June and Sept are missing in 2015.
DECLARE #OneToTwelve TABLE(Nmbr INT)
INSERT INTO #OneToTwelve VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12);
DECLARE #myData TABLE(yearNo INT, MonthNo INT)
INSERT INTO #myData VALUES
(2013,1),(2013,2),(2013,3),(2013,4),(2013,5),(2013,6),(2013,7),(2013,8),(2013,9),(2013,10),(2013,11),(2013,12)
,(2014,1),(2014,2),(2014,3),(2014,4),(2014,5),(2014,6),(2014,7),(2014,8),(2014,10),(2014,11),(2014,12)
,(2015,1),(2015,2),(2015,3),(2015,4),(2015,5),(2015,7),(2015,8),(2015,10),(2015,11),(2015,12);
WITH AllYears AS
(
SELECT DISTINCT yearNo FROM #myData
)
,AllCombinations AS
(
SELECT *
FROM #OneToTwelve AS months
CROSS JOIN AllYears
)
SELECT *
FROM AllCombinations
LEFT JOIN #myData AS md ON AllCombinations.Nmbr =md.MonthNo AND AllCombinations.yearNo=md.yearNo
WHERE md.MonthNo IS NULL
select distinct year, m.lev
from rag_month_dim a
join
(
select level lev
from dual
connect by level <= 12
) m
on 1=1
minus
select year, month_no
from rag_month_dim
order by 1, 2
select *
from (select count (-1) total, year from rag_month_dim group by year) as table
where total < 12.
you got a year that doesnt have 12 month data and total month record in your data.