Change output according to LAG - sql-server

I have the current issue:
I'm trying to get the amount of time each of our workers have worked in a day to calculate our company's productivity. We have the time each of our workers has entered and left the building.
The rule is, sometimes our workers leaves the building to smoke or get something from the news stand outside, so we don't take that into consideration and count as if the person never left the building.
We have a cafeteria inside our building so most people don't actually leave the building to have lunch/dinner, so we just remove 1 hour from their productivity calculation, but, if they leave for more then 45 minutes, we will consider that the worker left to lunch/dinner.
I need the end result to look like this:
+----------+----------------+----------------+---------+----------+
| PersonID | IN | OUT | MINUTES | EatOut |
+----------+----------------+----------------+---------+----------+
| 1 | 20170807 08:00 | 20170807 17:25 | 465 | 1 |
+----------+----------------+----------------+---------+----------+
| 2 | 20170807 08:00 | 20170807 17:00 | 540 | 0 |
+----------+----------------+----------------+---------+----------+
My query I have so far:
DECLARE #mytable TABLE(
PersonId INT,
Situation VARCHAR(3),
SituationDtm DATETIME
);
INSERT INTO #mytable VALUES
(1, 'IN', '20170807 08:00'),
(1, 'OUT', '20170807 12:30'),
(1, 'IN', '20170807 14:00'),
(1, 'OUT', '20170807 17:15'),
(2, 'IN', '20170807 08:00'),
(2, 'OUT', '20170807 09:15'),
(2, 'IN', '20170807 09:30'),
(2, 'OUT', '20170807 17:00');
WITH CTE AS (
SELECT
[PersonId],
Situation AS 'CUR.Situation',
SituationDtm AS 'CUR.SituationDtm',
LEAD(Situation) OVER(PARTITION BY PersonId ORDER BY SituationDtm) AS 'NEXT.Situation',
LEAD(SituationDtm) OVER(PARTITION BY PersonId ORDER BY SituationDtm) AS 'NEXT.SituationDtm'
FROM
#mytable
)
SELECT
[CUR.Situation],
[CUR.SituationDtm],
[NEXT.Situation],
[NEXT.SituationDtm],
DATEDIFF(MINUTE, [CUR.SituationDtm], [NEXT.SituationDtm]) AS 'MINUTES'
FROM
CTE
Thanks in advance

You can further query as below: Since you are looking your solution in SQL Server 2008 where you do not have lead/lag you can query as below:
;With Cte as (
Select *, RowN = Row_Number() over(Partition by PersonId order by SituationDtm) from #mytable
), Cte2 as (
Select c1.*, c2.Situation as NextSituation, c2.SituationDtm as NextSituationDtm from cte c1 left join cte c2 on c1.RowN+1 = c2.RowN
and c1.PersonId = c2.PersonId
)
Select PersonId,
Min(SituationDTM) as [In],
Max(Situationdtm) as [Out],
Sum(Case when Situation = 'OUT' and NextSituation = 'IN' and datediff(mi,SituationDtm, NextSituationDTM) > 60 then 1 else 0 end) EatOut,
Sum(Case when Situation = 'OUT' and NextSituation = 'IN' and datediff(mi,SituationDtm, NextSituationDTM) > 60 then 0 else datediff(mi,SituationDtm, NextSituationDTM) end) as [minutes]
from Cte2
group by PersonId
In later versions after >= 2012 you can query as below:
Select PersonId,
Min(SituationDTM) as [In],
Max(Situationdtm) as [Out],
Sum(Case when Situation = 'OUT' and NextSituation = 'IN' and datediff(mi,SituationDtm, NextSituationDTM) > 60 then 1 else 0 end) EatOut,
Sum(Case when Situation = 'OUT' and NextSituation = 'IN' and datediff(mi,SituationDtm, NextSituationDTM) > 60 then 0 else datediff(mi,SituationDtm, NextSituationDTM) end) as [minutes]
from (
Select *, NextSituationDTM = lead(situationdtm) over (partition by personid order by situationdtm),
NextSituation = lead(Situation) over (partition by personid order by situationdtm) from #mytable
) a
group by PersonId
Output as below:
+----------+-------------------------+-------------------------+--------+---------+
| PersonId | In | Out | EatOut | minutes |
+----------+-------------------------+-------------------------+--------+---------+
| 1 | 2017-08-07 08:00:00.000 | 2017-08-07 17:15:00.000 | 1 | 465 |
| 2 | 2017-08-07 08:00:00.000 | 2017-08-07 17:00:00.000 | 0 | 540 |
+----------+-------------------------+-------------------------+--------+---------+

Related

SQL Query to get in/out time

I need to create a report when the user entering and exiting time. So far I only manage to get the min and max time. Here, the example of table:
ID | Flag_Location (bit) | Time
----------------------------
1001 | 1 | 8:00
1001 | 1 | 9:00
1001 | 1 | 10:00
1001 | 0 | 11:00
1001 | 0 | 12:00
1001 | 1 | 13:00
1001 | 1 | 14:00
The output that I need for the report is like this :
ID | ENTERTIME | EXITTIME
-------------------------
1001 | 8:00 | 10:00
1001 | 13:00 | 14:00
So far I only manage to get 1 row of result :
ID | ENTERTIME | EXITTIME
-------------------------
1001 | 8:00 | 14:00
You can use the window function to create an ad-hoc Grp
Example
Select ID
,TimeIn = min(Time)
,TimeOut = max(Time)
From (
Select *
,Grp = sum(case when flag_location=0 then 1 else 0 end ) over (partition by id order by time)
From YourTable
) A
Where Flag_Location=1
Group By ID,Grp
Returns
ID TimeIn TimeOut
1001 08:00:00.0000000 10:00:00.0000000
1001 13:00:00.0000000 14:00:00.0000000
If it helps with the visualization, the nested query generates the following:
You can just bucket the to identify group by and do group by as below:
;with cte as (select *, bucket = sum(case when flag_location = 0 then 1 when flag_location = 1 and nextflag = 0 then 2 else 0 end) over (partition by id order by [time]),
[time] as endtime from
(
select *,
lag(flag_location) over(partition by id order by [time]) nextflag
from #table4
) a
)
select id, min([time]), max([time]) from cte
where flag_location = 1
group by id, bucket
Query results:
+------+------------------+------------------+
| id | Entertime | ExitTime |
+------+------------------+------------------+
| 1001 | 08:00:00.0000000 | 10:00:00.0000000 |
| 1001 | 13:00:00.0000000 | 14:00:00.0000000 |
+------+------------------+------------------+
Try below query (explanations in code)
declare #tbl table (ID int, Flag_Location bit, Time varchar(5));
insert into #tbl values
(1001,1,'8:00'),
(1001,1,'9:00'),
(1001,1,'10:00'),
(1001,0,'11:00'),
(1001,0,'12:00'),
(1001,1,'13:00'),
(1001,1,'14:00');
select ID,
cast(max(ts) as varchar(10)),
cast(min(ts) as varchar(10))
from (
select ID, ts, Flag_Location,
row_number() over (order by ts) -
row_number() over (partition by Flag_Location order by ts) grp
from (
select *,
-- add 0 at the beginning for correct cast and cast it to timestamp for correct ordering
cast(right('00000' + time, 5) as timestamp) ts
from #tbl
) a
) a where Flag_Location = 1
group by ID, grp

How can I sum durations grouped by overlapping times in SQL Server

I am trying to create a stored proc in SQL Server 2008.
I have a "Timings" Table (which could have thousands of records):
StaffID | MachineID | StartTime | FinishTime
1 | 1 | 01/01/2018 12:00 | 01/01/18 14:30
2 | 1 | 01/01/2018 12:00 | 01/01/18 13:00
3 | 2 | 01/01/2018 12:00 | 01/01/18 13:00
3 | 2 | 01/01/2018 13:00 | 01/01/18 14:00
4 | 3 | 01/01/2018 12:00 | 01/01/18 12:30
5 | 3 | 01/01/2018 11:00 | 01/01/18 13:30
This shows how long each staff member was working on each machine.
I would like to produce a results table as below:
MachineID | StaffQty | TotalMins
1 | 1 | 90
1 | 2 | 60
2 | 1 | 120
3 | 1 | 120
3 | 2 | 30
This would show how many minutes each machine had only one person using it, how many minutes each machine had 2 people using it etc.
Normally, I would post what I have tried so far, but all my attempts seem to be so far away, I don't think there is much point.
Obviously, I would be very grateful of a complete solution but I would also appreciate even just a little nudge in the right direction.
I think this answers your question:
declare #t table (StaffID int, MachineID int, StartTime datetime2,FinishTime datetime2)
insert into #t(StaffID,MachineID,StartTime,FinishTime) values
(1,1,'2018-01-01T12:00:00','2018-01-01T14:30:00'),
(2,1,'2018-01-01T12:00:00','2018-01-01T13:00:00'),
(3,2,'2018-01-01T12:00:00','2018-01-01T12:30:00')
;With Times as (
select MachineID,StartTime as Time from #t
union
select MachineID,FinishTime from #t
), Ordered as (
select
*,
ROW_NUMBER() OVER (PARTITION BY MachineID ORDER BY Time) rn
from Times
), Periods as (
select
o1.MachineID,o1.Time as StartTime,o2.Time as FinishTime
from
Ordered o1
inner join
Ordered o2
on
o1.MachineID = o2.MachineID and
o1.rn = o2.rn - 1
)
select
p.MachineID,
p.StartTime,
MAX(p.FinishTime) as FinishTime,
COUNT(*) as Cnt,
DATEDIFF(minute,p.StartTime,MAX(p.FinishTime)) as TotalMinutes
from
#t t
inner join
Periods p
on
p.MachineID = t.MachineID and
p.StartTime < t.FinishTime and
t.StartTime < p.FinishTime
group by p.MachineID,p.StartTime
Results:
MachineID StartTime FinishTime Cnt TotalMinutes
----------- --------------------------- --------------------------- ----------- ------------
1 2018-01-01 12:00:00.0000000 2018-01-01 13:00:00.0000000 2 60
1 2018-01-01 13:00:00.0000000 2018-01-01 14:30:00.0000000 1 90
2 2018-01-01 12:00:00.0000000 2018-01-01 12:30:00.0000000 1 30
Hopefully you can see what each of the CTEs is doing. The only place where this may not give you exactly the results you're seeking is if one person's FinishTime is precisely equal to another person's StartTime on the same machine. Should be rare in real data hopefully.
For Sql server 2012+,
Please mention your Sql server version.
Try my script with other sample data.
Please post other sample data if it is not working.
I think my script can be fix for other Test scenario.
create table #temp(StaffID int,MachineID int,StartTime datetime,FinishTime datetime)
insert into #temp VALUES
(1, 1,'01/01/2018 12:00','01/01/18 14:30')
,(2, 1,'01/01/2018 12:00','01/01/18 13:00')
,(3, 2,'01/01/2018 12:00','01/01/18 12:30')
;
WITH CTE
AS (
SELECT t.*
,t1.StaffQty
,datediff(MINUTE, t.StartTime, t.FinishTime) TotalMinutes
FROM #temp t
CROSS APPLY (
SELECT count(*) StaffQty
FROM #temp t1
WHERE t.machineid = t1.machineid
AND (
t.StartTime >= t1.StartTime
AND t.FinishTime <= t1.FinishTime
)
) t1
)
SELECT MachineID
,StaffQty
,TotalMinutes - isnull(LAG(TotalMinutes, 1) OVER (
PARTITION BY t.MachineID ORDER BY t.StartTime
,t.FinishTime
), 0)
FROM cte t
 
drop table #temp
for Sql server 2008,
;
WITH CTE
AS (
SELECT t.*
,t1.StaffQty
,datediff(MINUTE, t.StartTime, t.FinishTime) TotalMinutes
,ROW_NUMBER() OVER (
PARTITION BY t.machineid ORDER BY t.StartTime
,t.FinishTime
) rn
FROM #temp t
CROSS APPLY (
SELECT count(*) StaffQty
FROM #temp t1
WHERE t.machineid = t1.machineid
AND (
t.StartTime >= t1.StartTime
AND t.FinishTime <= t1.FinishTime
)
) t1
)
SELECT t.MachineID
,t.StaffQty
,t.TotalMinutes - isnull(t1.TotalMinutes, 0) TotalMinutes
FROM cte t
OUTER APPLY (
SELECT TOP 1 TotalMinutes
FROM cte t1
WHERE t.MachineID = t1.machineid
AND t1.rn < t.rn
ORDER BY t1.rn DESC
) t1

SQL Server 2014 Merging Overlapping Date Ranges

I have a table with 200.000 rows in a SQL Server 2014 database looking like this:
CREATE TABLE DateRanges
(
Contract VARCHAR(8),
Sector VARCHAR(8),
StartDate DATE,
EndDate DATE
);
INSERT INTO DateRanges (Contract, Sector, StartDate, Enddate)
SELECT '111', '999', '01-01-2014', '03-31-2014'
union
SELECT '111', '999', '04-01-2014', '06-30-2014'
union
SELECT '111', '999', '07-01-2014', '09-30-2014'
union
SELECT '111', '999', '10-01-2014', '12-31-2014'
union
SELECT '111', '888', '08-01-2014', '08-31-2014'
union
SELECT '111', '777', '08-15-2014', '08-31-2014'
union
SELECT '222', '999', '01-01-2014', '03-31-2014'
union
SELECT '222', '999', '04-01-2014', '06-30-2014'
union
SELECT '222', '999', '07-01-2014', '09-30-2014'
union
SELECT '222', '999', '10-01-2014', '12-31-2014'
union
SELECT '222', '666', '11-01-2014', '11-30-2014'
UNION
SELECT '222', '555', '11-15-2014', '11-30-2014';
As you can see there can be multiple overlaps for each contract and what I would like to have is the result like this
Contract Sector StartDate EndDate
---------------------------------------------
111 999 01-01-2014 07-31-2014
111 888 08-01-2014 08-14-2014
111 777 08-15-2014 08-31-2014
111 999 09-01-2014 12-31-2014
222 999 01-01-2014 10-31-2014
222 666 11-01-2014 11-14-2014
222 555 11-15-2014 11-30-2014
222 999 12-01-2014 12-31-2014
I can not figure out how this can be done and the examples i have seen on this site quite do not fit my problem.
This answer makes use of a few different techniques. The first is a recursive-cte that creates a table with every relevant cal_date which then gets cross apply'd with unique Contract values to get every combination of both values. The second is window-functions such as lag and row_number to determine a variety of things detailed in the comments below. Lastly, and probably most importantly, gaps-and-islands to determine when one Contract/Sector combination ends and the next begins.
Answer:
--determine range of dates
declare #bgn_dt date = (select min(StartDate) from DateRanges)
, #end_dt date = (select max(EndDate) from DateRanges)
--use a recursive CTE to create a record for each day / Contract
; with dates as
(
select #bgn_dt as cal_date
union all
select dateadd(d, 1, a.cal_date) as cal_date
from dates as a
where a.cal_date < #end_dt
)
select d.cal_date
, c.Contract
into #contract_dates
from dates as d
cross apply (select distinct Contract from DateRanges) as c
option (maxrecursion 0)
--Final Select
select f.Contract
, f.Sector
, min(f.cal_date) as StartDate
, max(f.cal_date) as EndDate
from (
--Use the sum-over to obtain the Island Numbers
select dr.Contract
, dr.Sector
, dr.cal_date
, sum(dr.IslandBegin) over (partition by dr.Contract order by dr.cal_date asc) as IslandNbr
from (
--Determine if the record is the start of a new Island
select a.Contract
, a.Sector
, a.cal_date
, case when lag(a.Sector, 1, NULL) over (partition by a.Contract order by a.cal_date asc) = a.Sector then 0 else 1 end as IslandBegin
from (
--Determine which Contract/Date combinations are valid, and rank the Sectors that are in effect
select cd.cal_date
, dr.Contract
, dr.Sector
, dr.EndDate
, row_number() over (partition by dr.Contract, cd.cal_date order by dr.StartDate desc) as ConractSectorRnk
from #contract_dates as cd
left join DateRanges as dr on cd.Contract = dr.Contract
and cd.cal_date between dr.StartDate and dr.EndDate
) as a
where a.ConractSectorRnk = 1
and a.Contract is not null
) as dr
) as f
group by f.Contract
, f.Sector
, f.IslandNbr
order by f.Contract asc
, min(f.cal_date) asc
Output:
+----------+--------+------------+------------+
| Contract | Sector | StartDate | EndDate |
+----------+--------+------------+------------+
| 111 | 999 | 2014-01-01 | 2014-07-31 |
| 111 | 888 | 2014-08-01 | 2014-08-14 |
| 111 | 777 | 2014-08-15 | 2014-08-31 |
| 111 | 999 | 2014-09-01 | 2014-12-31 |
| 222 | 999 | 2014-01-01 | 2014-10-31 |
| 222 | 666 | 2014-11-01 | 2014-11-14 |
| 222 | 555 | 2014-11-15 | 2014-11-30 |
| 222 | 999 | 2014-12-01 | 2014-12-31 |
+----------+--------+------------+------------+

Extending a Pivot

I have a Documents table and an Events table.
Documents table has ID and a bunch of other fields not relevant to
this question.
Events table has DocID, EventType, EventDate, and UserID.
A document may have zero or more Events of any of these EventTypes:
0 = Created
1 = Modified
2 = Submitted
3 = Approved
DocID | EventType | EventDate | UserID
-----------------------------------------------
1 | 0 | 1-2-2017 | 123
1 | 1 | 1-3-2017 | 456
1 | 1 | 1-4-2017 | 489
1 | 2 | 1-5-2017 | 357
2 | 0 | 1-6-2017 | 951
2 | 1 | 1-7-2017 | 654
2 | 2 | 1-8-2017 | 654
2 | 3 | 1-9-2017 | 357
Pivoting the Events table is easy enough:
SELECT DocID, [0] AS CreatedDate, [1] AS ModifiedDate,
[2] AS SubmittedDate, [3] AS ApprovedDate
FROM (SELECT DocID, EventType, EventDate FROM Events
WHERE DocID IS NOT NULL AND EventDate IS NOT NULL) AS DocEvents
PIVOT (MAX(EventDate) FOR EventType IN ([0], [1], [2], [3]))
AS DocEventsPivot
For my purposes, the most recent event of a given type is wanted, thus the MAX aggregate:
DocID | CreatedDate | ModifiedDate | SubmittedDate | ApprovedDate
-----------------------------------------------------------------
1 | 1-2-2017 | 1-4-2017 | 1-5-2017 | NULL
2 | 1-6-2017 | 1-7-2017 | 1-8-2017 | 1-9-2017
How can I get the UserID translated to CreatedBy, ModifiedBy, SubmittedBy, and ApprovedBy to correspond to the dates of the appropriate EventType?
I will not know the possible values of UserID in advance.
Desired Output:
DocID | CreatedDate | ModifiedDate | SubmittedDate | ApprovedDate | CreatedBy | ModifiedBy | SubmittedBy | ApprovedBy
---------------------------------------------------------------------------------------------------------------------
1 | 1-2-2017 | 1-4-2017 | 1-5-2017 | NULL | 123 | 489 | 357 | NULL
2 | 1-6-2017 | 1-7-2017 | 1-8-2017 | 1-9-2017 | 951 | 654 | 654 | 657
Rather than using PIVOT another solution is using OUTER APPLY.
CREATE TABLE #Documents (ID int)
CREATE TABLE #Events (DocID int, EventType int, EventDate date, UserID int)
INSERT INTO #Documents VALUES
(1),
(2)
INSERT INTO #Events VALUES
(1, 0, '1-2-2017', 123),
(1, 1, '1-3-2017', 456),
(1, 1, '1-4-2017', 489),
(1, 2, '1-5-2017', 357),
(2, 0, '1-6-2017', 951),
(2, 1, '1-7-2017', 654),
(2, 2, '1-8-2017', 654),
(2, 3, '1-9-2017', 357)
SELECT
DOC.ID AS 'DocID',
CRT.EventDate AS 'CreatedDate',
MFY.EventDate AS 'ModifiedDate',
SUB.EventDate AS 'SubmittedDate',
APR.EventDate AS 'ApprovedDate',
CRT.UserID AS 'CreatedBy',
MFY.UserID AS 'ModifiedBy',
SUB.UserID AS 'SubmittedBy',
APR.UserID AS 'ApprovedBy'
FROM
#Documents AS DOC
OUTER APPLY (SELECT TOP 1 EventDate, UserID FROM #Events WHERE DocID = DOC.ID AND EventType = 0 ORDER BY EventDate DESC) AS CRT
OUTER APPLY (SELECT TOP 1 EventDate, UserID FROM #Events WHERE DocID = DOC.ID AND EventType = 1 ORDER BY EventDate DESC) AS MFY
OUTER APPLY (SELECT TOP 1 EventDate, UserID FROM #Events WHERE DocID = DOC.ID AND EventType = 2 ORDER BY EventDate DESC) AS SUB
OUTER APPLY (SELECT TOP 1 EventDate, UserID FROM #Events WHERE DocID = DOC.ID AND EventType = 3 ORDER BY EventDate DESC) AS APR
DROP TABLE #Documents
DROP TABLE #Events
Try the following
CREATE TABLE #Events (DocID int, EventType int, EventDate date, UserID int)
INSERT INTO #Events VALUES
(1, 0, '1-2-2017', 123),
(1, 1, '1-3-2017', 456),
(1, 1, '1-4-2017', 489),
(1, 2, '1-5-2017', 357),
(1, 2, '1-4-2017', 666),
(2, 0, '1-6-2017', 951),
(2, 1, '1-7-2017', 654),
(2, 2, '1-8-2017', 654),
(2, 3, '1-9-2017', 357)
SELECT
DocID,
MAX(CASE WHEN EventType=0 THEN EventDate END) [CreatedDate],
MAX(CASE WHEN EventType=1 THEN EventDate END) [ModifiedDate],
MAX(CASE WHEN EventType=2 THEN EventDate END) [SubmittedDate],
MAX(CASE WHEN EventType=3 THEN EventDate END) [ApprovedDate],
MAX(CASE WHEN EventType=0 THEN LastUserID END) [CreatedBy],
MAX(CASE WHEN EventType=1 THEN LastUserID END) [ModifiedBy],
MAX(CASE WHEN EventType=2 THEN LastUserID END) [SubmittedBy],
MAX(CASE WHEN EventType=3 THEN LastUserID END) [ApprovedBy]
FROM
(
SELECT
DocID,
EventDate,
EventType,
LAST_VALUE(UserID)OVER(
PARTITION BY DocID,EventType
ORDER BY EventDate
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) LastUserID
FROM #Events
) q
GROUP BY DocID
DROP TABLE #Events
Or you can use IFF instead CASE. I prefer use CASE because it isn't block ELSE
SELECT
DocID,
MAX(IIF(EventType=0,EventDate,NULL)) [CreatedDate],
MAX(IIF(EventType=1,EventDate,NULL)) [ModifiedDate],
MAX(IIF(EventType=2,EventDate,NULL)) [SubmittedDate],
MAX(IIF(EventType=3,EventDate,NULL)) [ApprovedDate],
MAX(IIF(EventType=0,LastUserID,NULL)) [CreatedBy],
MAX(IIF(EventType=1,LastUserID,NULL)) [ModifiedBy],
MAX(IIF(EventType=2,LastUserID,NULL)) [SubmittedBy],
MAX(IIF(EventType=3,LastUserID,NULL)) [ApprovedBy]
FROM
(
SELECT
DocID,
EventDate,
EventType,
LAST_VALUE(UserID)OVER(
PARTITION BY DocID,EventType
ORDER BY EventDate
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) LastUserID
FROM #Events
) q
GROUP BY DocID

Improve the query by removing the sub query

I have a Customer table
+--------+---------+
| Id | Name |
+--------+---------+
| 1 | A |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | 3 |
| 6 | f |
| 7 | g |
+--------+---------+
and an order table
+-----+------+--------------------------+
| ID | C_Id | OrderDate |
+-----+------+--------------------------+
| 1 | 1 | 2017-05-12 00:00:00.000 |
| 2 | 2 | 2017-12-12 00:00:00.000 |
| 3 | 3 | 2017-11-12 00:00:00.000 |
| 4 | 4 | 2017-12-12 00:00:00.000 |
| 5 | 1 | 2017-12-12 00:00:00.000 |
| 6 | 2 | 2017-12-12 00:00:00.000 |
| 7 | 3 | 2017-12-12 00:00:00.000 |
| 8 | 4 | 2017-11-12 00:00:00.000 |
| 9 | 2 | 2017-06-12 00:00:00.000 |
| 10 | 3 | 2017-07-12 00:00:00.000 |
+-----+------+--------------------------+
I need the result of the customers who did not buy in last month.
That is from the order table Customer 3 and 4 have purchased in last month(November). The result should not include customer 3 and 4 even they had purchase in earlier months.
I have this query which returns the result perfectly.
SELECT C_ID , MONTH(OrderDate) from [Order]
WHERE MONTH(OrderDate) <> MONTH(GETDATE()) - 1
AND C_ID NOT IN (
SELECT C_ID FROM [Order]
WHERE MONTH(OrderDate) = MONTH(GETDATE()) - 1)
Can anyone help me to write this query without using subquery
EDIT: For more clarity, I need to exclude the customers from the result(get all orders for current year) if they had any purchase in November, also I need results for this year alone.
I think you need to use another way for checking earlier purchases because with MONTH(OrderDate) <> MONTH(GETDATE()) - 1 you have a problem if purchases are in different years.
You need to expand your condition. For example
(
(MONTH(OrderDate)<MONTH(DATEADD(MONTH,-1,GETDATE())) AND YEAR(OrderDate)=YEAR(DATEADD(MONTH,-1,GETDATE())))
OR YEAR(OrderDate)<YEAR(DATEADD(MONTH,-1,GETDATE()))
)
Or you can use the EOMONTH function (from SQL Server 2012) for it. I think this variant will be more useful.
SELECT
C_ID,
MONTH(OrderDate)
FROM [Order]
WHERE EOMONTH(OrderDate)<EOMONTH(DATEADD(MONTH,-1,GETDATE())) -- check for month and year
AND C_ID NOT IN (
SELECT C_ID FROM [Order]
WHERE EOMONTH(OrderDate)=EOMONTH(DATEADD(MONTH,-1,GETDATE())) -- check for month and year
)
I think here is more useful using a variable
DECLARE #lastMonth date=EOMONTH(DATEADD(MONTH,-1,GETDATE()))
SELECT
C_ID,
MONTH(OrderDate)
FROM [Order]
WHERE EOMONTH(OrderDate)<#lastMonth -- check for month and year
AND C_ID NOT IN (
SELECT C_ID FROM [Order]
WHERE EOMONTH(OrderDate)=#lastMonth -- check for month and year
)
A variant without a subquery
SELECT
C_ID,
MIN(OrderDate) FirstOrderDate,
MAX(OrderDate) LastOrderDate
FROM [Order]
WHERE OrderDate<=EOMONTH(DATEADD(MONTH,-1,GETDATE()))
GROUP BY C_ID
HAVING EOMONTH(MAX(OrderDate))<EOMONTH(DATEADD(MONTH,-1,GETDATE()))
Or
DECLARE #lastMonth date=EOMONTH(DATEADD(MONTH,-1,GETDATE()))
SELECT
C_ID,
MIN(OrderDate) FirstOrderDate,
MAX(OrderDate) LastOrderDate
FROM [Order]
WHERE OrderDate<=#lastMonth
GROUP BY C_ID
HAVING EOMONTH(MAX(OrderDate))<#lastMonth
But here I return only MIN(OrderDate) and MAX(OrderDate) but maybe it'll suit you.
I don't think that a variant with a subquery is worse. I think it's more clearly.
DECLARE #lastMonth date=EOMONTH(DATEADD(MONTH,-1,GETDATE()))
SELECT
C_ID,
YEAR(OrderDate) [Year],
MONTH(OrderDate) [Month]
COUNT(ID) OrderCount
FROM [Order]
WHERE EOMONTH(OrderDate)<#lastMonth
--AND YEAR(OrderDate)=YEAR(#lastMonth) -- if you need only orders from this year
AND C_ID IN(
SELECT DISTINCT C_ID
FROM [Order]
WHERE EOMONTH(OrderDate)=#lastMonth
)
GROUP BY C_ID,YEAR(OrderDate),MONTH(OrderDate)
When working with dated information you cannot just use a month number because month 1 comes after month 12 (of last year). So, work with dates, not month numbers.
For this query we need "this month" and "last month" by their dates (not the month numbers) and we can start with this by using getdate()
A useful "trick" here is to calculate the first day of this month which we can do by calculating the number of months from zero datediff(month,0, getdate() ) then adding that number to zero dateadd(month, ..., 0). So once we have the first of this month it is easy to calculate first of last month and first of next month simply by subtracting or adding 1 month.
So, for a solution that will work in any version of SQL Server:
SQL Fiddle
MS SQL Server 2014 Schema Setup:
CREATE TABLE Orders
([ID] int, [C_Id] int, [OrderDate] datetime)
;
INSERT INTO Orders
([ID], [C_Id], [OrderDate])
VALUES
(1, 1, '2017-05-12 00:00:00'),
(2, 2, '2017-12-12 00:00:00'),
(3, 3, '2017-11-12 00:00:00'),
(4, 4, '2017-12-12 00:00:00'),
(5, 1, '2017-12-12 00:00:00'),
(6, 2, '2017-12-12 00:00:00'),
(7, 3, '2017-12-12 00:00:00'),
(8, 4, '2017-11-12 00:00:00'),
(9, 2, '2017-06-12 00:00:00'),
(10, 3, '2017-07-12 00:00:00')
;
CREATE TABLE Customers
([Id] int, [Name] varchar(1))
;
INSERT INTO Customers
([Id], [Name])
VALUES
(1, 'A'),
(2, 'b'),
(3, 'c'),
(4, 'd'),
(5, '3'),
(6, 'f'),
(7, 'g')
;
Query 1:
declare #this_month datetime = dateadd(month, datediff(month,0, getdate() ), 0)
declare #last_month datetime = dateadd(month,-1,#this_month)
select
c.Id
, c.name
, count(case when o.OrderDate >= #last_month and o.OrderDate < #this_month then 1 end) last_month
, count(case when o.OrderDate >= #this_month then 1 end) this_month
from customers c
LEFT join orders o on c.id = o.c_id
and OrderDate >= #last_month
and OrderDate < dateadd(month,1,#this_month)
group by c.Id, c.name
having count(case when o.OrderDate >= #last_month and o.OrderDate < #this_month then 1 end) = 0
and count(case when o.OrderDate >= #this_month then 1 end) > 0
Results:
| Id | name | last_month | this_month |
|----|------|------------|------------|
| 1 | A | 0 | 1 |
| 2 | b | 0 | 2 |
----
declare #this_year datetime = dateadd(year, datediff(year,0, getdate() ), 0)
declare #this_month datetime = dateadd(month, datediff(month,0, getdate() ), 0)
declare #last_month datetime = dateadd(month,-1,#this_month)
select
c.Id
, c.name
, count(case when o.OrderDate >= #last_month and o.OrderDate < #this_month then 1 end) last_month
, count(o.OrderDate) this_year
from customers c
LEFT join orders o on c.id = o.c_id
and OrderDate >= #this_year
and OrderDate < dateadd(year,1,#this_year)
group by c.Id, c.name
having count(case when o.OrderDate >= #last_month and o.OrderDate < #this_month then 1 end) = 0
and count(o.OrderDate) > 0
;

Resources