Get UserID from RosterResource Table - sql

I have 5 tables: Location, Charge, Resource, Roster and SpecialRoster.
CREATE TABLE Location(
LocationID int identity,
StartDate datetime,
DaysInRoster int)
CREATE TABLE Charge(
ChargeID int identity,
TotalAmount money,
ChargeDate datetime,
UserID int)
CREATE TABLE [Resource](
ResourceID int identity,
ResourceName varchar(100))
CREATE TABLE Roster(
RosterDay int,
ResourceID int,
StartDate datetime,
EndDate datetime,
UserID int)
CREATE TABLE SpecialRoster(
RosterDate datetime,
ResourceID int,
UserID int)
I need to construct a report which shows the sum of Charge.TotalAmount on different days, by ResourceID.
Charge only has a UserID, but the rules are fairly simple:
If the date and ResourceID is in SpecialRoster, the UserID is taken from SpecialRoster
If not, the UserID for the Resource and the RosterDay for the date is taken from the Roster current at that time
Basically, I need to map ResourceID, through UserID from Roster or SpecialRoster, to Charge.
The RosterDay is the current day of the roster, and can be calculated from location, where the RosterDay = DateDiff(day, Location.RosterStartDate, GetDate()) % Location.DaysInRoster
that is, the modulus of the difference between the start of the entire roster at that location, with the number of days in a roster period.
I have written a Function to do this:
CREATE FUNCTION dbo.GetRosteredUser
(
#ResourceID int,
#Date datetime,
#LocationID int
)
RETURNS int
AS
BEGIN
DECLARE #UserID int
DECLARE #RosterOffset int
Select #UserID = UserID from SpecialRoster Where ResourceID = #ResourceID and RosterDate = #Date
If #UserID is NOT NULL
return #UserID
else
Select #RosterOffset = DATEDIFF(day, StartDate, #Date) % DaysInRoster
from Location Where LocationID = #LocationID
Select #UserID = UserID from Roster Where ResourceID = #ResourceID and RosterDay = #RosterOffset
and ( #Date between StartDate and EndDate or #Date > StartDate and EndDate is NULL)
return #UserID
END
GO
But the function isn't great as it is slow, and only allows the report to be run for a single day at a time:
Select a.ResourceID, Name, a.UserID, Sum(c.TotalAmount ) as AmountCharged
from Charge c
left outer join (
Select ResourceID, Name,
dbo.GetUserByResourceDate(ResourceID, '1/FEB/2013', 1) as UserID
from [Resource]) a
on c.UserID = a.UserID
Where ChargeDate between '1/FEB/2013' and '2/FEB/2013'
group by a.ResourceID, Name, a.UserID
Is there a better way to do this? A User will want to run this to compare productivity of a resource over a period of time. eg:
Date ResourceID Name UserID TotalAmount
01/FEB/2013 1 Sales1 22 $1024
02/FEB/2013 1 Sales1 11 $1454
03/FEB/2013 1 Sales1 14 $1900
04/FEB/2013 1 Sales1 23 $3045

Perhaps, something like this might be of help:
SELECT
re.ResourceID,
re.ResourceName,
ch.UserID,
AmountCharged = SUM(ch.TotalAmount)
FROM
Location lo
CROSS JOIN Charge ch
CROSS APPLY (
SELECT DATEDIFF(DAY, lo.StartDate, ch.ChargeDate) % lo.DaysInRoster
) x (RosterDay)
INNER JOIN
[Resource] re
LEFT JOIN Roster ro
ON
ro.RosterDay = x.RosterDay
AND ch.ChargeDate BETWEEN ro.StartDate
AND ro.RecourceID = re.ResourceID
LEFT JOIN SpecialRoster sr
ON
sr.RosterDate = ch.ChargeDate
AND sr.RecourceID = re.ResourceID
ON
ch.UserID = ISNULL(sr.UserID, ro.UserID)
GROUP BY
re.ResourceID,
re.ResourceName,
ch.UserID
WHERE
lo.LocationID = #LocationID
AND ch.ChargeDate BETWEEN ...
;
In the above script, it is assumed that all datetime values are in fact only dates.
Also, I was somewhat surprised not to found a connection between Location and any of the other tables specified. If there is one and you simply forgot to mention it, please let me know if you need any help with incorporating the necessary condition(s) into the script.

Related

Get total working hours from SQL table

I have an attendance SQL table that stores the start and end day's punch of employee. Each punch (punch in and punch out) is in a separate record.
I want to calculate the total working hour of each employee for a requested month.
I tried to make a scalar function that takes two dates and employee ID and return the calculation of the above task, but it calculate only the difference of one date between all dates.
The data is like this:
000781 2015-08-14 08:37:00 AM EMPIN 539309898
000781 2015-08-14 08:09:48 PM EMPOUT 539309886
My code is:
#FromDate NVARCHAR(10)
,#ToDate NVARCHAR(10)
,#EmpID NVARCHAR(6)
CONVERT(NVARCHAR,DATEDIFF(HOUR
,(SELECT Time from PERS_Attendance att where attt.date between convert(date,#fromDate) AND CONVERT(Date,#toDate)
AND (EmpID= #EmpID OR ISNULL(#EmpID, '') = '') AND Funckey = 'EMPIN')
,(SELECT Time from PERS_Attendance att where attt.date between convert(date,#fromDate) AND CONVERT(Date,#toDate)
AND (EmpID= #EmpID OR ISNULL(#EmpID, '') = '') AND Funckey = 'EMPOUT') ))
FROM PERS_Attendance attt
One more approach that I think is simple and efficient.
It doesn't require modern functions like LEAD
it works correctly if the same person goes in and out several times during the same day
it works correctly if the person stays in over the midnight or even for several days in a row
it works correctly if the period when person is "in" overlaps the start OR end date-time.
it does assume that data is correct, i.e. each "in" is matched by "out", except possibly the last one.
Here is an illustration of a time-line. Note that start time happens when a person was "in" and end time also happens when a person was still "in":
All we need to do it calculate a plain sum of time differences between each event (both in and out) and start time, then do the same for end time. If event is in, the added duration should have a positive sign, if event is out, the added duration should have a negative sign. The final result is a difference between sum for end time and sum for start time.
summing for start:
|---| +
|----------| -
|-----------------| +
|--------------------------| -
|-------------------------------| +
--|====|--------|======|------|===|=====|---|==|---|===|====|----|=====|--- time
in out in out in start out in out in end out in out
summing for end:
|---| +
|-------| -
|----------| +
|--------------| -
|------------------------| +
|-------------------------------| -
|--------------------------------------| +
|-----------------------------------------------| -
|----------------------------------------------------| +
I would recommend to calculate durations in minutes and then divide result by 60 to get hours, but it really depends on your requirements. By the way, it is a bad idea to store dates as NVARCHAR.
DECLARE #StartDate datetime = '2015-08-01 00:00:00';
DECLARE #EndDate datetime = '2015-09-01 00:00:00';
DECLARE #EmpID nvarchar(6) = NULL;
WITH
CTE_Start
AS
(
SELECT
EmpID
,SUM(DATEDIFF(minute, (CAST(att.[date] AS datetime) + att.[Time]), #StartDate)
* CASE WHEN Funckey = 'EMPIN' THEN +1 ELSE -1 END) AS SumStart
FROM
PERS_Attendance AS att
WHERE
(EmpID = #EmpID OR #EmpID IS NULL)
AND att.[date] < #StartDate
GROUP BY EmpID
)
,CTE_End
AS
(
SELECT
EmpID
,SUM(DATEDIFF(minute, (CAST(att.[date] AS datetime) + att.[Time]), #StartDate)
* CASE WHEN Funckey = 'EMPIN' THEN +1 ELSE -1 END) AS SumEnd
FROM
PERS_Attendance AS att
WHERE
(EmpID = #EmpID OR #EmpID IS NULL)
AND att.[date] < #EndDate
GROUP BY EmpID
)
SELECT
CTE_End.EmpID
,(SumEnd - ISNULL(SumStart, 0)) / 60.0 AS SumHours
FROM
CTE_End
LEFT JOIN CTE_Start ON CTE_Start.EmpID = CTE_End.EmpID
OPTION(RECOMPILE);
There is LEFT JOIN between sums for end and start times, because there can be EmpID that has no records before the start time.
OPTION(RECOMPILE) is useful when you use Dynamic Search Conditions in T‑SQL. If #EmpID is NULL, you'll get results for all people, if it is not NULL, you'll get result just for one person.
If you need just one number (a grand total) for all people, then wrap the calculation in the last SELECT into SUM(). If you always want a grand total for all people, then remove #EmpID parameter altogether.
It would be a good idea to have an index on (EmpID,date).
My approach would be as follows:
CREATE FUNCTION [dbo].[MonthlyHoursByEmpID]
(
#StartDate Date,
#EndDate Date,
#Employee NVARCHAR(6)
)
RETURNS FLOAT
AS
BEGIN
DECLARE #TotalHours FLOAT
DECLARE #In TABLE ([Date] Date, [Time] Time)
DECLARE #Out TABLE ([Date] Date, [Time] Time)
INSERT INTO #In([Date], [Time])
SELECT [Date], [Time]
FROM PERS_Attendance
WHERE [EmpID] = #Employee AND [Funckey] = 'EMPIN' AND ([Date] > #StartDate AND [Date] < #EndDate)
INSERT INTO #Out([Date], [Time])
SELECT [Date], [Time]
FROM PERS_Attendance
WHERE [EmpID] = #Employee AND [Funckey] = 'EMPOUT' AND ([Date] > #StartDate AND [Date] < #EndDate)
SET #TotalHours = (SELECT SUM(CONVERT([float],datediff(minute,I.[Time], O.[Time]))/(60))
FROM #in I
INNER JOIN #Out O
ON I.[Date] = O.[Date])
RETURN #TotalHours
END
Assuming the entries are properly paired (in -> out -> in -> out -> in etc).
SQL Server 2012 and later:
DECLARE #Year int = 2015
DECLARE #Month int = 8
;WITH
cte AS (
SELECT EmpID,
InDate = LAG([Date], 1) OVER (PARTITION BY EmpID ORDER BY [Date]),
OutDate = [Date],
HoursWorked = DATEDIFF(hour, LAG([Date], 1) OVER (PARTITION BY EmpID ORDER BY [Date]), [Date]),
Funckey
FROM PERS_Attendance
)
SELECT EmpID,
TotalHours = SUM(HoursWorked)
FROM cte
WHERE Funckey = 'EMPOUT'
AND YEAR(InDate) = #Year
AND MONTH(InDate) = #Month
GROUP BY EmpID
SQL Server 2005 and later:
;WITH
cte1 AS (
SELECT *,
rn = ROW_NUMBER() OVER (PARTITION BY EmpID ORDER BY [Date])
FROM PERS_Attendance
),
cte2 AS (
SELECT a.EmpID, b.[Date] As InDate, a.[Date] AS OutDate,
HoursWorked = DATEDIFF(hour, b.[Date], a.[Date])
FROM cte1 a
LEFT JOIN cte1 b ON a.EmpID = b.EmpID and a.rn = b.rn + 1
WHERE a.Funckey = 'EMPOUT'
)
SELECT EmpID,
TotalHours = SUM(HoursWorked)
FROM cte2
WHERE YEAR(InDate) = #Year
AND MONTH(InDate) = #Month
GROUP BY EmpID

Number of entries per month

I have something similar to the following situation, probably not the best example but somewhat similar to what I really have. Let's say I have 4 tables that due to certain circumstances I can't change.
CREATE TABLE [Hospital] (
[HospitalID] INT IDENTITY NOT NULL,
[HospitalName] VARCHAR(MAX) NOT NULL
);
CREATE TABLE [Doctors] (
[DoctorID] INT IDENTITY NOT NULL ,
[HospitalID] INT NOT NULL,
[DoctorName] VARCHAR(MAX) NOT NULL
);
CREATE TABLE [Patient] (
[PatientID] INT IDENTITY NOT NULL ,
[DoctorID] INT NOT NULL
);
CREATE TABLE [PatientAppointment] (
[PatientID] INT IDENTITY NOT NULL,
[Date] DATETIME NOT NULL
);
I want to write a stored procedure that get's a year and a month as parameters and it is supposed to return [HospitalName] , [DoctorsName] and the number of patient appointments for that time period.
This is what I have now and I am stuck
CREATE PROCEDURE [dbo].[Procedure]
#year INT ,
#month INT
AS
SELECT COUNT([Date]) AS NumberOfAppointments FROM [PatientAppointment]
WHERE MONTH([Date]) = #month AND YEAR([Date]) = #year
SELECT [Hospital].HospitalName , [Doctors].DoctorName FROM [Doctors]
INNER JOIN [Hospital] ON [Hospital].HospitalID = [Doctors].DoctorID
RETURN 0
I can't figure out how to extract the info I need and I am limited to one stored procedure.
try this:
CREATE PROCEDURE [dbo].[Procedure]
#year INT ,
#month INT
AS
BEGIN
SELECT h.HospitalId, d.DoctorName, count(p.PatientId) as NumberOfAppointments
FROM Hospital h
INNER JOIN Doctors d
ON h.HospitalId = d.HospitalId
INNER JOIN Patient p
ON d.DoctorId = p.DoctorId
INNER JOIN PatientAppointment ap
ON p.PatientId = ap.PatientId
AND MONTH(ap.[Date]) = #month
AND YEAR(ap.[Date]) = #year
GROUP BY h.HospitalId, d.DoctorName
END
My answer is very similar to the one posted by Joachim but there is one VERY significant difference. This code is SARGable where the original query and the fine example already posted are not. Since you say you can't change the table structure I will refrain from suggesting changes....although you could greatly improve on these structures.
Create Procedure dbo.SomeBetterName
(
#Date date
) AS
select h.HospitalName
, d.DoctorName
, COUNT(pa.[Date]) as NumAppointments
from Hospital h
join Doctors d on h.HospitalID = d.HospitalID
join Patient p on p.DoctorID = d.DoctorID
join PatientAppointment pa on pa.PatientID = p.PatientID
where pa.Date >= dateadd(month, datediff(month, 0, #Date), 0)
and pa.Date < dateadd(month, datediff(month, 0, #Date) + 1, 0)
group by h.HospitalName
, d.DoctorName

adding a column to a table with different parameters

I am trying to enter 2 tables into one output my problem is that the parameter is pulling from 2 different columns. I need to be able to show what was completed and the work ordered for a months period. I do not get any errors as the script is written but after 3 hours I stopped it from running.
If I change the join on the #Ordered to iAuditID from EffectiveDate it takes a few seconds to run, but it does not pull the correct information then. I only need the count of ordered audits to be correct and in the #data table and correct. Work may have been ordered a few months before it was completed, therefore I need to be able see how much work is coming in each month and what is being completed.
I am using SQL 2008.
DECLARE
#StartDate datetime,
#EndDate datetime,
#iClientID int,
#iServiceLevelID int
SET #StartDate = '1-1-13'
SET #EndDate = '12-30-13'
SET DATEFIRST 7
DECLARE #TB1 TABLE (EffectiveDate datetime,
iAuditID int,
iClientID int,
iServiceLevelID int)
INSERT INTO #TB1
SELECT dateadd(month, datediff(month, 0, a.dtClosedDate),0) AS EffectiveDate,
a.iAuditID, a.iClientID, a.iServiceLevelID
FROM vwtblAudit a
WHERE (a.dtClosedDate >= #StartDate)
AND (a.dtClosedDate <= #EndDate)
AND (a.iClientID = #iClientID OR #iClientID is null)
AND (a.iServiceLevelID = #iServiceLevelID or #iServiceLevelID is null)
GROUP BY dateadd(month, datediff(month, 0, a.dtClosedDate),0),
a.iClientID, a.iAuditID, a.iServiceLevelID
DECLARE #Ordered TABLE (EffectiveDate datetime,
iAuditID int,
iClientID int,
iServiceLevelID int,
dtOpenDate int)
INSERT INTO #Ordered
SELECT dateadd(month, datediff(month, 0, a.dtOpenDate),0) AS EffectiveDate,
a.iAuditID, a.iClientID, a.iServiceLevelID,
Count(a.dtOpenDate) as Ordered
FROM tblAudit a
WHERE (a.dtOpenDate >= #StartDate)
AND (a.dtOpenDate <= #EndDate)
AND (a.iClientID = #iClientID OR #iClientID is null)
AND (a.iServiceLevelID = #iServiceLevelID or #iServiceLevelID is null)
GROUP BY dateadd(month, datediff(month, 0, a.dtOpenDate),0),
a.iClientID, a.iServiceLevelID, a.iAuditID
order by iClientID, EffectiveDate
DECLARE #DATA table(iclientID int,
sClientCode varchar(8),
sClientName varchar(50),
iServiceLevelID int,
sServiceLevelName varchar(50),
EffectiveDate datetime,
iAuditCount int,
Completed int)
SELECT tblClient.iclientID, tblClient.sClientCode, tblClient.sClientName,
tblServiceLevel.iServiceLevelID,
TB1.EffectiveDate, COUNT(*) AS iAuditCount,
COUNT(Ordered.dtOpenDate) as Ordered
FROM #TB1 TB1 INNER JOIN
tblClient ON TB1.iClientID = tblClient.iClientID INNER JOIN
tblServiceLevel ON TB1.iServiceLevelID = tblServiceLevel.iServiceLevelID
join #Ordered Ordered on TB1.EffectiveDate=Ordered.EffectiveDate
GROUP BY tblClient.iClientID, TB1.iServiceLevelID, tblclient.sClientCode,
tblClient.sClientName, tblServiceLevel.iServiceLevelID,
tblServiceLevel.sServiceLevelName, TB1.EffectiveDate
return
SET DATEFIRST 7
Every a from the #data worked like it should, but I was ask to add the work coming in, so I added #ordered table to bring in the correct information. The data is still correct except for the ordered column. See sample below:
clientID ClientCode ClientName iServiceLevelID EffectiveDate iAuditCount Ordered
0001 1001 Sample 1 Jan 2014 104 19(this number should be 134)

SQL Server Stored Procedure Issue. Get Multiple result From Database

Here is my stored procedure to get a list of employees and their hours worked between StartDate and EndDate.
ALTER procedure [dbo].[usp_get_employeehourlyreport]
#startdate datetime,
#enddate datetime
as
begin
select *
from (
select
(select FullName from Registration
where Registration.UserId = UserTime.UserId) Fullname,
ISNULL(SUM(HoursWorked), 0) HoursWorked
from UserTime
where CheckIn between #startdate and #enddate
group by UserId, CheckIn) tbl
end
The result I am getting is :
Fullname HoursWorked
Antonio Jake 0
Antonio Jake 0.016666
So basically it is displaying one row extra with hours 0. I Do not have any duplicate FullName in my database.
Table structures are:
USERTIME:
UserTimeId int Unchecked
UserId int Checked
CheckIn datetime Checked
LoginStatus nvarchar(50) Checked
Day varchar(50) Checked
HoursWorked float Checked
Date nvarchar(50) Checked
REGISTRATION:
UserId int Unchecked
FullName varchar(50) Checked
Try something like this....
ALTER procedure [dbo].[usp_get_employeehourlyreport]
#startdate datetime,
#enddate datetime
AS
BEGIN
SET NOCOUNT ON;
SELECT R.FullName, ISNULL(SUM(UT.HoursWorked),0) AS Total_Hours
FROM Registration R LEFT JOIN USERTIME UT
ON R.UserId = UT.UserId Fullname ,ISNULL(SUM(HoursWorked),0) HoursWorked from UserTime
AND UT.CheckIn >= #startdate
AND UT.CheckIn <= #enddate
GROUP BY R.FullName
END

Best way to pairing & finding anomalies in SQL data

The problem is that it takes way to long in SQL and there must be a better way. I’ve picked out the slow part for the scenario bellow.
Scenario:
Two (temp) tables with event times for start and end for vehicles that have to be paired up to figure idle durations. The issue is that some of the event data is missing. I figured out a rudimentary way of going through and determining when the last end time is after the next start time and removing the invalid start. Again not elegant + very slow.
Tables :
create table #start(VehicleIp int null, CurrentDate datetime null,
EventId int null,
StartId int null)
create table #end(VehicleIp int null,
CurrentDate datetime null,
EventId int null,
EndId int null)
--//Note: StartId and EndId are both pre-filled with something like:
ROW_NUMBER() Over(Partition by VehicleIp order by VehicleIp, CurrentDate)
--//Slow SQL
while exists(
select top 1 tOn.EventId
from #start as tOn
left JOIN #end tOff
on tOn.VehicleIp = tOff.VehicleIp and
tOn.StartID = tOff.EndID +1
)
begin
declare #badEntry int
select top 1 #badEntry = tOn.EventId
from #s as tOn
left JOIN #se tOff
on tOn.VehicleIp = tOff.VehicleIp and
tOn.StartID = tOff.EndID +1
order by tOn.CurrentDate
delete from #s where EventId = #badEntry
;with _s as ( select VehicleIp, CurrentDate, EventId,
ROW_NUMBER() Over(Partition by VehicleIp
order by VehicleIp, CurrentDate) StartID
from #start)
update #start
set StartId = _s.StartId
from #s join _s on #s.EventId = _s.EventId
end
Assuming you start with a table containing Vehicle and interval in which it was used, this query will identify gaps.
select b.VehicleID, b.IdleStart, b.IdleEnd
from
(
select VehicleID,
-- If EndDate is not inclusive, remove +1
EndDate + 1 IdleStart,
-- First date after current for this vehicle
-- If you don't want to show unused vehicles to current date remove isnull part
isnull((select top 1 StartDate
from TableA a
where a.VehicleID = b.VehicleID
and a.StartDate > b.StartDate
order by StartDate
), getdate()) IdleEnd
from TableA b
) b
where b.IdleStart < b.IdleEnd
If dates have time portion they should be truncated to required precision, here is for day:
dateadd(dd, datediff(dd,0, getDate()), 0)
Replace dd with hh, mm or whatever precision is needed.
And here is Sql Fiddle with test