Returning Records by Week - sql

I'm trying to come up with a way to return a result set from the below data without a loop that shows the number of records by Team for a particular date range by week.
I've got a Date table (https://www.mssqltips.com/sqlservertip/4054/creating-a-date-dimension-or-calendar-table-in-sql-server/) that has every day/week/year referenced, but not sure how to connect it up.
Create Table #Team
(
TeamID int,
TeamName varchar(20)
)
insert into #Team
(
TeamID,
TeamName
)
select
1,
'Team 1'
union all
select
2,
'Team 2'
union all
select
3,
'Team 3'
union all
select
4,
'Team 4'
Create Table #Entries
(
EntryID int,
DateCreated datetime,
TeamID int
)
insert into #Entries
(
EntryID,
DateCreated,
TeamID
)
select
1,
'2 Nov 2020',
1
union all
select
2,
'4 Nov 2020',
2
I've got this query:
select
T.TeamName,
WeekOfYear,
WeekOfMonth,
count(*) as Count(*)
from
#Team T
Left Join #Entries E on
T.TeamID = E.TeamID
Inner Join DimDate D on
cast(E.DateCreated as date) = D.[Date]
group by
T.TeamName,
WeekOfYear,
WeekOfMonth
Where it fails is:
It doesn't include the teams with 0 results
I need to be able to show results for multiple weeks through a date range. In the above example, they would be 0.

I think the trick is to first generate all the rows you need, then LEFT JOIN those onto their results to get what you want.
Note that in your query, you are pulling out WeekOfYear and WeekOfMonth, but you probably also want to pull out Year in case the data crosses years or goes for multiple years.
For the date range
I have two variables #RangeStart and #RangeEnd- both dates - to do filtering
I create a table (probably incorrect) to model the date dimension table
CREATE TABLE #DimDate ([Date] date, WeekOfYear int, WeekOfMonth int, y_year int)
INSERT INTO #DimDate ([Date], WeekOfYear, WeekOfMonth, y_year) VALUES
('20201029', 35, 4, 2020),
('20201030', 35, 4, 2020),
('20201031', 35, 4, 2020),
('20201101', 36, 1, 2020),
('20201102', 36, 1, 2020),
('20201103', 36, 1, 2020),
('20201104', 36, 1, 2020);
-- Note that I called the year 'y_year' - will need to be changed
-- to your value (or converted to YEAR([date]) function)
DECLARE #RangeStart date = '20201030';
DECLARE #RangeEnd date = '20201102';
WITH AllTeamDates AS
(SELECT T.TeamId,
D.[Date],
D.WeekOfMonth,
D.WeekOfYear,
D.y_year
FROM #Team T
CROSS JOIN #DimDate D
WHERE D.[Date] BETWEEN #RangeStart AND #RangeEnd
)
SELECT ATD.y_year,
ATD.WeekOfYear,
ATD.WeekOfMonth,
ATD.TeamID,
COUNT(E.EntryID) AS NumEntries
FROM AllTeamDates ATD
LEFT OUTER JOIN #Entries E
ON ATD.TeamID = E.TeamID AND ATD.Date = E.DateCreated
GROUP BY ATD.y_year,
ATD.WeekOfYear,
ATD.WeekOfMonth,
ATD.TeamID;
Results for the above, with your data and my date table and range dates applied (noting that the date range I selected gets the first value in #Entries for 2 Nov, but doesn't get the second for 4 Nov).
y_year WeekOfYear WeekOfMonth TeamID NumEntries
2020 35 4 1 0
2020 35 4 2 0
2020 35 4 3 0
2020 35 4 4 0
2020 36 1 1 1
2020 36 1 2 0
2020 36 1 3 0
2020 36 1 4 0
Note that in this case I am creating all possible dates, then grouping to get week-by-week at the very end. It is possible to also do this by grouping into week-by-week data as soon as possible (e.g., the CTE will return data by week instead of day, then the outer part of the LEFT JOIN also then needs to be grouped into weeks first).
WITH AllTeamWeeks AS
(SELECT T.TeamId, D.WeekOfMonth, D.WeekOfYear, D.y_year
FROM #Team T
CROSS JOIN #DimDate D
WHERE D.[Date] BETWEEN #RangeStart AND #RangeEnd
GROUP BY T.TeamId, D.WeekOfMonth, D.WeekOfYear, D.y_year
),
AllEntries AS
(SELECT E.TeamId, D.WeekOfMonth, D.WeekOfYear, D.y_year,
COUNT(E.EntryID) AS NumEntries
FROM #Entries E
INNER JOIN #DimDate D ON E.DateCreated = D.Date
WHERE E.[DateCreated] BETWEEN #RangeStart AND #RangeEnd
GROUP BY E.TeamId, D.WeekOfMonth, D.WeekOfYear, D.y_year
)
SELECT ATW.y_year,
ATW.WeekOfYear,
ATW.WeekOfMonth,
ATW.TeamID,
ISNULL(AE.NumEntries,0) AS NumEntries
FROM AllTeamWeeks ATW
LEFT OUTER JOIN AllEntries AE
ON ATW.TeamID = AE.TeamID
AND ATW.WeekOfMonth = AE.WeekOfMonth
AND ATW.WeekOfYear = AE.WeekOfYear
AND ATW.y_year = AE.y_year;
This gives the same results, and possibly provides a performance benefit, but is more complex and you'd probably need to ensure that SQL Server is getting accurate estimates/etc when doing the multiple GROUP BYs.
As such I wouldn't use it unless there is a performance issue with the first one - and if there was, I'd also try turning the CTE into a temporary table first, then joining that to #Entries.

Related

Count records using Date filter related tables SQL Server

I'm trying to calculates the number of reports (report_user table) per user between two date (Calendar table) and by day worked (agenda_user table).
Here is the diagram of my tables:
Calendar table :
DATE Year Month
---------------------------------
2020-01-01 2020 1
2020-01-02 2020 1
2020-01-03 2020 1
2020-01-04 2020 1
AGENDA_USER table :
ID_USER DATE Value
---------------------------------
1 2020-01-01 1
2 2020-01-01 1
1 2020-01-02 0
2 2020-01-02 1
User table :
ID_USER Name
-------------------------
1 Jack
2 Robert
Report_Result table :
ID_USER Date Result
-----------------------------------
1 2020-01-01 good
1 2020-01-01 good
2 2020-01-01 bad
2 2020-01-01 good
2 2020-01-02 good
2 2020-01-02 good
Result I'm trying to find with an SQL query
ID_USER Date Number of report Day work report/work
---------------------------------------------------------------------------
1 2020-01-01 2 1 2/1 = 2
2 2020-01-01 2 1 1
1 2020-01-02 0 0 0
2 2020-01-02 2 1 2
SELECT
REPORT_USER.ID_USER,
COUNT(ID_USER) AS result
FROM [DB].[dbo].REPORT_USER AS report,
JOIN [DB].[dbo].[USER] AS [USER]
ON [USER].ID_USER = report.ID_USER
JOIN [DB].[dbo].AGENDA_USER AS agenda
ON agenda.ID_USER = report.ID_USER
WHERE CAST(agenda.[Date] AS DATE) >= '2020-09-01'
AND CAST(agenda.[Date] AS DATE) <= '2021-07-28'
AND [USER].ID_user = 1167
GROUP BY
report.ID_VENDEUR;
I'm not entirely sure I understand your problem, but I think I'm reasonably close so here is a start, point out my invalid assumptions and we can refine. More data, particularly in Agenda and Reports would really help. An explanation is below (plus see the comment in the code).
The overall flow is to generate a list of days/people you want to report on (cteUserDays), generate a list of how many reports each user generated on each date (cteReps), generate a list of who worked on what days (cteWork), and then JOIN all 3 parts together using a LEFT OUTER JOIN so the report covers all workers on all days.
EDIT: Add cteRepRaw where DATETIME is converted to DATE and "bad" reports are filtered out. Grouping and counting still happens in cteReps, but joining to cteUserDays is not there because it was adding 1 to count if there was a NULL.
DECLARE #Cal TABLE (CalDate DATETIME, CalYear int, CalMonth int)
DECLARE #Agenda TABLE (UserID int, CalDate DATE, AgendaVal int)
DECLARE #User TABLE (UserID int, UserName nvarchar(50))
DECLARE #Reps TABLE (UserID int, CalDate DATETIME, RepResult nvarchar(50))
INSERT INTO #Cal(CalDate, CalYear, CalMonth)
VALUES ('2020-01-01', 2020, 1), ('2020-01-02', 2020, 1), ('2020-01-03', 2020, 1), ('2020-01-04', 2020, 1)
INSERT INTO #Agenda(UserID, CalDate, AgendaVal)
VALUES (1, '2020-01-01', 1), (2, '2020-01-01', 1), (1, '2020-01-02', 0), (2, '2020-01-02', 1)
INSERT INTO #User (UserID , UserName )
VALUES (1, 'Jack'), (2, 'Robert')
INSERT INTO #Reps (UserID , CalDate , RepResult )
VALUES (1, '2020-01-01', 'good'), (1, '2020-01-01', 'good')
, (2, '2020-01-01', 'bad'), (2, '2020-01-01', 'good')
, (2, '2020-01-02', 'good'), (2, '2020-01-02', 'good')
; with cteUserDays as (
--First, you want zeros in your table where no reports are, so build a table for that
SELECT CONVERT(DATE, D.CalDate) as CalDate --EDIT add CONVERT here
, U.UserID FROM #Cal as D CROSS JOIN #User as U
WHERE D.CalDate >= '2020-01-01' AND D.CalDate <= '2021-07-28'
--EDIT Watch the <= date here, a DATE is < DATETIME with hours of the same day
), cteRepRaw as (--EDIT Add this CTE to convert DATETIME to DATE so we can group on it
--Map the DateTime to a DATE type so we can group reports from any time of day
SELECT R.UserID
, CONVERT(DATE, R.CalDate) as CalDate --EDIT add CONVERT here
, R.RepResult
FROM #Reps as R
WHERE R.RepResult='good' --EDIT Add this test to only count good ones
), cteReps as (
--Get the sum of all reports for a given user on a given day, though some might be missing (fill 0)
SELECT R.UserID , R.CalDate , COUNT(*) as Reports --SUM(COALESCE(R.RepResult, 0)) as Reports
FROM cteRepRaw as R--cteUserDays as D
--Some days may have no reports for that worker, so use a LEFT OUTER JOIN
--LEFT OUTER JOIN cteRepRaw as R on D.CalDate = R.CalDate AND D.UserID = R.UserID
GROUP BY R.UserID , R.CalDate
) , cteWork as (
--Unclear what values "value" in Agenda can take, but assuming it's some kind of work
-- unit, like "hours worked" or "shifts" so add them up
SELECT A.UserID , A.CalDate, SUM(A.AgendaVal) as DayWork FROM #Agenda as A
WHERE A.CalDate >= '2020-01-01' AND A.CalDate <= '2021-07-28'
GROUP BY A.CalDate, A.UserID
)
SELECT D.UserID , D.CalDate, COALESCE(R.Reports, 0) as Reports, W.DayWork
--NOTE: While it's probably a mistake to credit a report to a day a worker had
--no shifts, it could happen and would throw an error so check
, CASE WHEN W.DayWork > 0 THEN R.Reports / W.DayWork ELSE 0 END as RepPerWork
FROM cteUserDays as D
LEFT OUTER JOIN cteReps as R on D.CalDate=R.CalDate AND R.UserID = D.UserID
LEFT OUTER JOIN cteWork as W on D.UserID = W.UserID AND D.CalDate = W.CalDate
ORDER BY CalDate , UserID
First, as per the comments in your OP "Agenda" represents when the user is working, you don't say how it's structured so I'll assume it can have multiple entries for a given person on a given day (i.e. a 4 hour shift and an 8 hour shift) so I'll add them up to get total work (cteWork). I also assume that if somebody didn't work, you can't have a report for them. I check for this, but normally I'd expect your data validator to pre-screen those out.
Second, I'll assume reports are 1 per record, and a given user can have multiple per day. You have that in your given, but it's important to this solution so I'm restating in case somebody else reads this later.
Third, I assume you want all days reported for all users, I assure this by generating a CROSS join between users and days (cteUserDays)

Efficient Multiple Group-bys

I have the following table:
Year
Week
Day_1
Day_2
Day_3
2020
1
Walk
Jump
Swim
2020
3
Walk
Swim
Walk
2020
1
Jump
Walk
Swim
I want to group by YEAR, WEEK and Event (Walk, jump, Swim) and count the number of times each event occurs in Day_1, Day_2, Day_3. I.e.
Year
Week
Event
Count_Day_1
Count_Day_2
Count_Day_3
2020
1
Walk
1
1
0
2020
3
Walk
1
0
1
2020
1
Jump
1
1
0
2020
3
Jump
0
0
0
2020
1
Swim
0
0
2
2020
3
Swim
. 0
1
0
How can I do this efficiently?
In BigQuery, I would unpivot using arrays and then aggregate:
with t as (
select 2020 as year, 1 as week, 'Walk' as day_1, 'Jump' as day_2, 'Swim' as day_3 union all
select 2020, 3, 'Walk', 'Swim', 'Walk' union all
select 2020, 1, 'Jump', 'Walk', 'Swim'
)
select t.year, t.week, s.event,
countif(day = 1) as day_1, countif(day = 2) as day_2, countif(day = 3) as day_3
from t cross join
unnest([struct(t.day_1 as event, 1 as day),
struct(t.day_2 as event, 2 as day),
struct(t.day_3 as event, 3 as day)
]) s
group by t.year, t.week, s.event;
Consider this less verbose option
select year, week, event,
countif(offset = 0) as day_1,
countif(offset = 1) as day_2,
countif(offset = 2) as day_3
from `project.dataset.table`,
unnest([day_1, day_2, day_3]) event with offset
where not event is null
group by year, week, event
If applied to sample data in your question - output is
Demo code is MS SQL!
If you want to generate a full grid for every week and every year for every event then there are two pre-aggregates required, one for event and another one for every year and week.
Like:
DECLARE
#OriginalData
TABLE
(
numYear smallint,
numWeek tinyint,
dscDay1 nvarchar(20),
dscDay2 nvarchar(20),
dscDay3 nvarchar(20)
)
;
INSERT INTO
#OriginalData
(
numYear, numWeek, dscDay1, dscDay2, dscDay3
)
VALUES
( 2020, 1, N'Walk', N'Jump', N'Swim' ),
( 2020, 3, N'Walk', N'Swim', N'Walk' ),
( 2020, 1, N'Jump', N'Walk', N'Swim' )
;
SELECT
numYear, numWeek, dscDay1, dscDay2, dscDay3
FROM
#OriginalData
;
WITH
cteNormalise
(
dscActivity
)
AS
(
SELECT
dscDay1
FROM
#OriginalData
GROUP BY
dscDay1
UNION
SELECT
dscDay2
FROM
#OriginalData
GROUP BY
dscDay2
UNION
SELECT
dscDay3
FROM
#OriginalData
GROUP BY
dscDay3
),
cteGrid
(
numYear,
numWeek
)
AS
(
SELECT
numYear,
numWeek
FROM
#OriginalData
GROUP BY
numYear,
numWeek
)
SELECT
--/* Debug output */ *
YearWeek.numYear,
YearWeek.numWeek,
Normalised.dscActivity,
Count( Day1.dscDay1 ) AS CountDay1,
Count( Day2.dscDay2 ) AS CountDay2,
Count( Day3.dscDay3 ) AS CountDay3
FROM
cteNormalise AS Normalised
CROSS JOIN cteGrid AS YearWeek
LEFT OUTER JOIN #OriginalData AS Day1
ON Day1.dscDay1 = Normalised.dscActivity
AND Day1.numYear = YearWeek.numYear
AND Day1.numWeek = YearWeek.numWeek
LEFT OUTER JOIN #OriginalData AS Day2
ON Day2.dscDay2 = Normalised.dscActivity
AND Day2.numYear = YearWeek.numYear
AND Day2.numWeek = YearWeek.numWeek
LEFT OUTER JOIN #OriginalData AS Day3
ON Day3.dscDay3 = Normalised.dscActivity
AND Day3.numYear = YearWeek.numYear
AND Day3.numWeek = YearWeek.numWeek
GROUP BY
YearWeek.numYear,
YearWeek.numWeek,
Normalised.dscActivity
ORDER BY
YearWeek.numYear,
Normalised.dscActivity,
YearWeek.numWeek
;
This will work, however efficiency is questionable due to the steps to normalise the data before the actual aggregation happens.
If possible I suggest converting the table first into a 3NF with just key columns of Year, Week, Event and Day. Then a fairly efficient summary can be produced. At the cost of the normalisation beforehand. Otherwise the cost of transformation is required in the query.
You need to find distinct event, do cross join with your table and use conditional aggregation as follows:
select t.year, t.week, e.event,
count(case when t.day_1 = e.event then 1 end) as count_day_1,
count(case when t.day_2 = e.event then 1 end) as count_day_2,
count(case when t.day_3 = e.event then 1 end) as count_day_3
from your_Table t
cross join (select distinct day_1 as event from your_table
union all select day_2 from your_table
union all select day_3 from your_table) e
group by t.year, t.week, e.event

Normalization of Year bringing nulls back

I have the following query:
SELECT DISTINCT
YEAR(DateRegistered) as Years,
Months.[MonthName],
COUNT(UserID)as totalReg
FROM
Months WITH(NOLOCK)
LEFT OUTER JOIN
UserProfile WITH(NOLOCK)
ON
Months.MonthID = MONTH(DateRegistered)
AND
DateRegistered > DATEADD(MONTH, -12,GETDATE())
GROUP BY YEAR(DateRegistered), Months.[MonthName]
ORDER BY Months.[MonthName]
As you can tell this will always bring back 12 months worth of data. As such it is working, although there is a bug with this method.
It creates Null values in months where there is no data, now the record should exist(whole point of the query) but Year field is bringing Nulls which is something I dont want.
Now I understand the problem is because there is no data, how is it supposed to know what year?
So my question is - is there any way to sort this out and replace the nulls? I suspect I will have to completely change my methodology.
**YEAR** **MONTH** **TOTAL**
2013 April 1
2013 August 1
NULL December 0
2013 February 8
2013 January 1
2013 July 1
NULL June 0
2013 March 4
NULL May 0
NULL November 0
NULL October 0
2012 September 3
If you want 12 months of data, then construct a list of numbers from 1 to 12 and use these as offsets with getdate():
with nums as (
select 12 as level union all
select level - 1
from nums
where level > 1
)
select YEAR(thedate) as Years,
Months.[MonthName],
COUNT(UserID) as totalReg
FROM (select DATEADD(MONTH, - nums.level, GETDATE()) as thedate
from nums
) mon12 left outer join
Months WITH (NOLOCK)
on month(mon12.thedate) = months.monthid left outer join
UserProfile WITH (NOLOCK)
ON Months.MonthID = MONTH(DateRegistered) and
DateRegistered > DATEADD(MONTH, -12, GETDATE())
GROUP BY YEAR(thedate), Months.[MonthName]
ORDER BY Months.[MonthName];
I find something strange about the query though. You are defining the span from the current date. However, you seem to be splitting the months themselves on calendar boundaries. I also find the table months to be awkward. Why aren't you just using the datename() and month() functions?
Try this out:
;With dates as (
Select DateName(Month, getdate()) as [Month],
DatePart(Year, getdate()) as [Year],
1 as Iteration
Union All
Select DateName(Month,DATEADD(MONTH, -Iteration, getdate())),
DatePart(Year,DATEADD(MONTH, -Iteration, getdate())),
Iteration + 1
from dates
where Iteration < 12
)
SELECT DISTINCT
d.Year,
d.Month as [MonthName],
COUNT(up.UserID)as totalReg
FROM dates d
LEFT OUTER JOIN UserProfile up ON d.Month = DateName(DateRegistered)
And d.Year = DatePart(Year, DateRegistered)
GROUP BY d.Year, d.Month
ORDER BY d.Year, d.Month
Here's my attempt at a solution:
declare #UserProfile table
(
id bigint not null identity(1,1) primary key clustered
, name nvarchar(32) not null
, dateRegistered datetime not null default(getutcdate())
)
insert #UserProfile
select 'person 1', '2011-01-23'
union select 'person 2', '2013-01-01'
union select 'person 3', '2013-05-27'
declare #yearMin int, #yearMax int
select #yearMin = year(MIN(dateRegistered))
, #yearMax= year(MAX(dateRegistered))
from #UserProfile
;with monthCte as
(
select 1 monthNo, DATENAME(month, '1900-01-01') Name
union all
select monthNo + 1, DATENAME(month, dateadd(month,monthNo,'1900-01-01'))
from monthCte
where monthNo < 12
)
, yearCte as
(
select #yearMin yearNo
union all
select yearNo + 1
from yearCte
where yearNo < #yearMax
)
select y.yearNo, m.Name, COUNT(up.id) UsersRegisteredThisPeriod
from yearCte y
cross join monthCte m
left outer join #UserProfile up
on year(up.dateRegistered) = y.yearNo
and month(up.dateRegistered) = m.monthNo
group by y.yearNo, m.monthNo, m.Name
order by y.yearNo, m.monthNo
SQL Fiddle Version: http://sqlfiddle.com/#!6/d41d8/6640
You have to calculate the counts in a Derived Table (or a CTE) first and then join
untested:
SELECT
COALESCE(dt.Years, YEAR(DATEADD(MONTH, -Months.MonthID, GETDATE()))),
Months.[MonthName],
COALESCE(dt.totalReg, 0)
FROM
Months WITH(NOLOCK)
LEFT OUTER JOIN
(
SELECT
YEAR(DateRegistered) AS Years,
MONTH(DateRegistered) AS Mon,
COUNT(UserID)AS totalReg
FROM UserProfile WITH(NOLOCK)
WHERE DateRegistered > DATEADD(MONTH, -12,GETDATE())
GROUP BY
YEAR(DateRegistered),
MONTH(DateRegistered)
) AS dt
ON Months.MonthID = dt.mon
ORDER BY 1, Months.MonthID
I changed the order to Months.MonthID instead of MonthName and i added year because you might have august 2012 and 2013 in your result.

SQL Case select

maybe someone can help me
i have an SQL datebase that is used for logging employees leave. we have some many different shifts, that on our employees table we have 7 fields that represent the days they work. these are a bit data type, 1 for working that day, and 0 for not working.
a second table has all the employees leave. containing employee id, leave date and reason.
i can easily query the employees table and get how many people are to work on any given day of the week, and i can easily query the leave table to see how many people are off on a given date.
what i looking to do is based on the day of the week in the leave table, count how many people are supposed to be in on that day.
the code im trying to make work is
select TBL_Leave.Leave_Date AS 'Date',
datepart(weekday,TBL_Leave.Leave_Date) - 1 AS 'Day Of Week',
count(TBL_Leave.Leave_Date) AS 'Total Off',
case
when datepart(weekday,TBL_Leave.Leave_Date) - 1 = 5 then select SUM(convert(int,Mon)) from TBL_Employees)
else 'Flase'
end
from TBL_Leave
where Leave_Date between '2010-01-01' AND '2010-12-31'
group by TBL_Leave.Leave_Date
but sure enough, it dont work.
im trying to count the number of people working from one table based the the day of the week from a field in another.
any help anyone can give will be great
cheers
Paul
i have this query to get how many people are off on any date
select TBL_Leave.Leave_Date AS 'Date',
datepart(weekday,TBL_Leave.Leave_Date) - 1 AS 'Day Of Week',
count(TBL_Leave.Leave_Date) AS 'Total Off'
from TBL_Leave
where Leave_Date between '2010-01-01' AND '2010-12-31'
group by TBL_Leave.Leave_Date
and this to see how many people are in on any day
select SUM(convert(int,Mon)) as 'Monday',
SUM(convert(int,Tue)) AS 'Tuesday',
SUM(convert(int,Wed)) AS 'Wednesday',
SUM(convert(int,Thu)) AS 'Thursday',
SUM(convert(int,Fri)) AS 'Friday',
SUM(convert(int,Sat)) AS 'Saturday',
SUM(convert(int,Sun)) AS 'Sunday'
from TBL_Employees
where planned = 1
IMHO you should set a view in place as a helper for queries like these:
create view V_EmployeeWorkingDays as
select EmployeeID,
case ShortDayName
when 'Mon' then 1 when 'Tue' then 2 when 'Wed' then 3
when 'Thu' then 4 when 'Fri' then 5 when 'Sat' then 6
when 'Sun' then 7 end as weekday,
IsWorking
from TBL_Employees
unpivot (IstWorking for ShortDayName in (Mon,Tue,Wed,Thu,Fri,Sat,Sun)) p;
Secondly you need the calendar dates within your range. You could use a function like this:
create function F_DateValues(#FromDate datetime, #ToDate datetime)
returns table as
return (
select dateadd(day,Nr-1,#FromDate) as Date
from (select row_number() over (rand()) as Nr
from (values (1),(1),(1),(1)) a
cross join (values (1),(1),(1),(1)) b
cross join (values (1),(1),(1),(1)) c
cross join (values (1),(1),(1),(1)) c) n
where Nr > datediff(day,#FromDate,#ToDate)
);
Now you can put this alltogether:
select d.Date,
isnull(w.CountWorkingPlanned,0)-isnull(l.CountLeaves,0) as CountWorking
from F_DateValue('20101118','20101128') d
left join (select LeaveDate, count(*) as CountLeaves
from TBL_LeaveDate group by LeaveDate) l
on l.LeaveDate = d.Date
left join (select weekday, count(*) as CountWorkingPlanned
from V_EmployeeWorkingDays where IsWorking=1 group by weekday) w
on w.weekday = datepart(weekday,d.Date);
This should be working (not tested - so please don't kill me for typos ;) ).
You should redesign the table layout. As you have a field for each weekday, that means that you have data in the field names. Data belongs inside the table, so you should put that data as rows in a separate table.
Then it's easy to get the data. Example:
select count(*)
from Employees e
left join Leave l on l.EmployeeId = e.EmployeeId and LeaveDate = #Today
left join Workdays w on w.EmployeeId = e.EmployeeId and w.WeekDay = datepart(weekday, #Today)
where l..EmployeeId is null and w.EmployeeId is null
SELECT count(id) FROM employee WHERE monday = true
Seems easy enough unless I still don't get what you need...
Here's a query that might work for you.
The query uses derived queries to get the leave and work counts. I included an UNPIVOT operation on the TBL_Employee data to make it easier to get the employee data. You can avoid this with design changes that have been suggested.
SELECT Leave.Leave_Date, WorkCount, LeaveCount,
WorkCount-LeaveCount AS CountDifference
FROM
(
-- Get Leave counts by date
SELECT Leave_Date, UPPER(LEFT(DATENAME(dw, Leave_Date), 3)) AS WorkDay,
COUNT(*) as LeaveCount
FROM TBL_Leave
WHERE Leave_Date between '2010-01-01' AND '2010-12-31'
GROUP BY Leave_Date, UPPER(LEFT(DATENAME(dw, Leave_Date), 3))
) AS Leave
LEFT OUTER JOIN
(
-- Get Work counts by day of week
SELECT WorkDay, COUNT(*) WorkCount
FROM
(
SELECT EmpID, Mon, Tue, Wed, Thu, Fri, Sat, Sun
FROM TBL_Employees
) p
UNPIVOT
(IsWorking FOR WorkDay IN
(Mon, Tue, Wed, Thu, Fri, Sat, Sun)
)AS unpvt
WHERE unpvt.IsWorking = 1
GROUP BY WorkDay
) AS Work ON Leave.WorkDay = Work.WorkDay -- Join on day of week
thanks for everyones help on this, i have picked up a few tips. i managed to get this sorted last night and when i look at it, i think i made it sound more complicated than it is. here is what i came up with.
create table TEMP_planned (id int null, mon int null, tue int null, wed int null, thur int null, fri int null, sat int null, sun int null)
insert into TEMP_planned (id, mon, tue, wed, thur, fri, sat, sun)
values(1,
(select SUM(convert(int,Mon)) from TBL_Employees where Planned = 1),
(select SUM(convert(int,Tue)) from TBL_Employees where Planned = 1),
(select SUM(convert(int,Wed)) from TBL_Employees where Planned = 1),
(select SUM(convert(int,Thu)) from TBL_Employees where Planned = 1),
(select SUM(convert(int,Fri)) from TBL_Employees where Planned = 1),
(select SUM(convert(int,Sat)) from TBL_Employees where Planned = 1),
(select SUM(convert(int,Sun)) from TBL_Employees where Planned = 1))
select TBL_Leave.Leave_Date,
'Planned' = case DATEPART(dw,TBL_Leave.Leave_Date) - 1
when 1 then (select mon from TEMP_Planned where ID = 1)
when 2 then (select tue from TEMP_Planned where ID = 1)
when 3 then (select wed from TEMP_Planned where ID = 1)
when 4 then (select thur from TEMP_Planned where ID = 1)
when 5 then (select fri from TEMP_Planned where ID = 1)
when 6 then (select sat from TEMP_Planned where ID = 1)
when 7 then (select sun from TEMP_Planned where ID = 1)
end,
COUNT(tbl_leave.Leave_Date) as 'Total Staff Off'
from TBL_Leave
group by TBL_Leave.Leave_Date
drop table temp_planned

SQL calculate number of days of residency by month, by user, by location

I'm working on a query for a rehab organization where tenants (client/patients) live in a building when they first arrive, as they progress in their treatment they move to another building and as they near the end of treatment they are in a third building.
For funding purposes we need to know how many nights a tenant spent in each building in each month.
I can use DateDiff to get the total number of nights, but how do I get the total for each client in each month in each building?
For example, John Smith is in Building A 9/12-11/3; moves to Building B 11/3-15; moves to Building C on and is still there: 11/15 - today
What query returns a result that show the number of nights he spent in:
Building A in Septmeber, October and November.
Buidling B in November
Building C in November
Two tables hold the client's name, building name and move-in date and move-out date
CREATE TABLE [dbo].[clients](
[ID] [nvarchar](50) NULL,
[First_Name] [nvarchar](100) NULL,
[Last_Name] [nvarchar](100) NULL
) ON [PRIMARY]
--populate w/ two records
insert into clients (ID,First_name, Last_name)
values ('A2938', 'John', 'Smith')
insert into clients (ID,First_name, Last_name)
values ('A1398', 'Mary', 'Jones')
CREATE TABLE [dbo].[Buildings](
[ID_U] [nvarchar](50) NULL,
[Move_in_Date_Building_A] [datetime] NULL,
[Move_out_Date_Building_A] [datetime] NULL,
[Move_in_Date_Building_B] [datetime] NULL,
[Move_out_Date_Building_B] [datetime] NULL,
[Move_in_Date_Building_C] [datetime] NULL,
[Move_out_Date_Building_C] [datetime] NULL,
[Building_A] [nvarchar](50) NULL,
[Building_B] [nvarchar](50) NULL,
[Building_C] [nvarchar](50) NULL
) ON [PRIMARY]
-- Populate the tables with two records
insert into buildings (ID_U,Move_in_Date_Building_A,Move_out_Date_Building_A, Move_in_Date_Building_B,
Move_out_Date_Building_B, Move_in_Date_Building_C, Building_A, Building_B, Building_C)
VALUES ('A2938','2010-9-12', '2010-11-3','2010-11-3','2010-11-15', '2010-11-15', 'Kalgan', 'Rufus','Waylon')
insert into buildings (ID_U,Move_in_Date_Building_A,Building_A)
VALUES ('A1398','2010-10-6', 'Kalgan')
Thanks for your help.
I'd use a properly normalized database schema, your Buildings table is not useful like this. After splitting it up I believe that getting your answer will be pretty easy.
Edit (and updated): Here's a CTE which will take this strange table structure and split it into a more normalized form, displaying the user id, building name, move in and move out dates. By grouping on the ones you want (and using DATEPART() etc.) you should be able to get the data you need with that.
WITH User_Stays AS (
SELECT
ID_U,
Building_A Building,
Move_in_Date_Building_A Move_In,
COALESCE(Move_out_Date_Building_A, CASE WHEN ((Move_in_Date_Building_B IS NULL) OR (Move_in_Date_Building_C<Move_in_Date_Building_B)) AND (Move_in_Date_Building_C>Move_in_Date_Building_A) THEN Move_in_Date_Building_C WHEN Move_in_Date_Building_B>=Move_in_Date_Building_A THEN Move_in_Date_Building_B END, GETDATE()) Move_Out
FROM dbo.Buildings
WHERE Move_in_Date_Building_A IS NOT NULL
UNION ALL
SELECT
ID_U,
Building_B,
Move_in_Date_Building_B,
COALESCE(Move_out_Date_Building_B, CASE WHEN ((Move_in_Date_Building_A IS NULL) OR (Move_in_Date_Building_C<Move_in_Date_Building_A)) AND (Move_in_Date_Building_C>Move_in_Date_Building_B) THEN Move_in_Date_Building_C WHEN Move_in_Date_Building_A>=Move_in_Date_Building_B THEN Move_in_Date_Building_A END, GETDATE())
FROM dbo.Buildings
WHERE Move_in_Date_Building_B IS NOT NULL
UNION ALL
SELECT
ID_U,
Building_C,
Move_in_Date_Building_C,
COALESCE(Move_out_Date_Building_C, CASE WHEN ((Move_in_Date_Building_B IS NULL) OR (Move_in_Date_Building_A<Move_in_Date_Building_B)) AND (Move_in_Date_Building_A>Move_in_Date_Building_C) THEN Move_in_Date_Building_A WHEN Move_in_Date_Building_B>=Move_in_Date_Building_C THEN Move_in_Date_Building_B END, GETDATE())
FROM dbo.Buildings
WHERE Move_in_Date_Building_C IS NOT NULL
)
SELECT *
FROM User_Stays
ORDER BY ID_U, Move_In
This query run on your sample data produces he following output:
ID_U Building Move_In Move_Out
-------- ----------- ----------------------- -----------------------
A1398 Kalgan 2010-10-06 00:00:00.000 2010-11-23 18:35:59.050
A2938 Kalgan 2010-09-12 00:00:00.000 2010-11-03 00:00:00.000
A2938 Rufus 2010-11-03 00:00:00.000 2010-11-15 00:00:00.000
A2938 Waylon 2010-11-15 00:00:00.000 2010-11-23 18:35:59.050
(4 row(s) affected)
As you can see, from here on it will be much easier to isolate the days per patient or building, and also to find the records for specific months and calculate the correct stay duration in that case. Note that the CTE displays the current date for patients which are still in a building.
Edit (again): In order to get all months including their start and end dates for all relevant years, you can use a CTE like this:
WITH User_Stays AS (
[...see above...]
)
,
Months AS (
SELECT m.IX,
y.[Year], dateadd(month,(12*y.[Year])-22801+m.ix,0) StartDate, dateadd(second, -1, dateadd(month,(12*y.[Year])-22800+m.ix,0)) EndDate
FROM (
SELECT 1 IX UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 8 UNION ALL
SELECT 9 UNION ALL
SELECT 10 UNION ALL
SELECT 11 UNION ALL
SELECT 12
)
m
CROSS JOIN (
SELECT Datepart(YEAR, us.Move_In) [Year]
FROM User_Stays us UNION
SELECT Datepart(YEAR, us.Move_Out)
FROM User_Stays us
)
y
)
SELECT *
FROM months;
So since we now have a tabular representation of all date ranges which can be of interest, we simply join this together:
WITH User_Stays AS ([...]),
Months AS ([...])
SELECT m.[Year],
DATENAME(MONTH, m.StartDate) [Month],
us.ID_U,
us.Building,
DATEDIFF(DAY, CASE WHEN us.Move_In>m.StartDate THEN us.Move_In ELSE m.StartDate END, CASE WHEN us.Move_Out<m.EndDate THEN us.Move_Out ELSE DATEADD(DAY, -1, m.EndDate) END) Days
FROM Months m
JOIN User_Stays us ON (us.Move_In < m.EndDate) AND (us.Move_Out >= m.StartDate)
ORDER BY m.[Year],
us.ID_U,
m.Ix,
us.Move_In
Which finally produces this output:
Year Month ID_U Building Days
----------- ------------ -------- ---------- -----------
2010 October A1398 Kalgan 25
2010 November A1398 Kalgan 22
2010 September A2938 Kalgan 18
2010 October A2938 Kalgan 30
2010 November A2938 Kalgan 2
2010 November A2938 Rufus 12
2010 November A2938 Waylon 8
-- set the dates for which month you want
Declare #startDate datetime
declare #endDate datetime
set #StartDate = '09/01/2010'
set #EndDate = '09/30/2010'
select
-- determine if the stay occurred during this month
Case When #StartDate <= Move_out_Date_Building_A and #EndDate >= Move_in_Date_Building_A
Then
(DateDiff(d, #StartDate , #enddate+1)
)
-- drop the days off the front
- (Case When #StartDate < Move_in_Date_Building_A
Then datediff(d, #StartDate, Move_in_Date_Building_A)
Else 0
End)
--drop the days of the end
- (Case When #EndDate > Move_out_Date_Building_A
Then datediff(d, #EndDate, Move_out_Date_Building_A)
Else 0
End)
Else 0
End AS Building_A_Days_Stayed
from Clients c
inner join Buildings b
on c.id = b.id_u
Try using a date table. For example, you could create one like so:
CREATE TABLE Dates
(
[date] datetime,
[year] smallint,
[month] tinyint,
[day] tinyint
)
INSERT INTO Dates(date)
SELECT dateadd(yy, 100, cast(row_number() over(order by s1.object_id) as datetime))
FROM sys.objects s1
CROSS JOIN sys.objects s2
UPDATE Dates
SET [year] = year(date),
[month] = month(date),
[day] = day(date)
Just modify the initial Dates population to meet your needs (on my test instance, the above yielded dates from 2000-01-02 to 2015-10-26). With a dates table, the query is pretty straight forward, something like this:
select c.First_name, c.Last_name,
b.Building_A BuildingName, dA.year, dA.month, count(distinct dA.day) daysInBuilding
from clients c
join Buildings b on c.ID = b.ID_U
left join Dates dA on dA.date between b.Move_in_Date_Building_A and isnull(b.Move_out_Date_Building_A, getDate())
group by c.First_name, c.Last_name,
b.Building_A, dA.year, dA.month
UNION
select c.First_name, c.Last_name,
b.Building_B, dB.year, dB.month, count(distinct dB.day)
from clients c
join Buildings b on c.ID = b.ID_U
left join Dates dB on dB.date between b.Move_in_Date_Building_B and isnull(b.Move_out_Date_Building_B, getDate())
group by c.First_name, c.Last_name,
b.Building_B, dB.year, dB.month
UNION
select c.First_name, c.Last_name,
b.Building_C, dC.year, dC.month, count(distinct dC.day)
from clients c
join Buildings b on c.ID = b.ID_U
left join Dates dC on dC.date between b.Move_in_Date_Building_C and isnull(b.Move_out_Date_Building_C, getDate())
group by c.First_name, c.Last_name,
b.Building_C, dC.year, dC.month
If you can't restructure the Building table you can create a query that will normalize it for you and allow for easier calculations:
SELECT "A" as Building, BuidlingA as Name, Move_in_Date_Building_A as MoveInDate,
Move_out_Date_Building_A As MoveOutDate
UNION
SELECT "B", BuidlingB, Move_in_Date_Building_B, Move_out_Date_Building_B
UNION
SELECT "C", BuidlingC, Move_in_Date_Building_C, Move_out_Date_Building_C