Consecutive streak of dates - sql

Hopefully this isn't a dupe of another question, but I couldn't see it anywhere else - also this is a simplified version of another question I asked, hopefully to get me started on working out how to approach it.
I am looking to work out consecutive ranges of payments where there has been at least one payment in each month.
I have the following sample data
CREATE TABLE #data
(
Contact_reference NVARCHAR(55)
,Date_payment DATETIME
,Payment_value MONEY
)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2003-06-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2004-06-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2004-12-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-04-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-05-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-06-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-07-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-08-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-09-08',12.82)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-10-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-11-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2005-12-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-01-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-02-10',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-02-28',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-04-12',12.8205)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2006-05-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-06-11',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-07-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-08-09',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-09-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-10-09',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-11-09',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2007-12-10',19.2308)
INSERT INTO #data VALUES ('18EC3CD2-3065-4FF4-BE40-000004228590','2008-01-10',19.2308)
And what I would like to be able to do is to work out for each contact the ranges over which they gave consecutively (defined as giving at least once in every calendar month), the number of consecutive payments, the total value per range (and ideally if possible the gap between the current range and the end of the most recent one).
For the test data above my output would look like this:
CREATE TABLE #results
(
contact_reference NVARCHAR(55)
,Range_start DATETIME
,Range_end DATETIME
,Payments INT
,Value MONEY
,months_until_next_payment INT --works out the gap between the range_end date for a group and the range_start date for the next group
)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2003-06-08','2003-06-08',1,12.82,12)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2004-06-08','2004-06-08',1,12.82,6)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2004-12-08','2004-12-08',1,12.82,4)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2005-04-08','2006-02-28',12,153.843,2)
INSERT INTO #results VALUES('18EC3CD2-3065-4FF4-BE40-000004228590','2006-04-12','2008-06-06',27,416.6673,NULL)
I've looked for answers using islands, or iterations but I quite frankly don't even know where to begin applying them to my question, so any help massively appreciated :)

Edit: I've added in the months_until_next_payment column. This would be more efficiently done in the application rather than with a self join however as SQL Server does not have any particularly satisfactory way of referencing next and previous rows.
;WITH base AS (
SELECT Contact_reference ,
Payment_value,
DATEPART(YEAR, Date_payment)*12 + DATEPART(MONTH, Date_payment) -
DENSE_RANK() OVER
(PARTITION BY Contact_reference
ORDER BY DATEPART(YEAR, Date_payment)*12 + DATEPART(MONTH, Date_payment)) AS G,
Date_payment
FROM #data
),
cte AS
(
SELECT
Contact_reference,
ROW_NUMBER() over (partition by Contact_reference
order by MIN(Date_payment)) RN,
MIN(Date_payment) Range_start,
MAX(Date_payment) Range_end,
COUNT(Payment_value) Payments,
SUM(Payment_value) Value
FROM base
GROUP BY Contact_reference, G
)
SELECT
c1.Contact_reference,
c1.Payments,
c1.Range_end,
c1.Range_start,
c1.Value,
DATEDIFF(month, c1.Range_end,c2.Range_start) months_until_next_payment
FROM cte c1
LEFT join cte c2 ON c1.Contact_reference=c2.Contact_reference and c2.RN = c1.RN+1

You can do it using cursor. Language like c#/java are better choice for this problem.
DECLARE #date DATETIME
DECLARE #nextDate DATETIME
DECLARE #rangeStart DATETIME
DECLARE #rangeEnd DATETIME
DECLARE #value decimal(18,2)
DECLARE #valueSum decimal(18,2)
DECLARE #count int
DECLARE #PaymentCursor CURSOR
SET #PaymentCursor = CURSOR FOR
SELECT Date_payment, Payment_value FROM #data
ORDER BY Date_payment
OPEN #PaymentCursor
FETCH NEXT FROM #PaymentCursor INTO #nextDate, #value
SET #date = #nextDate
SET #rangeStart = #nextDate
SET #valueSum = 0
SET #count = 0
WHILE (##FETCH_STATUS = 0)
BEGIN
FETCH NEXT FROM #PaymentCursor INTO #nextDate, #value
SET #count = #count + 1
SET #valueSum = #valueSum + #value
IF (DATEDIFF(mm, #date, #nextDate) > 1)
BEGIN
SELECT #rangeStart AS RangeStart, #date AS RangeEnd, #count AS Coount, #valueSum AS VALUE, DATEDIFF(mm, #date, #nextDate) AS months_until_next_payment
SET #valueSum = 0
SET #count = 0
SET #rangeStart = #nextDate
END
SET #date = #nextDate
END
SELECT #rangeStart AS RangeStart, #date AS RangeEnd, #count AS Coount, #valueSum AS VALUE, null AS months_until_next_payment
CLOSE #PaymentCursor
DEALLOCATE #PaymentCursor

Related

How to update a table using while loops and waitfor delay to insert current date with a one second delay between records?

I'd like to create a table that contains two columns (id int, today datetime) and, using while loops, to insert the current date every 1 second. However, the resulting table shows the same time for all rows. Below is my code. Can anyone help me understand what I'm doing wrong, please? Thank you!
declare #mytable table (id int, today datetime)
declare #id int=1
declare #today datetime=getdate()
while #id<10
begin
waitfor delay '00:00:01'
insert into #mytable values (#id,#today)
set #id=#id+1
end
The reason every row has the same value is because you aren't setting the value of #Today anywhere apart from before your WHILE loop. GETDATE() returns a scalar value, and setting a variable to that value means it will be set the value that GETDATE() returned at the time the SET was run. The value of the variable won't change after time has passed. For example:
DECLARE #d datetime;
SET #d = GETDATE();
SELECT #d, GETDATE(); --Will return very similar values
WAITFOR DELAY '00:00:05';
SELECT #d, GETDATE(); --#d will have the same value as before, as its value is static, but GETDATE()'s value will have changed.
To do what you're after, I don't see any need for the variable for #Today, this would work fine:
DECLARE #mytable table (id int,
today datetime);
DECLARE #id int = 1;
WHILE #id < 10
BEGIN
WAITFOR DELAY '00:00:01';
INSERT INTO #mytable
VALUES (#id, GETDATE());
SET #id = #id + 1;
END;
However a loop is a bad choice anyway, as an RDBMS excels at set based operations, not iterative. You would be far better to achieve what you're after by doing:
DECLARE #mytable table (id int,
today datetime);
DECLARE #id int = 1;
WITH N AS (
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1
CROSS JOIN N N2 --Not actually eneded here, but shows how to increase row count
)
INSERT INTO #mytable (id,
today)
SELECT TOP 10
T.I + #ID,
DATEADD(SECOND, T.I, GETDATE())
FROM Tally T
ORDER BY T.I;
This builds an inline tally table, and then inserts a value for a row for 10 ID, and adds 1 second to each incremented ID.

How to turn potential loop query to set based?

I have something like this currently -
declare #thisDate smalldatetime
create table #theseDates (thisDate smalldatetime)
insert into #theseDates(thisDate)
values ('11/1/2016', '5/1/2017', '9/30/2017)
create table #someData (productid int, productname varchar(32), productprice float, productsolddate smalldatetime)
declare date_cursor cursor for
select thisDate from #theseDates
open date_cursor
fetch next for date_cursor into #thisDate
while ##fetch_status = 0
begin
insert into #someData(soldid, productid, productname, productprice, productsolddate)
select soldid, productid, productname, productprice, productsolddate
from soldproducts
where productsolddate between #thisDate and getdate()
fetch next from date_cursor into #thisDate
end
close date_cursor
deallocate date_cursor
How do I change this from a cursor to a set-based solution?
NOTE: added fetch line
You don't need a cursor to solve this OP. Use CROSS Join instead
This way faster than Cursor.
See below.
FYI. the mydates table is just a random date example because I don't want type them
declare #myproducts table (prodid int, proddesc varchar(30), prodprice money, solddate date)
insert into #myproducts
values
(1,'prod 1',1,dateadd(day,-1,getdate())),
(2,'prod 2',10,dateadd(day,-10,getdate())),
(3,'prod 3',100,dateadd(day,-5,getdate())),
(1,'prod 1',1.5,dateadd(day,-6,getdate())),
(2,'prod 2',8.99,dateadd(day,-20,getdate())),
(3,'prod 3',95,dateadd(day,-15,getdate())),
(1,'prod 1',0.89,dateadd(day,-2,getdate()))
select * from #myproducts
declare #mydates table (mydate date)
/* reporting datefrom */
insert into #mydates
values (dateadd(day,-11,getdate())),(dateadd(day,-20,getdate())),(dateadd(day,-3,getdate()))
select * from #mydates
/* apply cross join to distribute the datefrom but should only be to data where solddate is between datefrom and today */
-- insert statement to dump all qualified data or not
select p.*,d.mydate [DateFrom] from #myproducts p
cross join #mydates d
where solddate between mydate and cast(getdate() as date)

How to make this code more professional look?

DECLARE #Coid INT
DECLARE #DTRID INT
DECLARE #EMPID INT
DECLARE #DATE datetime
SELECT TOP 1 #EMPID = tblEmployees.Id, #Coid = tblEmployees.CompanyId, #DATE = tblDailyTimeRecord.TimeIn, #DTRID = tblDailyTimeRecord.Id FROM tblEmployees INNER JOIN tblDailyTimeRecord ON tblEmployees.Id = tblDailyTimeRecord.EmployeeId WHERE tblDailyTimeRecord.Date = CONVERT(date, GETDATE()) AND tblDailyTimeRecord.TimeOut IS NULL ORDER BY tblDailyTimeRecord.ID DESC
IF #Coid IS NULL
BEGIN
DECLARE #IdentityValue TABLE ( ContactID int,EmpId int,DATE datetime)
INSERT INTO [tblDailyTimeRecord]([EmployeeId],[Date],[TimeOut],[IsModified],[CompanyId])
OUTPUT INSERTED.Id,INSERTED.EmployeeId,INSERTED.TimeOut INTO #IdentityValue
Select Id,CONVERT(date, GETDATE()),GETDATE(),0,CompanyId From tblEmployees Where AccessCode = 'GI0056'
INSERT INTO tblTimeLog([EmployeeId],[Time],[Type],[TimeLogSourceId],[CreationDate])
Select EmpId,DATE,2,11,GETDATE() From #IdentityValue
END
ELSE
BEGIN
DECLARE #PAIR int
DECLARE #TIMEOUT datetime
SET #TIMEOUT = GETDATE()
IF #DATE IS NULL
SET #PAIR = 0
ELSE
SET #PAIR = 1
UPDATE [tblDailyTimeRecord] SET TimeOut = #TIMEOUT, PairNo = #PAIR WHERE ID = #DTRID
INSERT INTO tblTimeLog([EmployeeId],[Time],[Type],[TimeLogSourceId],[CreationDate])
VALUES (#EMPID,#TIMEOUT,2,11,#TIMEOUT)
END
Just general formatting rules:
1. All declaration better are in the beginning.
2. Be consistent: all reserved words in capital
3. Do not use reserved words for column names.
4. Comment your code.
5. Use unified intends.
6. Finish statements with semicolon.
7. If you can, use CASE instead of IF SET.
8. Use short aliases for tables and views.
9. Name variables in SELECT statement, then it will be clear what goes where.
Have I forgot something?
Logic/Design.
1. Instead of multiple variables you could use temp table variable or just temp table. It would make code shorter and more readable.
2. First SELECT returns one row, but in the INSERT statement you might insert something completely irrelevant.
3. Having the same data in two tables might be a sign of bad design.
4. Having Date and Datetime for the same event in the same row is very suspicious.
It might be as the code below. Hope that helps.
DECLARE #Coid INT;
DECLARE #DTRID INT;
DECLARE #EMPID INT;
DECLARE #DATE DATETIME;
DECLARE #PAIR INT;
DECLARE #TIMEOUT DATETIME;
DECLARE #IdentityValue TABLE (
ContactID int,
EmpId int,
ContactDate DATETIME
);
-- Select ONLY one row
SELECT TOP 1
#EMPID = e.Id,
#Coid = e.CompanyId,
#DATE = tr.TimeIn,
#DTRID = tr.Id,
#TIMEOUT = GETDATE(),
#PAIR = CASE WHEN tr.TimeIn IS NULL THEN 0 ELSE 1 END
FROM tblEmployees as e
INNER JOIN tblDailyTimeRecord as tr
ON e.Id = tr.EmployeeId
WHERE tr.Date = CONVERT(DATE, GETDATE())
AND tr.TimeOut IS NULL
ORDER BY tr.ID DESC;
IF #Coid IS NULL
BEGIN
-- Insert more than one row or might insert nothing
INSERT INTO [tblDailyTimeRecord] (
[EmployeeId],
[Date],
[TimeOut],
[IsModified],
[CompanyId]
)
OUTPUT INSERTED.Id,
INSERTED.EmployeeId,
INSERTED.TimeOut
INTO #IdentityValue
SELECT Id as [EmployeeId],
CONVERT(DATE, GETDATE()) as [Date],
#TIMEOUT as [TimeOut],
0 as [IsModified],
CompanyId
FROM tblEmployees
WHERE AccessCode = 'GI0056';
INSERT INTO tblTimeLog(
[EmployeeId],
[Time],
[Type],
[TimeLogSourceId],
[CreationDate])
SELECT EmpId as [EmployeeId],
ContactDate as [Time],
2 as [Type],
11 as [TimeLogSourceId],
#TIMEOUT as [CreationDate]
FROM #IdentityValue;
END
ELSE
BEGIN
UPDATE [tblDailyTimeRecord]
SET TimeOut = #TIMEOUT,
PairNo = #PAIR
WHERE ID = #DTRID
INSERT INTO tblTimeLog(
[EmployeeId],
[Time],
[Type],
[TimeLogSourceId],
[CreationDate]
) VALUES (
#EMPID,
#TIMEOUT,
2,
11,
#TIMEOUT
);
END

SQL Server : loop through every row, add incremented value to column

I have just added a new column, Person_Id_Helper to MyTable. It is supposed to contain 1,2,3 etc, in the order the table is now sorted.
This is what I want to do:
DECLARE #i INT = 1, #NumberOfRows INT = SELECT COUNT(*) FROM MyTable
WHILE(#i <= #NumberOfRows)
BEGIN
-- Person_Id_Helper = #i
-- #i = #i + 1
END
How do I write this?
I think, that it might be the wrong idea to persist the sort oder within a column. But - for sure! - it is the wrong idea to do this in a while loop.
Read about row-based and set-based approaches. SQL demands for set-based thinking...
Look at this as an example how to do this (just paste it into an empty query window and execute, adapt to your needs):
DECLARE #tbl TABLE(SortDate DATE, Inx INT);
INSERT INTO #tbl VALUES({d'2016-01-20'},0)
,({d'2016-01-19'},0)
,({d'2016-01-14'},0)
,({d'2016-01-16'},0);
WITH cte AS
(
SELECT Inx,ROW_NUMBER() OVER(ORDER BY SortDate) AS RN
FROM #tbl
)
UPDATE cte SET Inx=RN;
SELECT * FROM #tbl;
I think this is what you want to achieve -
DECLARE #i INT = 0
UPDATE MyTable
SET
#i = Person_Id_Helper = #i + 1
Now check your column value.
Well we can not use ORDER BY clause in update statement. But to use it here is the updated query.
UPDATE t
SET Person_Id_Helper = rn.RowNum
FROM MyTable t
INNER JOIN (SELECT
ID
,ROW_NUMBER() OVER (ORDER BY ID) AS RowNum
FROM MyTable) rn
ON t.ID = rn.ID
#shungo: Thanks for point out.
Try like this,
DECLARE #tbl TABLE
(
datecolumn DATE,
Person_Id_Helper INT
);
INSERT INTO #tbl
VALUES ({d'2016-01-20'},
0),
({d'2016-01-19'},
0),
({d'2016-01-14'},
0),
({d'2016-01-16'},
0);
SELECT *
FROM #tbl;
UPDATE T
SET Person_Id_Helper = rn
FROM (SELECT Person_Id_Helper,
Row_number()
OVER(
ORDER BY datecolumn) AS rn
FROM #tbl) T
SELECT *
FROM #tbl;
Looping add increment the value using cursor in sqlserver
CREATE TABLE [dbo].[Table1](
[PID] [int] NULL,
[ProductDesc] [nvarchar](50) NULL,
[ProductCode] [nvarchar](10) NULL
) ON [PRIMARY]
GO
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode], [Person_Id_Helper]) VALUES (1, N'Packet-Eye', N'P001', NULL)
GO
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode], [Person_Id_Helper]) VALUES (2, N'Wiggy', N'W099 ', NULL)
GO
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode], [Person_Id_Helper]) VALUES (3, N'Wimax-Lite', N'W001', NULL)
GO
INSERT [dbo].[Table1] ([PID], [ProductDesc], [ProductCode], [Person_Id_Helper]) VALUES (4, N'Wimax-Home', N'e W002 ', NULL)
GO
Declare #count int
DECLARE #PID int
set #count=0
DECLARE c1 CURSOR READ_ONLY
FOR
SELECT [PID] FROM [Table1]
OPEN c1
FETCH NEXT FROM c1 INTO #PID
WHILE ##FETCH_STATUS = 0
BEGIN
set #count=#count+1
update Table1 set Person_Id_Helper=#count where [PID]=#PID
FETCH NEXT FROM c1 INTO #PID END
CLOSE c1
DEALLOCATE c1
select * from Table1

SQL Query - gather data based on date range - possible variable number of columns

please forgive my inexperience, I hope this isn't too dumb of a question, I'm stuck and have no where else to turn. I'll keep it to the point:
I'm trying to gather payroll data with the results like so:
The issue I have is the variable number of columns. I will be given a date range and are required to return an attendance record for each day in the given range, or a null value if no data is present. I'm using WebAPI as middle tier so I have the ability to perform further data manipulation to achieve this result.
My tables are as follows:
I can't be the first person who needs this done, any articles/posts or anything that would help me accomplish this? Even pseudo code would help; anything!
Thanks a million in advnace!
This is what I've been able to come up with but I'm not even sure if its doable:
-- convert date range into days of month
-- to ensure null values are included in data??
DECLARE #intFlag INT = 0;
DECLARE #numberOfDays INT = DATEDIFF(DAY, #startDate, #endDate);
DECLARE #TMP TABLE (DaysOfMonth date)
WHILE (#intFlag <= #numberOfDays)
BEGIN
INSERT INTO #TMP VALUES (DATEADD(DAY, #intFlag, #startDate));
SET #intFlag = #intFlag + 1
END
-- select days in given data range so c# app can build header row
-- would it help if I pivot this data?
SELECT
DaysOfMonth
FROM
#TMP
ORDER BY
DaysOfMonth
-- get a count for number of people
DECLARE #count INT = 0;
DECLARE #TMPPPL TABLE (Id int identity(1,0), PId Int)
INSERT INTO
#TMPPPL
SELECT
p.PersonId
FROM
dbo.People p
JOIN
dbo.UserTypes ut on p.UserType_UserTypeId = ut.UserTypeId and (ut.Code = 'caregiver' or ut.Code = 'director')
DECLARE #numberOfPeople INT = (SELECT COUNT(1) FROM #TMPPPL)
-- create and execute sproc to return row of data for each person
WHILE (#count <= #numberOfPeople)
BEGIN
-- STUCK HERE, This obviously won't work but what else can I do?
EXEC GetPersonAttendanceHours #personId, #startDate, #endDate;
SET #count = #count + 1
END
This was interesting. I think this will do what you're looking for. First test data:
CREATE TABLE people (PersonID int, Name varchar(30))
INSERT INTO people (PersonID, Name)
SELECT 1, 'Kelly'
UNION ALL SELECT 2, 'Dave'
UNION ALL SELECT 3, 'Mike'
CREATE TABLE attendances (PersonID int, SignIn datetime, SignOut datetime)
INSERT INTO attendances (PersonID, SignIn, SignOut)
SELECT 1, '1-Feb-2015 08:00', '1-Feb-2015 09:00'
UNION ALL SELECT 1, '1-Feb-2015 12:00', '1-Feb-2015 12:30'
UNION ALL SELECT 2, '2-Feb-2015 08:00', '2-Feb-2015 08:15'
UNION ALL SELECT 1, '3-Feb-2015 08:00', '3-Feb-2015 09:00'
UNION ALL SELECT 1, '4-Feb-2015 08:00', '4-Feb-2015 08:30'
UNION ALL SELECT 2, '4-Feb-2015 08:00', '4-Feb-2015 10:00'
UNION ALL SELECT 2, '6-Feb-2015 12:00', '6-Feb-2015 15:00'
UNION ALL SELECT 3, '6-Feb-2015 15:00', '6-Feb-2015 17:00'
UNION ALL SELECT 3, '8-Feb-2015 10:00', '8-Feb-2015 12:00'
Then a dynamic query:
DECLARE #startDate DATETIME='1-Feb-2015'
DECLARE #endDate DATETIME='9-Feb-2015'
DECLARE #numberOfDays INT = DATEDIFF(DAY, #startDate, #endDate)
declare #dayColumns TABLE (delta int, colName varchar(12))
-- Produce 1 row for each day in the report. Note that this is limited by the
-- number of objects in sysobjects (which is about 2000 so it's a high limit)
-- Each row contains a delta date offset, #startDate+delta gives each date to report
-- which is converted to a valid SQL column name in the format colYYYYMMDD
INSERT INTO #dayColumns (delta, colName)
SELECT delta, 'col'+CONVERT(varchar(12),DATEADD(day,delta,#startDate),112) as colName from (
select (ROW_NUMBER() OVER (ORDER BY sysobjects.id))-1 as delta FROM sysobjects
) daysAhead
WHERE delta<=#numberOfDays
-- Create a comma seperated list of columns to report
DECLARE #cols AS NVARCHAR(MAX)= ''
SELECT #cols=CASE WHEN #cols='' THEN #cols ELSE #cols+',' END + colName FROM #dayColumns ORDER BY delta
DECLARE #totalHours AS NVARCHAR(MAX)= ''
SELECT #totalHours=CASE WHEN #totalHours='' THEN '' ELSE #totalHours+' + ' END + 'ISNULL(' + colName +',0)' FROM #dayColumns ORDER BY delta
-- Produce a SQL statement which outputs a variable number of pivoted columns
DECLARE #query AS NVARCHAR(MAX)
SELECT #query=
'declare #days TABLE (reportDay date, colName varchar(12))
INSERT INTO #days (reportDay, colName)
SELECT DATEADD(day,Delta,'''+CONVERT(varchar(22),#startDate,121)+'''), ''col''+CONVERT(varchar(12),DATEADD(day,delta,'''+CONVERT(varchar(22),#startDate,121)+'''),112) as colName from (
select (ROW_NUMBER() OVER (ORDER BY sysobjects.id))-1 as Delta FROM sysobjects
) daysAhead
WHERE Delta<='+CAST(#numberOfDays as varchar(10))+'
SELECT p.Name, pivotedAttendance.*,'+#totalHours+' as totalHours FROM (
SELECT * FROM (
select p.PersonID, d.colName, CAST(DATEDIFF(MINUTE, a.SignIn, a.SignOut)/60.0 as decimal(5,1)) as hrsAttendance
from #days d
CROSS JOIN people p
LEFT OUTER JOIN attendances a ON a.PersonID=p.PersonID AND CAST(a.SignOut as DATE)=d.reportDay
) as s
PIVOT (
SUM(hrsAttendance) FOR colName in ('+#cols+')
) as pa
) as pivotedAttendance
INNER JOIN people p on p.PersonID=pivotedAttendance.PersonID'
-- Run the query
EXEC (#query)
Which produces data in a similar format to your example, with all of the days in the report range and a row for each person. From the above I see:
For presentation purposes you should be able to convert the column name to a display-able date (just parse the YYYYMMDD out of the column name). The date can't be used as the column name directly as it produces an invalid column name.
SQL Fiddle example here.
This is a variation on a theme that I've done in order to display schedules or attendance. I expect something similar should work with your report. Here is the beginning of your stored procedure:
DECLARE #iDay INT = 0;
DECLARE #countDays INT = DATEDIFF(DAY, #startDate, #endDate);
DECLARE #tempDates TABLE ([tempDate] DATE);
DECLARE #filterDates NVARCHAR;
WHILE (#iDay <= #countDays)
BEGIN
INSERT INTO #tempDates VALUES (DATEADD(DAY, #iDay, #startDate));
SET #iDay = #iDay + 1;
END;
SELECT #filterDates = STUFF(
(SELECT N''',''' + CONVERT(NVARCHAR, [tempDate], 103) FROM #tempDates FOR XML PATH('')),
1,
2,
''
);
You were on the right track with your suggestion. The next query gets your data before you PIVOT it.
SELECT [People].[Person_PersonID], [tempDates].[tempDate], [Attendances].[SignIn], [Attendances].[SignOut],
MIN([Attendances].[SignOut], DATEADD(DAY, 1, [tempDates].[tempDate]))
- MAX([Attendances].[SignIn], [tempDates].[tempDate]) * 24 AS [numHours]
FROM [People]
CROSS JOIN #tempDates [tempDates]
LEFT JOIN [Attendances]
ON (
([Attendances].[SignIn] < DATEADD(DAY, 1, [tempDates].[tempDate]))
AND ([Attendances].[SignOut] > [tempDates].[tempDate])
);
Once we're satisfied with the results of the previous query, we substitute it with a query using PIVOT, which should look something like this.
SELECT *
FROM (
SELECT [People].[PersonID], [tempDates].[tempDate], [Attendances].[SignIn], [Attendances].[SignOut],
MIN([Attendances].[SignOut], DATEADD(DAY, 1, [tempDates].[tempDate]))
- MAX([Attendances].[SignIn], [tempDates].[tempDate]) * 24 AS [numHours]
FROM [People]
CROSS JOIN #tempDates [tempDates]
LEFT JOIN [Attendances]
ON (
([Attendances].[SignIn] < DATEADD(DAY, 1, [tempDates].[tempDate]))
AND ([Attendances].[SignOut] > [tempDates].[tempDate])
)
) AS [DatedAttendance]
PIVOT (
SUM([numHours]) FOR ([tempDate] IN (#filterDates))
) AS [PivotAttendance]
ORDER BY [PersonID]