SQL query for getting active employees in specific period - sql

Having the following table:
ID EmployeeID Status EffectiveDate
------------------------------------------------------
1 110545 Active 01AUG2011
2 110700 Active 05JAN2012
3 110060 Active 05JAN2012
4 110222 Active 30JUN2012
5 110545 Resigned 01JUL2012
6 110545 Active 12FEB2013
How do I get the number of active (or partially active) in a specific period?
For example, if I want to know all active (or partially active) employees from 01JAN2011 to 01AUG2012 I should get 4 (according to the table above). If I want to know all active employees from 01AUG2012 to 01JAN2013 it should be 3 only (because employee 110454 is resigned).
How will I do that?

Sample data:
CREATE TABLE #Employee
(
ID integer NOT NULL,
EmployeeID integer NOT NULL,
[Status] varchar(8) NOT NULL,
EffectiveDate date NOT NULL,
CONSTRAINT [PK #Employee ID]
PRIMARY KEY CLUSTERED (ID)
);
INSERT #Employee
(ID, EmployeeID, [Status], EffectiveDate)
VALUES
(1, 110545, 'Active', '20110801'),
(2, 110700, 'Active', '20120105'),
(3, 110060, 'Active', '20120105'),
(4, 110222, 'Active', '20120630'),
(5, 110545, 'Resigned', '20120701'),
(6, 110545, 'Active', '20130212');
Helpful indexes:
CREATE NONCLUSTERED INDEX Active
ON #Employee
(EffectiveDate)
INCLUDE
(EmployeeID)
WHERE
[Status] = 'Active';
CREATE NONCLUSTERED INDEX Resigned
ON #Employee
(EmployeeID, EffectiveDate)
WHERE
[Status] = 'Resigned';
Solution with comments in-line:
CREATE TABLE #Selected (EmployeeID integer NOT NULL);
DECLARE
#start date = '20110101',
#end date = '20120801';
INSERT #Selected (EmployeeID)
SELECT
E.EmployeeID
FROM #Employee AS E
WHERE
-- Employees active before the end of the range
E.[Status] = 'Active'
AND E.EffectiveDate <= #end
AND NOT EXISTS
(
SELECT *
FROM #Employee AS E2
WHERE
-- No record of the employee
-- resigning before the start of the range
-- and after the active date
E2.EmployeeID = E.EmployeeID
AND E2.[Status] = 'Resigned'
AND E2.EffectiveDate >= E.EffectiveDate
AND E2.EffectiveDate <= #start
)
OPTION (RECOMPILE);
-- Return a distinct list of employees
SELECT DISTINCT
S.EmployeeID
FROM #Selected AS S;
Execution plan:
SQLFiddle here

1. Turn your events into ranges:
ID EmployeeID Status EffectiveDate ID EmployeeID Status StartDate EndDate
-- ---------- -------- ------------- -- ---------- -------- --------- ---------
1 110545 Active 01AUG2011 1 110545 Active 01AUG2011 01JUL2012
2 110700 Active 05JAN2012 2 110700 Active 05JAN2012 31DEC9999
3 110060 Active 05JAN2012 => 3 110060 Active 05JAN2012 31DEC9999
4 110222 Active 30JUN2012 4 110222 Active 30JUN2012 31DEC9999
5 110545 Resigned 01JUL2012 5 110545 Resigned 01JUL2012 12FEB2013
6 110545 Active 12FEB2013 6 110545 Active 12FEB2013 31DEC9999
2. Get active employees based on this condition:
WHERE Status = 'Active'
AND StartDate < #EndDate
AND EndDate > #StartDate
3. Count distinct EmployeeID values.
This is how you could implement the above:
WITH ranked AS (
SELECT
*,
rn = ROW_NUMBER() OVER (PARTITION BY EmployeeID ORDER BY EffectiveDate)
FROM EmployeeActivity
),
ranges AS (
SELECT
s.EmployeeID,
s.Status,
StartDate = s.EffectiveDate,
EndDate = ISNULL(e.EffectiveDate, '31DEC9999')
FROM ranked s
LEFT JOIN ranked e ON s.EmployeeID = e.EmployeeID AND s.rn = e.rn - 1
)
SELECT
ActiveCount = COUNT(DISTINCT EmployeeID)
FROM ranges
WHERE Status = 'Active'
AND StartDate < '01JAN2013'
AND EndDate > '01AUG2012'
;
A SQL Fiddle demo for this query: http://sqlfiddle.com/#!3/c3716/3

This should work (not tested)
SELECT COUNT DISTINCT EmployeeID FROM TABLE
WHERE EffectiveDate > CONVERT(VARCHAR(11), '08-01-2012', 106) AS [DDMONYYYY]
and EffectiveDate < CONVERT(VARCHAR(11), '01-01-2013', 106) AS [DDMONYYYY]
AND Status = 'Active'

Another solution using the PIVOT operator
DECLARE #StartDate date = '20120801',
#EndDate date = '20130101'
SELECT COUNT(*)
FROM (
SELECT EffectiveDate, EmployeeID, [Status]
FROM EmployeeActivity
WHERE EffectiveDate < #EndDate
) x
PIVOT
(
MAX(EffectiveDate) FOR [Status] IN([Resigned], [Active])
) p
WHERE ISNULL(Resigned, '99991231') > #StartDate
See demo on SQLFiddle

This should work fine:
DECLARE #d1 date = '01AUG2012';
DECLARE #d2 date = '01JAN2014';
WITH CTE_Before AS
(
--Last status of each employee before period will be RN=1
SELECT *, ROW_NUMBER() OVER (PARTITION BY EmployeeID ORDER BY EffectiveDate DESC) RN
FROM dbo.Table1
WHERE EffectiveDate < #d1
)
, CTE_During AS
(
--Those who become active during period
SELECT * FROM dbo.Table1
WHERE [Status] = 'Active' AND EffectiveDate BETWEEN #d1 AND #d2
)
--Union of those who were active at the beginning of period and those who became active during period
SELECT EmployeeID FROM CTE_Before WHERE RN = 1 AND Status = 'Active'
UNION
SELECT EmployeeID FROM CTE_During
SQLFiddle DEMO

You can use this query to build a list of employees and their start/resignation dates:
select
start.*,
resignation.EffectiveDate as ResignationDate
from Employment start
outer apply (
select top 1
Id,
EmployeeId,
EffectiveDate
from Employment
where EmployeeId = start.EmployeeId
and Status = 'Resigned'
and Id > start.Id
order by Id
) resignation
where start.Status='Active'
The key here is the use of OUTER APPLY, which allows us to use a pretty "funky" join criterion.
Here's how it works: http://www.sqlfiddle.com/#!3/ec969/7
From here, it's just a matter of querying the records whose the employment interval overlaps the target interval.
There are many ways to write this, but I personally like using a CTE, because I find it a bit more readable:
;with EmploymentPeriods as (
select
start.EmployeeId,
start.EffectiveDate as StartDate,
isnull(resignation.EffectiveDate, '9999-01-01') as EndDate
from Employment start
outer apply (
select top 1
Id,
EmployeeId,
EffectiveDate
from Employment
where EmployeeId = start.EmployeeId
and Status = 'Resigned'
and Id > start.Id
order by Id
) resignation
where start.Status='Active'
)
select distinct EmployeeId
from EmploymentPeriods
where EndDate >= #QueryStartDate
and StartDate <= #QueryEndDate
SQLFiddles:
first interval: http://www.sqlfiddle.com/#!3/ec969/27
second interval: http://www.sqlfiddle.com/#!3/ec969/26

Related

Sql query to get unique date based on month

I am working on pulling some data from a table.
declare #SampleData as Table(Id int, ContactId int, Item varchar(25),CreatedOn date)
insert into #SampleData
VALUES(100,2500,'Some item name 1212', '9/5/2020'),
(104,2500,'Some item name 2232', '9/15/2020'),
(109,2500,'Some item name 3434', '9/20/2020'),
(112,3000,'Some item name 5422', '8/1/2020'),
(132,3000,'Some item name 344', '9/5/2020'),
(134,3000,'Some item name 454', '9/15/2020'),
(139,3500,'Some item name 6455', '7/5/2020'),
(146,3500,'Some item name 546', '8/5/2020'),
(142,3500,'Some item name 867', '9/5/2020'),
(149,3500,'Some item name 677', '9/15/2020'),
(150,3500,'Some item name 888', '9/19/2020')
The logic here is so that you can find new contact id each month (so logic is if same contact dont have any record in last 28 days from 1st of that month, it consider as new contact)
When you have two date periods, this is easy to do so you can exclude the records you want as below
SELECT *
FROM #SampleData
WHERE CreatedOn> = #FromDate
and CreatedOn <=#Date
and ContactId not in (SELECT ContactId
FROM #SampleData
WHERE CreatedOn >= DateAdd(Day, -28,#FromDate)
AND CreatedOn < #FromDate)
What I want is to pre-populate this data without having parameters to a some table so that user can use.
In this example data, I am expecting contact 3500 for July, 3000 for August and 2500&3000 for September.
Also it need to display only record per contact and not duplicate.
DECLARE #From date,
#To date
DECLARE date_cursor CURSOR FOR
select distinct DATEADD(month, DATEDIFF(month, 0, CreatedOn), 0) FromDate,EOMONTH(CreatedOn) ToDate
from #SampleData
OPEN date_cursor
FETCH NEXT FROM date_cursor INTO #From,#To
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT *
FROM (
SELECT DISTINCT ContactId,#From 'From Date', #To 'To Date'
FROM #SampleData D
WHERE D.CreatedOn>= #From AND D.CreatedOn <= #To
AND ContactId NOT IN (SELECT ContactId
FROM #SampleData
WHERE CreatedOn >= DateAdd(Day, -28,#From)
AND CreatedOn < #From)) ContactData
OUTER APPLY (
--pick first row for the contact as per the period
SELECT TOP 1 *
FROM #SampleData D
WHERE D.ContactId = ContactData.ContactId
AND D.CreatedOn >= ContactData.[From Date]
AND D.CreatedOn < ContactData.[To Date]
ORDER BY CreatedOn
) Records
FETCH NEXT FROM date_cursor INTO #From,#To
END
CLOSE date_cursor
DEALLOCATE date_cursor
Result
ContactId From Date To Date Id Item CreatedOn
3500 01/07/2020 31/07/2020 139 Some item name 6455 05/07/2020
3000 01/08/2020 31/08/2020 112 Some item name 5422 01/08/2020
2500 01/09/2020 30/09/2020 100 Some item name 1212 05/09/2020
3000 01/09/2020 30/09/2020 132 Some item name 344 05/09/2020
I would like to get rid of cursor, is there any possibility
You can assign a grouping to the contacts by using lag() and comparing the rows:
select sd.*,
sum(case when prev_createdon > dateadd(day, -28, createdon) then 0 else 1 end) over
(partition by contactid order by createdon) as grouping
from (select sd.*,
lag(createdon) over (partition by contactid order by createdon) as prev_createdon
from SampleData sd
) sd;
If you just want the first row in a series of adjacent records, then:
select sd.*
from (select sd.*,
lag(createdon) over (partition by contactid order by createdon) as prev_createdon
from SampleData sd
) sd
where prev_createdon < dateadd(day, -28, createdon) or prev_createdon is null;
Here is a db<>fiddle.
EDIT:
Based on the revised question, you want to summarize by group. You an do this using:
select contactid, min(createdon), max(createdon), min(id),
max(case when seqnum = 1 then item end) as item
from (select sd.*,
row_number() over (partition by contactid, grouping order by createdon) as seqnum
from (select sd.*,
sum(case when prev_createdon > dateadd(day, -28, createdon) then 0 else 1 end) over
(partition by contactid order by createdon) as grouping
from (select sd.*,
lag(createdon) over (partition by contactid order by createdon) as prev_createdon
from SampleData sd
) sd
) sd
) sd
group by contactid, grouping;
I updated the DB fiddle to have this as well.

Calculate salary by year

I want to calculate the salary of employees for each year. Let's say if an employee is in company for 1.5 years since July 2016 than I want to show the records in 2 rows like salary of 2016 and 2017 separately. If monthly salary is 20000 so for 18 months this is how result should be displayed.
EmpID Year MonthlySalary YearlySalary TotalSalary
1 2016 20000 120000 120000 (because was hired in jul)
1 2017 20000 240000 360000
2 2017 18000 216000 216000
I have a query that calculates the salary per month, years and total salary as well. I am calculating the total salary using months.
CREATE TABLE EmployeeInfo (
EmpID Int,
FirstName Varchar(25),
LastName Varchar(25),
MonthlySalary Int,
DOJ Date);
INSERT INTO EmployeeInfo VALUES (1, 'Ahmad', 'Usman', '20000', '2016-06-01');
INSERT INTO EmployeeInfo VALUES (2, 'Erick', 'Ortiz', '18000', '2017-01-01');
SELECT [EmployeeName] = [EI].[FirstName] + ' ' + [EI].[LastName],
[EI].[DOJ],
[MonthlySalary] = [EI].MonthlySalary,
[EXPERIENCE] = ( CONVERT(VARCHAR(2), Datediff(YEAR, [DOJ], Getdate())) ),
[TotalSalary] = ( ( CONVERT(VARCHAR(2), Datediff(MONTH, [DOJ], Getdate())) ) * ( Isnull(NULL, [EI].MonthlySalary) ) )
FROM [EmployeeInfo] [EI]
You need a calendar table to do this, I have used recursive cte to generate the dates.
;WITH cte
AS (SELECT EmpID, FirstName, LastName, MonthlySalary, DOJ, cntr = 1, SalaryMonth = DOJ
FROM EmployeeInfo
UNION ALL
SELECT e.EmpID, e.FirstName, e.LastName, e.MonthlySalary, e.DOJ, cntr = cntr + 1, Dateadd(month, cntr, e.DOJ)
FROM cte c
JOIN EmployeeInfo e
ON c.EmpID = e.EmpID
WHERE Dateadd(month, cntr, e.DOJ) < DATEADD(dd,-(DAY(Getdate())),Getdate()))
SELECT EmpID,
[Year] = Year(SalaryMonth),
MonthlySalary,
YearlySalary = Sum(MonthlySalary),
TotalSalary = Sum(Sum(MonthlySalary)) OVER(partition BY EmpID ORDER BY Year(SalaryMonth))
FROM cte
GROUP BY EmpID, Year(SalaryMonth), MonthlySalary
OPTION (MAXRECURSION 0)
Try Recursive CTE. Like this
;WITH CTE
AS
(
SELECT
EmpId,
EmpYr = YEAR(DOJ),
Month = 12-CASE WHEN MONTH(DOJ) = 1 THEN 0 ELSE MONTH(DOJ) END,
MonthlySalary,
YearlySalary = MonthlySalary*(12-CASE WHEN MONTH(DOJ) = 1 THEN 0 ELSE MONTH(DOJ) END),
TotalSal = MonthlySalary*(12-CASE WHEN MONTH(DOJ) = 1 THEN 0 ELSE MONTH(DOJ) END)
FROM EmployeeInfo
UNION ALL
SELECT
EmpId,
EmpYr = EmpYr+1,
Month = 12,
MonthlySalary,
YearlySalary = MonthlySalary*
(
CASE WHEN EmpYr = YEAR(GETDATE())
THEN MONTH(GETDATE())
ELSE 12 END
),
TotalSal = TotalSal+(MonthlySalary*
(
CASE WHEN EmpYr = YEAR(GETDATE())
THEN MONTH(GETDATE())
ELSE 12 END
))
FROM CTE
WHERE EmpYr < YEAR(GETDATE())
)
SELECT
EmpId,
EmpYr,
MonthlySalary,
YearlySalary,
TotalSalary = TotalSal
FROM CTE
ORDER BY EmpId
My Result
Check the DEMO
with sy as (
Select EmpId, year(DOJ) cyear, FirstName+' '+LastName AS EmployeeName,
avg(MonthlySalary) MonthlySalary, sum(MonthlySalary) YearlySalary, avg(MonthlySalary)*12 TotalSalary
FROM EmployeeInfo
group by year(DOJ),FirstName+' '+LastName, EmpID
)
select sy1.EmpId, sy1.cyear, sy1.EmployeeName, sy1.MonthlySalary MonthlySalary, sy1.YearlySalary, sum(sy2.YearlySalary) TotalSalary from
sy sy1, sy sy2
where sy1.EmpId = sy2.EmpId and sy1.EmployeeName = sy2.EmployeeName and sy1.cyear >= sy2.cyear
group by sy1.EmpId, sy1.cyear, sy1.EmployeeName, sy1.YearlySalary,sy1.MonthlySalary
(EmpId added)
You can make a simple reference table and then conduct the join.
Note: "Sum (....) OVER (....)" only works in SQL Server version 2012 or later.
WITH REF ( YY ) AS (
SELECT 2017 --OR USE THE LATEST YEAR PREFERRED.
UNION ALL
SELECT YY - 1
FROM REF
WHERE REF.YY > 1950 -- SET THE EARLIEST YEAR
)
SELECT
EI.EmpID,
REF.YY AS [Year],
EI.MonthlySalary,
CASE WHEN REF.YY = YEAR(DOJ) THEN MonthlySalary*(13-MONTH(DOJ))
ELSE MonthlySalary * 12 END AS YearlySalary,
SUM (CASE WHEN REF.YY = YEAR(DOJ) THEN MonthlySalary*(13-MONTH(DOJ))
ELSE MonthlySalary * 12 END ) OVER (PARTITION BY EMPID ORDER BY REF.YY) TotalSalary
FROM [EmployeeInfo] [EI]
JOIN
REF
ON
YEAR(DOJ) <= YY
ORDER BY
EI.EMPID, REF.YY
;

Group records only if it have intersected periods

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.

How to count open records, grouped by hour and day in SQL-server-2008-r2

I have hospital patient admission data in Microsoft SQL Server r2 that looks something like this:
PatientID, AdmitDate, DischargeDate
Jones. 1-jan-13 01:37. 1-jan-13 17:45
Smith 1-jan-13 02:12. 2-jan-13 02:14
Brooks. 4-jan-13 13:54. 5-jan-13 06:14
I would like count the number of patients in the hospital day by day and hour by hour (ie at
1-jan-13 00:00. 0
1-jan-13 01:00. 0
1-jan-13 02:00. 1
1-jan-13 03:00. 2
And I need to include the hours when there are no patients admitted in the result.
I can't create tables so making a reference table listing all the hours and days is out, though.
Any suggestions?
To solve this problem, you need a list of date-hours. The following gets this from the admit date cross joined to a table with 24 hours. The table of 24 hours is calculating from information_schema.columns -- a trick for getting small sequences of numbers in SQL Server.
The rest is just a join between this table and the hours. This version counts the patients at the hour, so someone admitted and discharged in the same hour, for instance is not counted. And in general someone is not counted until the next hour after they are admitted:
with dh as (
select DATEADD(hour, seqnum - 1, thedatehour ) as DateHour
from (select distinct cast(cast(AdmitDate as DATE) as datetime) as thedatehour
from Admission a
) a cross join
(select ROW_NUMBER() over (order by (select NULL)) as seqnum
from INFORMATION_SCHEMA.COLUMNS
) hours
where hours <= 24
)
select dh.DateHour, COUNT(*) as NumPatients
from dh join
Admissions a
on dh.DateHour between a.AdmitDate and a.DischargeDate
group by dh.DateHour
order by 1
This also assumes that there are admissions on every day. That seems like a reasonable assumption. If not, a calendar table would be a big help.
Here is one (ugly) way:
;WITH DayHours AS
(
SELECT 0 DayHour
UNION ALL
SELECT DayHour+1
FROM DayHours
WHERE DayHour+1 <= 23
)
SELECT B.AdmitDate, A.DayHour, COUNT(DISTINCT PatientID) Patients
FROM DayHours A
CROSS JOIN (SELECT DISTINCT CONVERT(DATE,AdmitDate) AdmitDate
FROM YourTable) B
LEFT JOIN YourTable C
ON B.AdmitDate = CONVERT(DATE,C.AdmitDate)
AND A.DayHour = DATEPART(HOUR,C.AdmitDate)
GROUP BY B.AdmitDate, A.DayHour
This is a bit messy and includes a temp table with the test data you provided but
CREATE TABLE #HospitalPatientData (PatientId NVARCHAR(MAX), AdmitDate DATETIME, DischargeDate DATETIME)
INSERT INTO #HospitalPatientData
SELECT 'Jones.', '1-jan-13 01:37:00.000', '1-jan-13 17:45:00.000' UNION
SELECT 'Smith', '1-jan-13 02:12:00.000', '2-jan-13 02:14:00.000' UNION
SELECT 'Brooks.', '4-jan-13 13:54:00.000', '5-jan-13 06:14:00.000'
;WITH DayHours AS
(
SELECT 0 DayHour
UNION ALL
SELECT DayHour+1
FROM DayHours
WHERE DayHour+1 <= 23
),
HospitalPatientData AS
(
SELECT CONVERT(nvarchar(max),AdmitDate,103) as AdmitDate ,DATEPART(hour,(AdmitDate)) as AdmitHour, COUNT(PatientID) as CountOfPatients
FROM #HospitalPatientData
GROUP BY CONVERT(nvarchar(max),AdmitDate,103), DATEPART(hour,(AdmitDate))
),
Results AS
(
SELECT MAX(h.AdmitDate) as Date, d.DayHour
FROM HospitalPatientData h
INNER JOIN DayHours d ON d.DayHour=d.DayHour
GROUP BY AdmitDate, CountOfPatients, DayHour
)
SELECT r.*, COUNT(h.PatientId) as CountOfPatients
FROM Results r
LEFT JOIN #HospitalPatientData h ON CONVERT(nvarchar(max),AdmitDate,103)=r.Date AND DATEPART(HOUR,h.AdmitDate)=r.DayHour
GROUP BY r.Date, r.DayHour
ORDER BY r.Date, r.DayHour
DROP TABLE #HospitalPatientData
This may get you started:
BEGIN TRAN
DECLARE #pt TABLE
(
PatientID VARCHAR(10)
, AdmitDate DATETIME
, DischargeDate DATETIME
)
INSERT INTO #pt
( PatientID, AdmitDate, DischargeDate )
VALUES ( 'Jones', '1-jan-13 01:37', '1-jan-13 17:45' ),
( 'Smith', '1-jan-13 02:12', '2-jan-13 02:14' )
, ( 'Brooks', '4-jan-13 13:54', '5-jan-13 06:14' )
DECLARE #StartDate DATETIME = '20130101'
, #FutureDays INT = 7
;
WITH dy
AS ( SELECT TOP (#FutureDays)
ROW_NUMBER() OVER ( ORDER BY name ) dy
FROM sys.columns c
) ,
hr
AS ( SELECT TOP 24
ROW_NUMBER() OVER ( ORDER BY name ) hr
FROM sys.columns c
)
SELECT refDate, COUNT(p.PatientID) AS PtCount
FROM ( SELECT DATEADD(HOUR, hr.hr - 1,
DATEADD(DAY, dy.dy - 1, #StartDate)) AS refDate
FROM dy
CROSS JOIN hr
) ref
LEFT JOIN #pt p ON ref.refDate BETWEEN p.AdmitDate AND p.DischargeDate
GROUP BY refDate
ORDER BY refDate
ROLLBACK

Summing historic cost rates over booked time (single effective date)

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.