I have a table with account deposits.
+-----------+------------+-----------+
| DepositId | Date | Amount |
+-----------+------------+-----------+
| 1 | 2014-06-12 | 2342,00 |
| 2 | 2014-08-05 | 23423,00 |
| 3 | 2014-09-07 | 7745,00 |
|....................................|
| 12 | 2014-12-05 | 35435,00 |
| 13 | 2014-12-11 | 353453,00 |
| 14 | 2014-12-29 | 53453,00 |
+-----------+------------+-----------+
I want to see weekly balance change like this:
+------------+----------+
| Date | Amount |
+------------+----------+
| 2014-10-07 | 74754,00 |
| 2014-10-14 | 74754,00 |
| 2014-10-21 | 6353,00 |
| 2014-10-28 | 6353,00 |
| ........ | ...... |
| 2014-12-30 | 53453,00 |
+------------+----------+
To see this changes for past 3 month (~ 13 weeks) I can use this queries:
select CONVERT(date, DATEADD(WEEK, -13, GETDATE())) as Date, ad.Amount
from AccountDeposits as ad
inner join
(select Max(Date) as Date
from AccountDeposits
where (Date < DATEADD(WEEK, -13, GETDATE())))
as ad2 on (ad.Date = ad2.Date)
union all
select CONVERT(date, DATEADD(WEEK, -12, GETDATE())) as Date, ad.Amount
from AccountDeposits as ad
inner join
(select Max(Date) as Date
from AccountDeposits
where (Date < DATEADD(WEEK, -12, GETDATE())))
as ad2 on (ad.Date = ad2.Date)
......................................................
select CONVERT(date, DATEADD(WEEK, -1, GETDATE())) as Date, ad.Amount
from AccountDeposits as ad
inner join
(select Max(Date) as Date
from AccountDeposits
where (Date < DATEADD(WEEK, -1, GETDATE())))
as ad2 on (ad.Date = ad2.Date)
I have to do this with recursive Common Table Expressions but in recursive part of CTE I can't use MAX() function. How I should write this query series to on query with CTE?
I may have misinterpreted the question (apologies if I have) but if the question is "for each week when there were deposits, give the sum of the total deposits for that week and the last day of that week" then the T-SQL below would give the correct results.
with myCte1 as
(
select *, datepart(week,d.[Date]) as wk, datepart(year,d.[Date]) as yr,
dateadd(dd, 7-(datepart(dw,d.[Date])), d.[Date]) as weekEndDate
from dbo.AccountDeposits as d
),
myCte2 as
(
select *, sum(m.Amount) over (partition by m.yr, m.wk) as totalWeeklyAmt
from myCte1 as m
)
select distinct m.weekEndDate, m.totalWeeklyAmt
from myCte2 as m
This uses two CTE's, one summarises our source data and the other uses a recursive CTE to generate all the weeks, this allows us to also show weeks where there were no deposits. It also uses two co-related subqueries to get the sumarised data from the first CTE.
I think this satisfies the requirements of your assignment.'
--NOTE: this gets data based on week end date, so all deposits for week of #WeeksHistory ago not just the deposits after the date (today minus #WeeksHistory weeks).
--NOTE: this gets all historical data so that we can start with opening balance of $0 otherwise Closing balance wont take previous deposits into account.
--NOTE: this gets the week starting #WeeksHistory ago and also this week so you will end up with #WeeksHistory +1 records - you might want to adjust this as necessary
-- set up our source data
declare #AccountDeposits table (DepID int, AcctHolderID int, TxnDate date, Amount numeric(10,2))
insert into #AccountDeposits
values
(1, 3,'12-25-2014', 2423.00),
(2, 1,'12-13-2014',4231.00),
(3, 2,'11-01-2014',666.00),
(4, 1,'11-01-2014',4241.34),
(5, 4,'10-23-2014',4221.00),
(6, 2,'10-22-2014',9992.00),
(7, 2,'10-04-2014',3524.00),
(8, 2,'10-14-2014',3524.00),
(9, 2,'10-15-2014',3524.00),
(10, 2,'10-16-2014',3524.00),
(11, 3,'10-14-2014',3524.00),
(12, 3,'10-15-2014',3524.00),
(13, 3,'10-16-2014',3524.00),
(14, 1,'10-01-2014',3524.00),
(15, 2,'10-01-2014',3524.00),
(16, 3,'10-01-2014',3524.00),
(17, 4,'01-01-2015',3524.00)
declare #AcctHolderID as int = 2
declare #WeeksHistory int = -13
select dateadd(week,#WeeksHistory,getdate()) ThirteenWeeksAgo
;with
src (AcctHolderID, WeekEndsOn, Amount)
as (select
AcctHolderID,
DATEADD(DAY, 7-DATEPART(WEEKDAY, TxnDate), TxnDate),
SUM(Amount)
from #AccountDeposits
where AcctHolderID = #AcctHolderID -- we filter up here so that we arent processing data we dont care about.
group by
AcctHolderID,
DATEADD(DAY, 7-DATEPART(WEEKDAY, TxnDate), TxnDate)
),
r_cte (AcctHolderID, WeekEndsOn, TotalDep, ClosingBal)
as (select
AcctHolderID,
dateadd(ww,-1,Min(WeekEndsOn)),
convert(numeric(10,2),0.00),
convert(numeric(10,2),0.00)
from
src
group by
AcctHolderID
union all
select
r_cte.AcctHolderID,
dateadd(WW,1,r_cte.WeekEndsOn),
convert(numeric(10,2),ISNULL((select Amount from src where AcctHolderID = r_cte.AcctHolderID and WeekEndsOn = dateadd(WW,1,r_cte.WeekEndsOn)),0)),
convert(numeric(10,2),ISNULL((select Amount from src where AcctHolderID = r_cte.AcctHolderID and WeekEndsOn = dateadd(WW,1,r_cte.WeekEndsOn)),0) + r_cte.ClosingBal)
from
r_cte
where
AcctHolderID = r_cte.AcctHolderID
and r_cte.WeekEndsOn < DATEADD(DAY, 7-DATEPART(WEEKDAY, Getdate()), DATEADD(WW,-1,Getdate()))
)
select AcctHolderID, DATEDIFF(ww, WeekEndsOn, getdate()) as WeeksAgo, WeekEndsOn, TotalDep, ClosingBal
from r_cte
where r_cte.WeekEndsOn > dateadd(week,#WeeksHistory,getdate())
order by
AcctHolderID,
WeekEndsOn
I have solved it without CTE...
First create table with startdate and enddate for 13 weeks starting from getdate ().
Create table weeklydates
(Startdate date,
Enddate date
)
Declare #startdate date
Declare #enddate date
Set #startdate = cast (dateadd (week,-13,getdate ()) as date)
Set #enddate = dateadd (day,7,#startdate)
While #enddate < = getdate ()
Begin
Insert into weeklydates
Select #startdate, #enddate
Set #startdate = dateadd (day,1,#enddate)
Set #enddate = dateadd (day,7,#startdate)
End
Now use this table to display amount which will be sum of amount whose dates fall between start date and end date
Select a.startdate,a.enddate, (select sum (amount) from yourtablehavingamount as b
Where b.deposit >=a.startdate and b.deposit <=a.enddate)
From weeklydates as a
USE:
SELECT TOP 1 [Date]
FROM AccountDeposits
--Add WHERE Clause
ORDER BY [Date] DESC
:) David
Related
I would like to bring the month into columns like in pivot. But first I would like to order by the month. How can I order the month starting from October as the first month?
October | November | December | January | ....
CREATE TABLE #month
(
[ID] INT,
[Date] DATETIME
)
GO
INSERT INTO #month VALUES (1,'2013-04-04')
INSERT INTO #month VALUES (2,'2013-07-07')
INSERT INTO #month VALUES (3,'2013-10-10')
INSERT INTO #month VALUES (4,'2013-01-01')
INSERT INTO #month VALUES (5,'2013-02-02')
INSERT INTO #month VALUES (6,'2013-03-03')
INSERT INTO #month VALUES (7,'2013-05-05')
INSERT INTO #month VALUES (8,'2013-06-06')
INSERT INTO #month VALUES (9,'2013-08-08')
INSERT INTO #month VALUES (10,'2013-09-09')
INSERT INTO #month VALUES (11,'2013-11-11')
INSERT INTO #month VALUES (12,'2013-12-12')
GO
SELECT
DATENAME(month,Date) AS [Month Name]
, [Date]
FROM #month
group by DATENAME(month,Date)
, [Date]
ORDER BY min([Date])
Just another option
SELECT
DATENAME(month,Date) AS [Month Name]
, [Date]
FROM #month
group by DATENAME(month,Date)
, [Date]
ORDER BY choose(datepart(month,date),4,5,6,7,8,9,10,11,12,1,2,3)
This is one method, whereby you take 10 away from the month number, and add 10 to the months that end up with a result less than 0:
SELECT *
FROM #month
ORDER BY CASE WHEN DATEPART(MONTH,[Date]) - 10 < 0 THEN DATEPART(MONTH,[Date]) + 10
ELSE DATEPART(MONTH,[Date]) - 10
END;
Use conditional sorting with a CASE expression in the ORDER BY clause:
ORDER BY MONTH(min([Date])) +
CASE WHEN MONTH(min([Date])) < 10 THEN 12 ELSE 0 END
See the demo.
Results:
> Month Name | Date
> :--------- | :---------
> October | 2013-10-10
> November | 2013-11-11
> December | 2013-12-12
> January | 2013-01-01
> February | 2013-02-02
> March | 2013-03-03
> April | 2013-04-04
> May | 2013-05-05
> June | 2013-06-06
> July | 2013-07-07
> August | 2013-08-08
> September | 2013-09-09
One more option:
SELECT DATENAME(month, Date) AS [Month Name], Date
FROM #month
ORDER BY DATEPART(MONTH, Date) + ((1 - (DATEPART(MONTH, Date) / 10)) * 12)
You really just need to add three months to compensate as starting in October could be logically treated as "Month -3". Adding back three brings your sequence into alignment with the regular ordering.
order by month(dateadd(month, 3, <date expression>))
I have a cost record and I would like to create N records from it.
The children records have some different parameters.
For example:
The parents record:
date | amount | duration
20170201 | 5000 | 5 months
The children records:
date | amount | duration
20170301 | 1000 | 1 months
20170401 | 1000 | 1 months
20170501 | 1000 | 1 months
20170601 | 1000 | 1 months
20170701 | 1000 | 1 months
How can I do this without iteration? Without cursor or while?
Following SQL CTE query could be used based on Abdul's solution
/*
Create Table PARENT (PARENT_DATE DATE, PARENT_AMOUNT DECIMAL(18,2),PARENT_MONTH INT)
INSERT INTO PARENT SELECT '20170201',5000 ,5
INSERT INTO PARENT SELECT '20180601',120 ,3
*/
;WITH CTE_CHILD
AS (
SELECT
Parent_Date,
Parent_Amount,
Parent_Month,
DateAdd(Month, 1, Parent_Date) as Child_Date,
Parent_Amount/Parent_Month AS Child_Amount,
1 AS Child_Duration
FROM Parent
UNION ALL
SELECT
Parent_Date,
Parent_Amount,
Parent_Month,
DateAdd(Month, 1, Child_Date) as Child_Date,
Child_Amount,
Child_Duration
FROM CTE_CHILD
WHERE
DateAdd(Month, 1, Child_Date) <= DateAdd(Month, Parent_Month, Parent_Date)
)
SELECT
Child_Date,
Child_Amount,
Child_Duration
FROM CTE_CHILD
assuming you have a table like below:
create table tblRecords ( date int, amount money, duration int);
insert into tblRecords values
(20170201,5000,5),
(20180101,9000,3);
you can use a query like below:
select
date= date + r*100
,amount= amount/duration
,duration =1
from tblRecords
cross apply
(
select top (select duration)
r= row_number() over(order by (select null))
from
sys.objects s1
cross join
sys.objects s2
) h
see working demo
One method is CTE.
DECLARE #PARENT AS TABLE
(PARENT_DATE DATE, PARENT_AMOUNT DECIMAL(18,2),PARENT_MONTH INT)
INSERT INTO #PARENT
SELECT '20170201',5000 ,5
;WITH CTE_CHILD
AS (
SELECT DATEADD(MONTH,1,PARENT_DATE) AS CHILD_DATE
,PARENT_AMOUNT/PARENT_MONTH AS CHILD_AMOUNT
,1 AS CHILD_DURATION
FROM #PARENT
WHERE DATEADD(MONTH,1,PARENT_DATE) <= DATEADD(MONTH,PARENT_MONTH,PARENT_DATE)
UNION ALL
SELECT DATEADD(MONTH,1,CHILD_DATE)
,PARENT_AMOUNT/PARENT_MONTH
,1
FROM CTE_CHILD
INNER JOIN #PARENT ON DATEADD(MONTH,1,CHILD_DATE) <= DATEADD(MONTH,PARENT_MONTH,PARENT_DATE)
)
SELECT * FROM CTE_CHILD
option (maxrecursion 0)
Output:-
CHILD_DATE CHILD_AMOUNT CHILD_DURATION
2017-03-01 1000.0000000000000 1
2017-04-01 1000.0000000000000 1
2017-05-01 1000.0000000000000 1
2017-06-01 1000.0000000000000 1
2017-07-01 1000.0000000000000 1
I have data in a table with dates, and want to count the rows by "Week of" (e.g., "Week of 2017-05-01"), where the result has the week's date (starting on Mondays) and the count of matching rows — even if there are no rows for that week. (This will all be in a date range.)
I can partition things into weeks readily enough by grouping on DATEPART(wk, D) (where D is the date column), but I'm struggling with:
How to get the "Week of" date and fill, and
How to have a row for a week where there are no matching rows in the data
Here's grouping by week:
SET DATEFORMAT ymd;
SET DATEFIRST 1; -- Monday is first day of week
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
SELECT DATEPART(wk, D) AS [Week Number], COUNT(*) AS [Count]
FROM #temp
GROUP BY DATEPART(wk, D)
ORDER BY DATEPART(wk, D);
Which gives me:
+−−−−−−−−−−−−−+−−−−−−−+
| Week Number | Count |
+−−−−−−−−−−−−−+−−−−−−−+
| 19 | 5 |
| 20 | 19 |
| 22 | 8 |
| 23 | 10 |
| 24 | 5 |
| 26 | 4 |
+−−−−−−−−−−−−−+−−−−−−−+
But ideally I want:
+−−−−−−−−−−−−+−−−−−−−+
| Week | Count |
+−−−−−−−−−−−−+−−−−−−−+
| 2017-05-01 | 5 |
| 2017-05-08 | 19 |
| 2017-05-15 | 0 |
| 2017-05-22 | 8 |
| 2017-05-29 | 10 |
| 2017-06-05 | 5 |
| 2017-06-12 | 0 |
| 2017-06-19 | 4 |
| 2017-06-26 | 0 |
+−−−−−−−−−−−−+−−−−−−−+
How can I do that?
Set up information for testing:
SET DATEFIRST 1;
SET DATEFORMAT ymd;
CREATE TABLE #temp (
D DATETIME
);
GO
INSERT INTO #temp (D)
VALUES -- Week of 2017-05-01 (#19)
('2017-05-01'),('2017-05-01'),('2017-05-01'),
('2017-05-06'),('2017-05-06'),
-- Week of 2017-05-08 (#20) - note no data actually on the 8th
('2017-05-10'),
('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),('2017-05-11'),
('2017-05-12'),('2017-05-12'),('2017-05-12'),('2017-05-12'),
('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),('2017-05-13'),
('2017-05-14'),
-- Week of 2017-05-15 (#21)
-- (note we have no data for this week)
-- Week of 2017-05-22 (#22)
('2017-05-22'),('2017-05-22'),('2017-05-22'),
('2017-05-23'),('2017-05-23'),('2017-05-23'),('2017-05-23'),('2017-05-23'),
-- Week of 2017-05-29 (#23)
('2017-05-29'),('2017-05-29'),('2017-05-29'),
('2017-06-02'),('2017-06-02'),
('2017-06-03'),
('2017-06-04'),('2017-06-04'),('2017-06-04'),('2017-06-04'),
-- Week of 2017-06-05 (#24) - note no data actually on the 5th
('2017-06-08'),('2017-06-08'),('2017-06-08'),
('2017-06-11'),('2017-06-11'),
-- Week of 2017-06-12 (#25)
-- (note we have no data for this week)
-- Week of 2017-06-19 (#26)
('2017-06-19'),('2017-06-19'),('2017-06-19'),
('2017-06-20');
GO
To do this, you have to generate a table or CTE with the Monday dates and their week numbers (as shown in this answer, slightly modified for what we need to do below), then LEFT JOIN or OUTER APPLY that with your data grouped by week, using the week numbers:
SET DATEFORMAT ymd;
SET DATEFIRST 1;
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
;WITH Mondays AS (
SELECT #startDate AS D, DATEPART(WK, #startDate) AS W
UNION ALL
SELECT DATEADD(DAY, 7, D), DATEPART(WK, DATEADD(DAY, 7, D))
FROM Mondays m
WHERE DATEADD(DAY, 7, D) < #endDate
)
SELECT LEFT(CONVERT(NVARCHAR(MAX), Mondays.D, 120), 10) AS [Week Of], d.Count
FROM Mondays
OUTER APPLY (
SELECT COUNT(*) AS [Count]
FROM #temp
WHERE DATEPART(WK, D) = W
AND D >= #startDate
AND D < #endDate
) d
ORDER BY Mondays.D;
Two notes on that:
I'm assuming we can ensure that #startDate is a Monday, which is easily done outside the query or could be done with a simple loop in T-SQL if needed (backing up until WEEKPART(WEEKDAY, #startDate) is 1). (Or worst case we could generate all the dates and then filter them with WEEKPART(WEEKDAY, ...).)
I'm assuming the date range is always a year or less; otherwise, we'd have duplicated week numbers. If the date range could be longer than a year, combine the week number with the year everywhere we're just using a week number above (e.g., DATEPART(YEAR, D) * 100 + DATEPART(wk, D)).
You can use this.
SET DATEFORMAT ymd;
SET DATEFIRST 1; -- Monday is first day of week
DECLARE #startDate DATETIME = '2017-05-01';
DECLARE #endDate DATETIME = '2017-07-01';
;WITH OrgResult AS ( -- Grouping result with missing week. Answer of the first question
SELECT
DATEADD(DAY, 1 - DATEPART (WEEKDAY, D), D) [Week] -- Fist Day Of the Week
, COUNT(*) [Count]
FROM #temp
WHERE D BETWEEN #startDate AND #endDate
GROUP BY
DATEADD(DAY, 1 - DATEPART (WEEKDAY, D), D)
)
, Result AS -- Adds only missing weeks. Answer of the second question
(
SELECT * FROM OrgResult
UNION ALL
SELECT DATEADD( DAY, 7, R.[Week] ), 0 [Count]
FROM Result R
WHERE NOT EXISTS( SELECT * FROM OrgResult O WHERE [Week] = DATEADD( DAY, 7, R.[Week] ) )
AND DATEADD( DAY, 7, R.[Week] ) <= #endDate
)
SELECT * FROM Result
ORDER BY [Week]
Result:
Week Count
----------- -----------
2017-05-01 5
2017-05-08 19
2017-05-15 0
2017-05-22 8
2017-05-29 10
2017-06-05 5
2017-06-12 0
2017-06-19 4
2017-06-26 0
Here's another approach. I included this as it will generate less reads than the Recursive CTE Solution and will be a lot fast
WITH E(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))x(x)),
iTally(N) AS
(
SELECT TOP (((DATEDIFF(day,#startdate, #endDate))/7)+1)
(ROW_NUMBER() OVER (ORDER BY (SELECT 1))-1)
FROM E a, E b, E c
)
SELECT WeekOf = DATEADD(WEEK,N,#startDate), [count] = COUNT(t.D)
FROM iTally i
LEFT JOIN #temp t ON t.D >= DATEADD(WEEK,N,#startDate) AND t.D < DATEADD(WEEK,N+1,#startDate)
GROUP BY DATEADD(WEEK,N,#startDate)
ORDER BY DATEADD(WEEK,N,#startDate); -- not required
Results:
WeekOf count
---------- -----------
2017-05-01 5
2017-05-08 19
2017-05-15 0
2017-05-22 8
2017-05-29 10
2017-06-05 5
2017-06-12 0
2017-06-19 4
2017-06-26 0
I have a SQL Server table named AgentLog in which I store for each agent his daily number of sales.
+-----------+------------+-------------+
| AgentName | Date | SalesNumber |
+-----------+------------+-------------+
| John | 01.01.2014 | 45 |
| Terry | 01.01.2014 | 30 |
| John | 02.01.2014 | 20 |
| Terry | 02.01.2014 | 15 |
| Terry | 03.01.2014 | 52 |
| Terry | 04.01.2014 | 24 |
| Terry | 05.01.2014 | 12 |
| Terry | 06.01.2014 | 10 |
| Terry | 07.01.2014 | 23 |
| John | 08.01.2014 | 48 |
| Terry | 08.01.2014 | 35 |
| John | 09.01.2014 | 37 |
| Terry | 10.01.2014 | 35 |
+-----------+------------+-------------+
If an agent doesn't work on one particular day, there is no record of his sales on that date.
I want to generate a report(query) on a given date interval (ex: 01.01.2014 - 10.01.2014) that counts on how many days an agent wasn't present for work (ex: John - 6 days), was at work (John - 4 days) and also returns the date interval it wasn't present (ex: John 03.01.2014 - 07.01.2014, 10.01.2014) (there can be multiple intervals).
You need to create a custom table and populate it with a record for each date you want in your range (Feel free to go as far back in the past and forward into the future as you feel you may need.). You could do this in Excel very easily and import it.
Select *
from Custom.DateListTable dlt
left outer join agentlog ag
on dlt.Date = ag.Date
I would approach this by getting the number of dates in the interval, as well as the number of dates the agent was at work, and you then have everything you need.
To get the number of days you can use DATEDIFF:
SELECT DATEDIFF(day, '2014-01-01', '2014-10-01') AS totalDays;
To get the number of days an agent worked, you can use the COUNT(*) aggregate function:
SELECT agentName, COUNT(*) AS daysWorked
FROM myTable
GROUP BY agentName;
Then, you can just add to that query to get the days not worked by subtracting totalDays - daysWorked:
SELECT agentName, COUNT(*) AS daysWorked, (DATEDIFF(day, '2014-01-01', '2014-10-01') - COUNT(*)) AS daysMissed
FROM myTable
GROUP BY agentName;
Here is an SQL Fiddle example.
The only way I can think of to resolve this is to creating a temporary table with only one column (datetime) and save there all the dates from the selected range. You can create an stored procedure that fills that temporary table using a cursor with all the dates from the interval. Then do a LEFT join between your table and the temporary table to look for null values in your table (The days where that person didn't come to work)
Try this...
SET DATEFIRST 1; --Monday
DECLARE #StartDate DATETIME = '2014-01.01',
#EndDate DATETIME = '2014-01.10';
WITH data as (
select 0 as i, DATEADD(DAY, 0, #StartDate) as TheDate
union all
select i + 1, DATEADD(DAY, i + 1, #StartDate) as TheDate
from data
where i < (#EndDate - #StartDate)
)
SELECT a.AgentName,
SUM(CASE WHEN c.Date IS NULL THEN 1 ELSE 0 END) AS Missing,
SUM(CASE WHEN c.Date IS NOT NULL THEN 1 ELSE 0 END) AS Working
FROM Agent a
JOIN data b ON NOT EXISTS(SELECT NULL FROM SpecialDate s WHERE s.date = b.TheDate)
LEFT JOIN AgentLog c ON
c.AgentName = a.AgentName
AND c.Date = b.TheDate
WHERE DATEPART(weekday, b.TheDate) <= 5
GROUP BY a.AgentName
OPTION (MAXRECURSION 10000);
It includes a check for weekends, as well as a reference to "SpecialDate" where a list of non working days can be maintained, and excluded from the check.
Reading your question again, I realise that this will only solve half your problem.
NOTE: The following answer mainly addresses the trickiest part of the question, which is how to obtain "absence from work" intervals.
Given these values as Interval Start - End dates:
DECLARE #IntervalStart DATE = '2013-12-30'
DECLARE #IntervalEnd DATE = '2014-01-10'
the following query gives you the "absence from work" intervals:
SELECT AgentName,
DATEADD(d, 1, t.[Date]) As OffWorkStart,
DATEADD(d, -1, t.NextDate) As OffWorkEnd
FROM (
SELECT AgentName, [Date], LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC) As NextDate,
DATEDIFF(DAY, [Date], LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC)) As NextMinusCurrent
FROM #AgentLog) t
WHERE t.NextMinusCurrent > 1
-- Get marginal beginning interval (in case such an interval exists)
UNION ALL
SELECT AgentName, #IntervalStart AS OffWorkStart, DATEADD(DAY, -1, MIN([Date])) AS OffWorkEnd
FROM #AgentLog
GROUP BY AgentName
HAVING MIN([Date]) > #IntervalStart
-- Get marginal ending interval (in case such an interval exists)
UNION ALL
SELECT AgentName, DATEADD(DAY, 1, MAX([Date])) AS OffWorkStart, #IntervalEnd
FROM #AgentLog
GROUP BY AgentName
HAVING MAX([Date]) < #IntervalEnd
ORDER By AgentName, OffWorkStart
With the input data you supplied, the above query gives you the following output:
AgentName OffWorkStart OffWorkEnd
---------------------------------------
John 2013-12-30 2013-12-31
John 2014-01-03 2014-01-07
John 2014-01-10 2014-01-10
Terry 2013-12-30 2013-12-31
Terry 2014-01-09 2014-01-09
The idea behind the basic part of the query is to employ the following nested query:
SELECT AgentName,
[Date],
LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC) As NextDate,
DATEDIFF(DAY, [Date], LEAD([Date]) OVER (PARTITION BY AgentName ORDER BY [Date] ASC)) As NextMinusCurrent
FROM #AgentLog
in order to get any existing gaps between the days a certain agent is present for work. A value of NextMinusCurrent > 1 indicates such a gap.
Counting days is trivial once you have the above query in place. E.g. placing the above query in a CTE you can count total number of absence days with sth like:
;WITH cte (
... query goes here
)
SELECT AgentName, SUM(DATEDIFF(DAY, OffWorkStart, OffWorkEnd) + 1) AS AbsenceDays
FROM cte
GROUP By AgentName
P.S. The above query makes use of SQL Server LEAD function, which is available from SQL SERVER 2012 onwards.
SQL Fiddle here
EDIT:
CTEs together with ROW_NUMBER() can be used to simulate LEAD function. The first part of the query becomes:
;WITH cte1 AS (
SELECT AgentName,
[Date],
ROW_NUMBER() OVER (PARTITION BY AgentName ORDER BY [Date] ASC) As rn
FROM #AgentLog
),
cte2 AS (
SELECT cte1.AgentName, cte1.[Date],
cteLead.[Date] AS NextDate,
DATEDIFF(DAY, cte1.[Date], cteLead.[Date]) As NextMinusCurrent
FROM cte1
LEFT OUTER JOIN cte1 AS cteLead
ON (cte1.rn = cteLead.rn - 1) AND (cte1.AgentName = cteLead.AgentName)
)
SELECT AgentName,
DATEADD(d, 1, cte2.[Date]) As OffWorkStart,
DATEADD(d, -1, cte2.NextDate) As OffWorkEnd
FROM cte2
WHERE NextMinusCurrent > 1
SQL Fiddle for SQL Server 2008 here. I hope it executes in SQL Server 2005 also!
I have a SQL Server table (tbl) with 2 columns: ts (timestamp) and val (value). I want to make a selection that gives back four columns: first is the day, the second is the average of the values from 10pm the preceding day till 6am, the third row is the average of the values from 6am till 2pm, and the fourth row contains the average of the stored values from 2pm till 10pm. So averages for 8-hour periods in a day which instead of midnight starts at the previous day at 10pm.
This is the query I have so far: http://sqlfiddle.com/#!3/41334/2
I have the average for the whole 24-hour periods (from 10pm), but now I'm stuck. I was thinking that I could make 3 selections for the 8 hours periods and then join them on the day, but I don't know how or if at all I'm on the right track. Please help.
The result I would like to get using my example data:
DAY | AVG_NITE | AVG_MORN | AVG_AFTN
2014.12.07 | 3.75 | 5.6667 | 4.5714
2014.12.08 | 4.6 | 5.6 | 5.4
2014.12.09 | 5.5 | (null) | (null)
the code below produces desired ouput. it uses CTEs but you can change them to subqueries or Views.
WITH tbl2 AS (
SELECT DATEADD(hour, 2, ts) AS ts2
,val
FROM tbl
)
, tbl_hours AS (
SELECT convert(varchar, ts2,102) AS [day]
,ROUND(DATEDIFF(hour, cast(ts2 AS DATE), ts2)/8,0) AS period
,val
FROM tbl2
)
SELECT
[day]
,AVG( case when period = 0 then val else null end) AS [avg_nite]
,AVG( case when period = 1 then val else null end) AS [avg_morn]
,AVG( case when period = 2 then val else null end) AS [avg_aftn]
FROM tbl_hours
GROUP BY [day]
select convert(varchar, dateadd(hour,2,ts), 102) as day,
avg(val) as avg_fullday
,avg(case when dateadd(day, -1, datepart(hour, ts)) in (22,23)
or DATEPART(hour, ts) in (0,1,2,3,4,5)
then (val) end)'Nite'
,avg(case when DATEPART(hour, ts) in( 6,7,8,9,10,11,12,13)
then (val) end) 'Morn'
,avg(case when DATEPART(hour, ts) in (14,15,16,17,18,19,20,21) then (val) end) 'Aftn'
from #tbl
group by convert(varchar, dateadd(hour,2,ts), 102);
drop table #tbl