I have two tables in SQL Server:
EMP
On_Vacation with colums EmpId, LeaveType, StartingFrom, EndingTo,RejoiningDate
The On_Vacation table stores the information of employees who are on leave.
I'm trying to query the table in such a way that my query has the following columns:
EmpId, 24-1-2016, 25-1-2016, 26-1-2016, 27-1-2016, 28-1-2016
The result query columns are the dates of this week. If Employee is not on leave on these dates, it should write available. Otherwise it should write the leave type.
I'm very new to this type of queries, kindly help me experts..
You can't change the column name without dynamic sQL (which complicates matters).
Here's another suggestion: what is you change your date table to includes the name of the day of the week, like this:
CREATE TABLE DaysWeeks
(CalYear SMALLINT NOT NULL,
WeekNumber TINYINT NOT NULL,
CalDate DATE NOT NULL,
DayOfWeekNumber TINYINT,
DayOfWeekName VARCHAR(9)
CONSTRAINT PK_DaysWeeks PRIMARY KEY CLUSTERED (CalYear, WeekNumber, CalDate)
)
with values like this:
INSERT INTO dbo.DaysWeeks
( CalYear, WeekNumber, CalDate,DayOfWeekNumber,DayOfWeekName )
VALUES ( 2016, 4, '01/24/2016',1,'Sunday'),
( 2016, 4, '01/25/2016',2,'Monday'),
( 2016, 4, '01/26/2016',3,'Tuesday'),
( 2016, 4, '01/27/2016',4,'Wednesday'),
( 2016, 4, '01/28/2016',5,'Thursday'),
( 2016, 4, '01/29/2016',6,'Friday'),
( 2016, 4, '01/30/2016',7,'Saturday')
Now, you can have a query which pivots based on the day of the week:
WITH cte AS (
SELECT EmpDays.Employee, EmpDays.CalDate, EmpDays.DoWName, ISNULL(v.Leave_Type,'Available') AS Available
FROM dbo.On_Vacation v
RIGHT OUTER JOIN
(SELECT e.EmpID AS Employee, dw.CalDate AS CalDate, dw.DayOfWeekName AS DoWName
FROM dbo.DaysWeeks dw,
dbo.Employee e
WHERE dw.CalYear = 2016 AND dw.WeekNumber = 4) AS EmpDays
ON
v.EmpID = EmpDays.Employee
AND v.StartingFrom <= empdays.CalDate
AND v.EndingTo >= empdays.CalDate
)
SELECT * FROM cte
PIVOT (MAX(cte.Available) FOR DoWName IN (['Sunday'],['Monday'],['Tuesday'],['Wednesday'],['Thursday'],['Friday'],['Saturday'])
If your really need the actual dates in your columns, I would adjust this to use dynamic SQL. But before doing that (which, IMHO, makes the code much harder to read and maintain, not that this is so straightforward), I'd ask how you were going to present the data and therefore whether that might be handled in the report or presentation layer.
You would do this with conditional aggregation:
select e.empid,
coalesce(max(case when '2016-01-24' between v.startdate and v.enddate
then leave_type end),
'available') as [2016-01-14],
coalesce(max(case when '2016-01-25' between v.startdate and v.enddate
then leave_type end),
'available') as [2016-01-15],
. . .
from emp e left join
from vacation v
on e.empid = v.empid
group by e.empid;
Related
I'm having a bit of an issue wrapping my head around the logic of this changing dimension. I would like to associate these two tables below. I need to match the Cost - Period fact table to the cost dimension based on the Id and the effective date.
As you can see - if the month and year field is greater than the effective date of its associated Cost dimension, it should adopt that value. Once a new Effective Date is entered into the dimension, it should use that value for any period greater than said date going forward.
EDIT: I apologize for the lack of detail but the Cost Dimension will actually have a unique Index value and the changing fields to reference for the matching would be Resource, Project, Cost. I tried to match the query you provided with my fields, but I'm getting the incorrect output.
FYI: Naming convention change: EngagementId is Id, Resource is ConsultantId, and Project is ProjectId
I've changed the images below and here is my query
,_cte(HoursWorked, HoursBilled, Month, Year, EngagementId, ConsultantId, ConsultantName, ProjectId, ProjectName, ProjectRetainer, RoleId, Role, Rate, ConsultantRetainer, Salary, amount, EffectiveDate)
as
(
select sum(t.Duration), 0, Month(t.StartDate), Year(t.StartDate), t.EngagementId, c.ConsultantId, c.ConsultantName, c.ProjectId, c.ProjectName, c.ProjectRetainer, c.RoleId, c.Role, c.Rate, c.ConsultantRetainer,
c.Salary, 0, c.EffectiveDate
from timesheet t
left join Engagement c on t.EngagementId = c.EngagementId and Month(c.EffectiveDate) = Month(t.EndDate) and Year(c.EffectiveDate) = Year(t.EndDate)
group by Month(t.StartDate), Year(t.StartDate), t.EngagementId, c.ConsultantName, c.ConsultantId, c.ProjectId, c.ProjectName, c.ProjectRetainer, c.RoleId, c.Role, c.Rate, c.ConsultantRetainer,
c.Salary, c.EffectiveDate
)
select * from _cte where EffectiveDate is not null
union
select _cte.HoursWorked, _cte.HoursBilled, _cte.Month, _cte.Year, _cte.EngagementId, _cte.ConsultantId, _cte.ConsultantName, _cte.ProjectId, _Cte.ProjectName, _cte.ProjectRetainer, _cte.RoleId, _cte.Role, sub.Rate, _cte.ConsultantRetainer,_cte.Salary, _cte.amount, sub.EffectiveDate
from _cte
outer apply (
select top 1 EffectiveDate, Rate
from Engagement e
where e.ConsultantId = _cte.ConsultantId and e.ProjectId = _cte.ProjectId and e.RoleId = _cte.RoleId
and Month(e.EffectiveDate) < _cte.Month and Year(e.EffectiveDate) < _cte.Year
order by EffectiveDate desc
) sub
where _cte.EffectiveDate is null
Example:
I'm struggling with writing the query that goes along with this. At first I attempted to partition by greatest date. However, when I executed the join I got the highest effective date for every single period (even those prior to the effective date).
Is this something that can be accomplished in a query or should I be focusing on incremental updates of the destination table so that any effective date / time period in the past is left alone?
Any tips would be great!
Thanks,
Channing
Try this one:
; with _CTE as(
select p.* , c.EffectiveDate, c.Cost
from period p
left join CostDimension c on p.id = c.id and p.Month = DATEPART(month, c.EffectiveDate) and p.year = DATEPART (year, EffectiveDate)
)
select * from _CTE Where EffectiveDate is not null
Union
select _CTE.id, _CTE.Month, _CTE.Year, sub.EffectiveDate, sub.Cost
from _CTE
outer apply (select top 1 EffectiveDate, Cost
from CostDimension as cd
where cd.Id = _CTE.id and cd.EffectiveDate < DATETIMEFROMPARTS(_CTE.Year, _CTE.Month, 1, 0, 0, 0, 0)
order by EffectiveDate desc
) sub
where _Cte.EffectiveDate is null
I have table like this
declare #data table
(
id int not null,
groupid int not null,
startDate datetime not null,
endDate datetime not null
)
insert into #data values
(1, 1, '20150101', '20150131'),
(2, 1, '20150114', '20150131'),
(3, 1, '20150201', '20150228');
and my current selecting statement is:
select groupid, 'some data', min(id), count(*)
from #data
group by groupid
But now I need to group records if it have intersected periods
desired result:
1, 'some data', 1, 2
1, 'some data', 3, 1
Is someone know how to do this?
One method is to identify the beginning of each group -- because it doesn't overlap with the previous one. Then, count the number of these as a group identifier.
with overlaps as (
select id
from #data d
where not exists (select 1
from #data d2
where d.groupid = d2.groupid and
d.startDate >= d2.startDate and
d.startDate < d2.endDate
)
),
groups as (
select d.*,
count(o.id) over (partition by groupid
order by d.startDate) as grpnum
from #data d left join
overlaps o
on d.id = o.id
)
select groupid, min(id), count(*),
min(startDate) as startDate, max(endDate) as endDate
from groups
group by grpnum, groupid;
Notes: This is using cumulative counts, which are available in SQL Server 2012+. You can do something similar with a correlated subquery or apply in earlier versions.
Also, this query assumes that the start dates are unique. If they are not, the query can be tweaked, but the logic becomes a bit more complicated.
We have a time management system where our employees or contractors (resources) enter the hours they have worked, and we derive a cost for it. I have a table with the historic costs:
CREATE TABLE ResourceTimeTypeCost (
ResourceCode VARCHAR(32),
TimeTypeCode VARCHAR(32),
EffectiveDate DATETIME,
CostRate DECIMAL(12,2)
)
So I have one date field which marks the effective date. If we have a record which is
('ResourceA', 'Normal', '2012-04-30', 40.00)
and I add a record which is
('ResourceA', 'Normal', '2012-05-04', 50.00)
So all hours entered between the 30th April and the 3rd of May will be at £40.00, all time after midnight on the 4th will be at £50.00. I understand this in principle but how do you write a query expressing this logic?
Assuming my time table looks like the below
CREATE TABLE TimeEntered (
ResourceCode VARCHAR(32),
TimeTypeCode VARCHAR(32),
ProjectCode VARCHAR(32),
ActivityCode VARCHAR(32),
TimeEnteredDate DATETIME,
HoursWorked DECIMAL(12,2)
)
If I insert the following records into the TimeEntered table
('ResourceA','Normal','Project1','Management1','2012-04-30',7.5)
('ResourceA','Normal','Project1','Management1','2012-05-01',7.5)
('ResourceA','Normal','Project1','Management1','2012-05-02',7.5)
('ResourceA','Normal','Project1','Management1','2012-05-03',7.5)
('ResourceA','Normal','Project1','Management1','2012-05-04',7.5)
('ResourceA','Normal','Project1','Management1','2012-05-07',7.5)
('ResourceA','Normal','Project1','Management1','2012-05-08',7.5)
I'd like to get a query that returns the total cost by resource
So in the case above it would be 'ResourceA', (4 * 7.5 * 40) + (3 * 7.5 * 50) = 2325.00
Can anyone provide a sample SQL query? I know this example doesn't make use of TimeType (i.e. it's always 'Normal') but I'd like to see how this is dealt with as well
I can't change the structure of the database. Many thanks in advance
IF OBJECT_ID ('tempdb..#ResourceTimeTypeCost') IS NOT NULL DROP TABLE #ResourceTimeTypeCost
CREATE TABLE #ResourceTimeTypeCost ( ResourceCode VARCHAR(32), TimeTypeCode VARCHAR(32), EffectiveDate DATETIME, CostRate DECIMAL(12,2) )
INSERT INTO #ResourceTimeTypeCost
SELECT 'ResourceA' as resourcecode, 'Normal' as timetypecode, '2012-04-30' as effectivedate, 40.00 as costrate
UNION ALL
SELECT 'ResourceA', 'Normal', '2012-05-04', 50.00
IF OBJECT_ID ('tempdb..#TimeEntered') IS NOT NULL DROP TABLE #TimeEntered
CREATE TABLE #TimeEntered ( ResourceCode VARCHAR(32), TimeTypeCode VARCHAR(32), ProjectCode VARCHAR(32), ActivityCode VARCHAR(32), TimeEnteredDate DATETIME, HoursWorked DECIMAL(12,2) )
INSERT INTO #TimeEntered
SELECT 'ResourceA','Normal','Project1','Management1','2012-04-30',7.5
UNION ALL SELECT 'ResourceA','Normal','Project1','Management1','2012-05-01',7.5
UNION ALL SELECT 'ResourceA','Normal','Project1','Management1','2012-05-02',7.5
UNION ALL SELECT 'ResourceA','Normal','Project1','Management1','2012-05-03',7.5
UNION ALL SELECT 'ResourceA','Normal','Project1','Management1','2012-05-04',7.5
UNION ALL SELECT 'ResourceA','Normal','Project1','Management1','2012-05-07',7.5
UNION ALL SELECT 'ResourceA','Normal','Project1','Management1','2012-05-08',7.5
;with ranges as
(
select
resourcecode
,TimeTypeCode
,EffectiveDate
,costrate
,row_number() OVER (PARTITION BY resourcecode,timetypecode ORDER BY effectivedate ASC) as row
from #ResourceTimeTypeCost
)
,ranges2 AS
(
SELECT
r1.resourcecode
,r1.TimeTypeCode
,r1.EffectiveDate
,r1.costrate
,r1.effectivedate as start_date
,ISNULL(DATEADD(ms,-3,r2.effectivedate),GETDATE()) as end_date
FROM ranges r1
LEFT OUTER JOIN ranges r2 on r2.row = r1.row + 1 --joins onto the next date row
AND r2.resourcecode = r1.resourcecode
AND r2.TimeTypeCode = r1.TimeTypeCode
)
SELECT
tee.resourcecode
,tee.timetypecode
,tee.projectcode
,tee.activitycode
,SUM(ranges2.costrate * tee.hoursworked) as total_cost
FROM #TimeEntered tee
INNER JOIN ranges2 ON tee.TimeEnteredDate >= ranges2.start_date
AND tee.TimeEnteredDate <= ranges2.end_date
AND tee.resourcecode = ranges2.resourcecode
AND tee.timetypecode = ranges2.TimeTypeCode
GROUP BY tee.resourcecode
,tee.timetypecode
,tee.projectcode
,tee.activitycode
What you have is a cost table that is, as some would say, a slowly changing dimension. First, it will help to have an effective and end date for the cost table. We can get this by doing a self join and group by:
with costs as
(select c.ResourceCode, c.EffectiveDate as effdate,
dateadd(day, -1, min(c1.EffectiveDate)) as endDate,
datediff(day, c.EffectiveDate, c1.EffectiveDate) - 1 as Span
from ResourceTimeTypeCost c left outer join
ResourceTimeTypeCost c1
group by c.ResourceCode, c.EffectiveDate
)
Although you say you cannot change the table structure, when you have a slowly changing dimension, having an effective and end date is good practice.
Now, you can use this infomation with TimeEntered as following:
select te.*, c.CostRate * te.HoursWorked as dayCost
from TimeEntered te join
Costs c
on te.ResouceCode = c.ResourceCode and
te.TimeEntered between c.EffDate and c.EndDate
To summarize by Resource for a given time range, the full query would look like:
with costs as
(select c.ResourceCode, c.EffectiveDate as effdate,
dateadd(day, -1, min(c1.EffectiveDate)) as endDate,
datediff(day, c.EffectiveDate, c1.EffectiveDate) - 1 as Span
from ResourceTimeTypeCost c left outer join
ResourceTimeTypeCost c1
group by c.ResourceCode, c.EffectiveDate
),
te as
(select te.*, c.CostRate * te.HoursWorked as dayCost
from TimeEntered te join
Costs c
on te.ResouceCode = c.ResourceCode and
te.TimeEntered between c.EffDate and c.EndDate
)
select te.ResourceCode, sum(dayCost)
from te
where te.TimeEntered >= <date1> and te.TimeEntered < <date2>
You might give this a try. CROSS APPLY will find first ResourceTimeTypeCost with older or equal date and same ResourceCode and TimeTypeCode as current record from TimeEntered.
SELECT te.ResourceCode,
te.TimeTypeCode,
te.ProjectCode,
te.ActivityCode,
te.TimeEnteredDate,
te.HoursWorked,
te.HoursWorked * rttc.CostRate Cost
FROM TimeEntered te
CROSS APPLY
(
-- First one only
SELECT top 1 CostRate
FROM ResourceTimeTypeCost
WHERE te.ResourceCode = ResourceTimeTypeCost.ResourceCode
AND te.TimeTypeCode = ResourceTimeTypeCost.TimeTypeCode
AND te.TimeEnteredDate >= ResourceTimeTypeCost.EffectiveDate
-- By most recent date
ORDER BY ResourceTimeTypeCost.EffectiveDate DESC
) rttc
Unfortunately I can no longer find article on msdn, hence the blog in link above.
Live test # Sql Fiddle.
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
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