Compare multiple dates on one table with multiple dates in another - sql-server

I'm using T-SQL in SSMS 2016. For a report, I want to compare two tables to see if the date ranges in table2 cover the date ranges in table1, then return the rows in table1 that are not fully covered by the date ranges in table2.
The number of entries in table1 and table2 will grow in number over time.
table1 table2
id start date end_date id start date end date
----------------------------- ----------------------------
1001 01/08/17 31/08/17 1001 07/07/17 02/09/17
1001 01/10/17 31/10/17 1001 01/11/17 12/12/17
1001 01/11/17 30/11/17
1001 01/01/18 05/01/18

Question 1 Query Answer
-- Does Table2 cover the date ranges in table1? Result is by each date in Table2.
DECLARE #t1_minStartDate DATE,
#t1_maxEndDate DATE
SELECT #t1_minStartDate = MIN(startDate),
#t1_maxEndDate = MAX(endDate)
FROM table1
SELECT startDate AS t2_startDate,
CASE WHEN startDate < #t1_minStartDate THEN
'Yes'
ELSE
'No'
END AS t2_startDate_covers_all,
endDate AS t2_endDate,
CASE WHEN startDate > #t1_maxEndDate THEN
'Yes'
ELSE
'No'
END AS t2_endDate_covers_all
FROM table2 t2
Result:
t2_startDate t2_startDate_covers_all t2_endDate t2_endDate_covers_all
-------------------------------------------------------------------------
2017-07-07 Yes 2017-09-02 No
2017-11-01 No 2017-12-12 No
Question 2 Query Answer
-- Rows in Table1 that are not fully covered by the date ranges in table2
SELECT t1.id,
t1.startDate,
t1.endDate
FROM table1 t1
WHERE NOT EXISTS (SELECT 1
FROM table2 t2
WHERE t1.startDate BETWEEN t2.startDate AND t2.endDate
AND t1.endDate BETWEEN t2.startDate AND t2.endDate)
Result:
id startDate endDate
----------------------------------
2 2017-10-01 2017-10-31
4 2018-01-01 2018-01-05

Related

SQL Query - Given two dates, repeat the records of a table

I need to make a query
I have two tables, TableA and TableB
TableA is as following:
Id
Name
1
Car
2
Plane
3
Ship
4
Bike
5
Skate
And my TableB is like the following:
ID
MyDate
FID
1
29-08-2021
1
2
29-08-2021
2
3
29-08-2021
3
4
30-08-2021
1
5
30-08-2021
5
FID: is the column that relates TableB to TableA.
So, I need to show every record of TableA, with the records of TableB.
I know how to do it with a single Date. Some Like this:
SELECT ID, NAME, MyDATE
FROM TableA
LEFT JOIN TableB ON FID = ID AND MyDate = SomeDate
ORDER BY ID
If SomeDate = '29-08-2021'
That query should return the following result:
ID
Name
MyDate
1
Car
29-08-2021
2
Plane
29-08-2021
3
Ship
29-08-2021
4
Bicicle
NULL
5
Skate
NULL
If SomeDate = '30-08-2021', It should show the following
ID
Name
MyDate
1
Car
30-08-2021
2
Plane
NULL
3
Ship
NULL
4
Bicicle
NULL
5
Skate
30-08-2021
So, What I need is to give two dates, and print Everything in that range of dates, all the records of TableA with their corresponding records of TableB
So if I give the Dates 28-08-2021 to 30-08-2021, the result should be like the following
ID
Name
MyDate
1
Car
NULL
2
Plane
NULL
3
Ship
NULL
4
Bicicle
NULL
5
Skate
NULL
1
Car
29-08-2021
2
Plane
29-08-2021
3
Ship
29-08-2021
4
Bicicle
NULL
5
Skate
NULL
1
Car
30-08-2021
2
Plane
NULL
3
Ship
NULL
4
Bicicle
NULL
5
Skate
30-08-2021
I need to make a query that returns this result set.
The first 5 records, are the records corresponding to 28-09-2021, because there are no recoreds in TableB with that date, it returns null.
Because there are three dates, it should repeat all the records of table A three times.
I tried to do this
SELECT ID, NAME, MyDATE
FROM TableA
LEFT JOIN TableB ON FID = ID AND MyDate BETWEEN StartDate AND EndDate
ORDER BY ID
But doing that only returns 5 records (the first 3 with the date 29-08-2021, and the second 2 with the date 30-08-201).
I need to know how to do a query that returns result set I provided.
What you seem to want to do is get the cartesian product of TableA with all the dates in the date range. And then add information on each row as to if there is a corresponding row in your TableB.
You can start with a simple recursive SQL, that gets your dates:
WITH cte_dates(theDates)
AS (
SELECT #startdate
UNION ALL
SELECT DATEADD(day, 1, thedates)
FROM cte_dates
WHERE thedates < #enddate
)
SELECT *
FROM cte_dates;
This will result in:
thedates
2021-08-28
2021-08-29
2021-08-30
The variable declarations are:
DECLARE #startdate DATE;
DECLARE #enddate DATE;
SET #startdate = '2021-08-28';
SET #enddate = '2021-08-30';
Then join that with your TableA using WHERE 1 = 1 to get the cartesian product:
WITH cte_dates(thedates)
AS (
SELECT #startdate
UNION ALL
SELECT DATEADD(day, 1, thedates)
FROM cte_dates
WHERE thedates < #enddate
)
SELECT *
FROM TableA
JOIN cte_dates ON (1 = 1)
;
This will give you 15 rows where each row in TableA has an extra column with the date. Each original row in TableA will result in 3 rows in this result set, since there are 3 dates.
Now you can use a outer left join to get the corresponding TableB values:
WITH cte_dates(thedates)
AS (
SELECT #startdate
UNION ALL
SELECT DATEADD(day, 1, thedates)
FROM cte_dates
WHERE thedates < #enddate
)
SELECT ta.id, ta.name, tb.mydate
FROM TableA AS ta
JOIN cte_dates AS dat ON (1 = 1)
LEFT JOIN TableB AS tb ON tb.fid = ta.id
AND tb.mydate = dat.thedates
;
dbfiddle

If Value is present in two consecutive months , display only one month in sql

I would want to check ID in consecutive months, IF Same ID is present in two consecutive months then consider that ID only for 1st month.
If ID's are not in consecutive month then show the distinct ID's grouped by start date month.(We consider only start date)
For example, ID 1 is present in start date months january and Feb , then Distinct count of this ID will be 1 in Jan, how ever ID 2 and 3 are
present in Jan and March and Feb and May Resp, now I would like to see this distinct count of ID in Jan and March.
Current Data
Table1:
ID StartDate EndDate
1 2017-01-12 2017-01-28
1 2017-01-19 2017-01-28
1 2017-01-29 2017-02-11
1 2017-02-01 2017-02-11
1 2017-02-19 2017-02-24
2 2017-01-12 2017-01-28
2 2017-01-19 2017-01-28
2 2017-03-09 2017-03-20
3 2017-02-12 2017-02-28
3 2017-02-19 2017-02-28
3 2017-05-05 2017-05-29
3 2017-05-09 2017-05-29
I tried with below logic bt I know I am missing on something here.
select t.* from Table1 t
join Table1 t t1
on t1.ID=t.ID
and datepart(mm,t.StartDate)<> datepart(mm,t1.StartDate)+1
Expected Result:
DistinctCount StartDateMonth(In Numbers)
1 1(Jan)
2 1(Jan)
2 3(March)
3 2(Feb)
3 5(May)
Any help is appreciated!
Here's my solution. The thinking for this is:
1) Round all the dates to the first of the month, then work with the distinct dataset of (ID, StartDateRounded). From your dataset, the result should look like this:
ID StartDateRounded
1 2017-01-01
1 2017-02-01
2 2017-01-01
2 2017-03-01
3 2017-02-01
3 2017-05-01
2) From this consolidated dataset, find all records by ID that do not have a record for the previous month (which means it's not a consecutive month and thus is a beginning of a new data point). This is your final dataset
with DatesTable AS
(
SELECT DISTINCT ID
,DATEADD(month,DateDiff(month,0,StartDate),0) StartDateRounded
,DATEADD(month,DateDiff(month,0,StartDate)+1,0) StartDateRoundedPlusOne
FROM Table1
)
SELECT t1.ID, DatePart(month,t1.StartDateRounded) AS StartDateMonth
FROM DatesTable t1
LEFT JOIN DatesTable t2
ON t1.ID = t2.ID
AND t1.StartDateRounded = t2.StartDateRoundedPlusOne
WHERE t2.ID IS NULL; --Verify no record exists for prior month
sqlfiddler for reference. Let me know if this helps
Just need to take advantage of the lag on the inner query to compare values between rows, and apply the logic in question on the middle query, and then do a final select.
/*SAMPLE DATA*/
create table #table1
(
ID int not null
, StartDate date not null
, EndDate date null
)
insert into #table1
values (1, '2017-01-12', '2017-01-28')
, (1, '2017-01-19', '2017-01-28')
, (1, '2017-01-29', '2017-02-11')
, (1, '2017-02-01', '2017-02-11')
, (1, '2017-02-19', '2017-02-24')
, (2, '2017-01-12', '2017-01-28')
, (2, '2017-01-19', '2017-01-28')
, (2, '2017-03-09', '2017-03-20')
, (3, '2017-02-12', '2017-02-28')
, (3, '2017-02-19', '2017-02-28')
, (3, '2017-05-05', '2017-05-29')
, (3, '2017-05-09', '2017-05-29')
/*ANSWER*/
--Final Select
select c.ID
, c.StartDateMonth
from (
--Compare record values to rule a record in/out based on OP's logic
select b.ID
, b.StartDateMonth
, case when b.StartDateMonth = b.StartDateMonthPrev then 0 --still the same month?
when b.StartDateMonth = b.StartDateMonthPrev + 1 then 0 --immediately prior month?
when b.StartDateMonth = 1 and b.StartDateMonthPrev = 12 then 0 --Dec/Jan combo
else 1
end as IncludeFlag
from (
--pull StartDateMonth of previous record into current record
select a.ID
, datepart(mm, a.StartDate) as StartDateMonth
, lag(datepart(mm, a.StartDate), 1, NULL) over (partition by a.ID order by a.StartDate asc) as StartDateMonthPrev
from #table1 as a
) as b
) as c
where 1=1
and c.IncludeFlag = 1
Output:
+----+----------------+
| ID | StartDateMonth |
+----+----------------+
| 1 | 1 |
| 2 | 1 |
| 2 | 3 |
| 3 | 2 |
| 3 | 5 |
+----+----------------+
Try the below query,
SELECT ID,MIN(YEARMONTH) AS YEARMONTH
FROM (
SELECT ID
,YEAR([StartDate])*100+MONTH([StartDate]) AS YEARMONTH
,LAG(YEAR([StartDate])*100+MONTH([StartDate]))
OVER(ORDER BY ID) AS PREVYEARMONTH
,ROW_NUMBER() OVER(ORDER BY ID) AS ROW_NO
FROM #Table1
GROUP BY ID,((YEAR([StartDate])*100)+MONTH([StartDate]))
) AS T
GROUP BY ID
,(CASE WHEN YEARMONTH - PREVYEARMONTH > 1 THEN ROW_NO ELSE 0 END)
ORDER BY ID
Output:
ID YEARMONTH
1 201701
2 201701
2 201703
3 201702
3 201705
Thank you all guys. most of the logic seemed to work..but I tried just with below one and I Was good with thiis.
SELECT t1.ID, DatePart(month,t1.Startdate) AS StartDateMonth
FROM DatesTable t1
LEFT JOIN DatesTable t2
ON t1.ID = t2.ID
AND DatePart(month,t1.Startdate) = DatePart(month,t2.Startdate)+1
WHERE t2.ID IS NULL;
Thanks again
Ok, I wrote my first query without checking, believed that will work correctly. This is my updated version, should be faster than other solutions
select
id
, min(st)%12 --this will return start month
, min(st)/12 + 1 --this will return year, just in case if you need it
from (
select
id, st, gr = st - row_number() over (partition by ID order by st)
from (
select
distinct ID, st = (year(StartDate) - 1) * 12 + month(StartDate)
from
#table2
) t
) t
group by id, gr

Understanding GroupBY

I have a table in SQL Server where personal entries and exits are recorded.
There are multiple hours of entry or exit on the same day. What I need is to recover the first entry and the last exit of the day.
Date hour Clock
------------------------
01/01/2017 09:00 1
01/01/2017 11:30 2
01/01/2017 17:00 2
02/01/2017 7:59 1
02/01/2017 16:00 1
I have this SQL query that works correctly.
SELECT
d.Date,
MIN(d.hour) as Entry,
MAX(dt.hour) as Exit
FROM
#temp1 AS d
LEFT JOIN
#temp1 AS dt ON d.Date = dt.Date
GROUP BY
d.Date
ORDER BY
Date DESC
BUT if I add 2 more columns to the query
SELECT
d.Date,
d.clock as ClockEntry, -- Aggregated column to display
MIN(d.hour) as Entry,
dt.clock as ClockExit, -- Aggregated column to display
MAX(dt.hour) as Exit
FROM
#temp1 AS d
LEFT JOIN
#temp1 AS dt ON d.Date = dt.Date
GROUP BY
d.Date
ORDER BY
Date DESC
I get this error:
Column '# temp1.clock' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
I just need to group by the field "date", I do not want to add more conditions to the GROUP BY.. How could I solve it?
I want this result
DATE ClockEntry Entry ClockExit Exit
-------------------------------------------------------
01/01/2017 1 09:00 2 17:00
02/01/2017 1 7:59 1 16:00
So there is an easy way to do this - use 2 ranking functions:
Partitioned by date, Ordered by the hour ascending
Partitioned by date, Ordered by the hour descending
At that point the rows can be joined where they both have a value of 1, and the dates match.
I tend to use a CTE for this:
with temp2 (MinId, MaxId, Date, Hour, Clock)
AS
(
select ROW_NUMBER() Over (partition by date order by hour),
ROW_NUMBER() Over (partition by date order by hour desc),
*
from temp1
)
select distinct
d1.Date,
d1.Clock,
d1.Hour,
d2.Clock,
d2.Hour
FROM temp2 d1
LEFT JOIN temp2 d2
ON d1.Date = d2.Date -- dates match
AND d1.MinId=d2.MaxId -- minId=earliest record MaxId=latest record
WHERE d1.MinId=1
GROUP BY Method
If you know that only a single value will get out the clock column,
just aggregate the values with a MAX or MIN aggregate function for example like this :
SELECT
d.Date,
MIN(d.clock) as ClockEntry,
MIN(d.hour) as Entry,
MAX(dt.clock) as ClockExit,
MAX(dt.hour) as Exit
FROM #temp1 AS d
LEFT JOIN #temp1 AS dt
ON d.Date= dt.Date
GROUP BY d.Date
order by Date desc
Or if you have multiple clock values and want to see them all,
add them to the GROUP BY statement :
SELECT
d.Date,
d.clock as ClockEntry,
MIN(d.hour) as Entry,
dt.clock as ClockExit,
MAX(dt.hour) as Exit
FROM #temp1 AS d
LEFT JOIN #temp1 AS dt
ON d.Date= dt.Date
GROUP BY d.Date, d.clock, dt.clock
order by Date desc
ORDER BY Method
Use a cursor or a Common Table Expression for each date to get both first entry and last exit.
First Entry for a given date
SELECT TOP 1 d.date, d.clock as ClockEntry, d.hour as Entry
FROM #temp1 AS d
WHERE d.date = #myDate
ORDER BY d.hour ASC
Last Exit for a given date
SELECT TOP 1 d.date, d.clock as ClockExit, d.hour as Exit
FROM #temp1 AS d
WHERE d.date = #myDate
ORDER BY d.hour DESC
Reference :
GROUP BY Documentation
AGGREGATE FUNCTIONS

Update table with overlap date range and change status

I have a table with following column and I would like to update it as following.
The Logic is the start date take the date will be updated if overlap with following rules: take the earliest start date and enddate of the latest row with overlapping date based on member id. And the status of the remaining overlap column will be updated to 2. Hope someone could help.
ID MemberID StartDate EndDate Status
1 2 2015-01-01 2015-02-28 1
2 2 2015-02-01 2015-02-03 1
3 2 2015-02-01 2015-03-01 1
4 1 2015-02-01 2015-02-28 1
5 3 2015-02-01 2015-02-28 1
6 2 2015-05-01 2015-05-20 1
I would like to update to
ID MemberID StartDate EndDate Status
1 2 2015-01-01 2015-03-01 1
2 2 2015-01-01 2015-03-01 2
3 2 2015-01-01 2015-03-01 2
4 1 2015-02-01 2015-02-28 1
5 3 2015-02-01 2015-02-28 1
6 2 2015-05-01 2015-05-20 1
I think this should do it :
update a set
a.startdate =
(select min(startdate) from #table where memberID = a.memberID),
a.enddate =
(select max(enddate) from #table where memberID = a.memberID),
a.status =
case when a.id =
(select min(id) from #table where memberID = a.memberID)
then status else 2
end
from #table a
Try this,
---- Creating CTE for finding overlapped dates
;WITH CTE AS (
SELECT A.ID,
B.ID AS MAPPED_ID,
A.MEMBERID,
B.STARTDATE,
B.ENDDATE,
B.STATUS
FROM #YOUR_TABLE A
JOIN #YOUR_TABLE B ON B.STARTDATE <= A.ENDDATE-- Condition for finding the overlapped dates
AND B.ENDDATE >= A.STARTDATE
AND A.MEMBERID = B.MEMBERID)-- end here
UPDATE T
SET T.STARTDATE = A.STARTDATE,
T.ENDDATE = A.ENDDATE,
T.STATUS = A.STATUS
FROM #YOUR_TABLE T
JOIN (SELECT ID,
MEMBERID,
STARTDATE,
ENDDATE,
STATUS=CASE
WHEN RN > 1 THEN 2
ELSE 1
END
FROM (SELECT T.ID,
T.MEMBERID,
CS1.STARTDATE,
CS2.ENDDATE,
ROW_NUMBER() -- ROWNUMBER FOR FINDING THE STATUS
OVER(
PARTITION BY T.MEMBERID, CS1.STARTDATE, CS2.ENDDATE
ORDER BY T.ID) AS RN
FROM #YOUR_TABLE T
CROSS APPLY (SELECT CAST(MIN(STARTDATE)AS DATETIME) AS STARTDATE --- FINDING MIN(STARTDATE) FOR THE OVERLAPPED GROUP
FROM CTE A
WHERE A.ID = T.ID) CS1
CROSS APPLY (SELECT ENDDATE -- FINDING LAST ENDDATE FOR THE OVERLAPPED GROUP (IE RN=1)
FROM (SELECT ENDDATE,--- ROW_NUMBER FOR THE OVERLAPPED GROUPS
ROW_NUMBER()
OVER(
ORDER BY B.MAPPED_ID DESC) AS RN
FROM CTE B
WHERE B.ID = T.ID)A
WHERE A.RN = 1)CS2)A)A ON A.ID = T.ID
SELECT *
FROM #YOUR_TABLE

How to Count number of days?

Using SQL Server 2005
Table1
ID FromDate ToDate
001 23-02-2009 25-02-2009
001 27-02-2009 29-02-2009
002 12-02-2009, 25-03-2009
...,
Table2
ID Name Total
001 Raja 30
002 Ravi 22
I want to get total day for the personid
Tried Query,
SELECT
table2.Id, table2.name, table2.total,
datediff(day, table1.fromdate, table2.todate)
FROM table1
LEFT OUTER JOIN table2 ON table1.personid = table2.personid
Getting output
ID Name Total Days
001 Raja 30 3
001 Raja 30 3
...,
It should total the days and it should display in one line,
Note: Suppose I am selecting the particular period date means it should display that days only
For example
where date between 26-02-2009 to 03-03-2009, It should display
ID Name Total Days
001 Raja 30 3
...,
Because am taking date after 25-02-2009,
Expected Output
ID Name Total Days
001 Raja 30 6
002 Ravi 22 16
How to modify my query?
DATEDIFF gives the number of days difference between two dates, so in the same way the different between 1 and 3 is 2 (3 - 1 = 2), DATEDIFF(d) is effectively D2 - D1. So to compensate for the extra day you want to count, you need to DATEADD a day to either (ToDate or FromDate) to offset your dates:
SELECT table2.id, table2.Name, table2.Total, SUM(DATEDIFF(d, DATEADD(d, -1, table1.FromDate), table1.ToDate))
FROM table1
INNER JOIN table2 ON table1.id = table2.id
GROUP BY table2.id, table2.Name, table2.Total
I think a GROUP BY query would be simpler:
SELECT table2.Id, table2.name, table2.total,
SUM(DATEDIFF(day, table1.fromdate, table1.todate)) AS Days
FROM table1
left outer join table2 on
table1.personid = table2.personid
GROUP BY table2.Id, table2.name, table2.total
SELECT table2.Id, table2.name, table2.total,
COALESCE(
(
SELECT SUM(DATEDIFF(day, table1.fromdate, table1.todate) + 1)
FROM table1
WHERE table1.personid = table2.personid
), 0) AS [days]
FROM table2

Resources