Select which has matching date or latest date record - sql

Here are two tables.
ItemInfo
Id Description
1 First Item
2 Second Item
ItemInfoHistory
Id ItemInfoId Price StartDate EndDate
1 1 45 2020-09-01 2020-09-15
2 2 55 2020-09-26 null
3 1 50 2020-09-16 null
Here is SQL query.
SELECT i.Id, Price, StartDate, EndDate
FROM Itemsinfo i
LEFT JOIN ItemInfoHistory ih ON i.id= ih.ItemsMasterId AND CONVERT(DATE, GETDATE()) >= StartDate AND ( CONVERT(DATE, GETDATE()) <= EndDate OR EndDate IS NULL)
Which gives following results, when runs the query on 9/20
Id Price StartDate EndDate
1 50 2020-09-16 NULL
2 NULL NULL NULL
For the second item, I want to get latest record from history table, as shown below.
Id Price StartDate EndDate
1 50 2020-09-16 NULL
2 55 2020-09-26 NULL
Thanks in advance.

Probably the most efficient method is two joins. Assuming the "latest" record has a NULL values for EndDate, then:
SELECT i.Id,
COALESCE(ih.Price, ih_last.Price) as Price,
COALESCE(ih.StartDate, ih_last.StartDate) as StartDate,
COALESCE(ih.EndDate, ih_last.EndDate) as EndDate
FROM Itemsinfo i LEFT JOIN
ItemInfoHistory ih
ON i.id = ih.ItemsMasterId AND
CONVERT(DATE, GETDATE()) >= StartDate AND
(CONVERT(DATE, GETDATE()) <= EndDate OR EndDate IS NULL) LEFT JOIN
ItemInfoHistory ih_last
ON i.id = ih_last.ItemsMasterId AND
ih_last.EndDate IS NULL;
Actually, the middle join doesn't need to check for NULL, so that could be removed.

Related

MsSQL LEFT JOIN with double GROUP BY

I am trying currently using MsSQL for a product shipping DB. I have spent a long time trying to write a SQL query to get the amount of products going to each delivery location in an area, per date, in a 4 day period beginning today.
In an area means that there is another location that is parent to that location.
The tables concerned are Products and Locations and are structured as follows
Products
ProductID DeliveryDate DestinationID
1 2018-10-05 1
2 2018-10-05 2
3 2018-10-05 3
4 2018-10-06 1
5 2018-10-06 5
Locations
LocationID OwnerID
1 4
2 4
3 4
4 Null
5 6
6 Null
The output desired is as follows
DeliveryDate Destination ProductCount
2018-10-04 1 Null
2018-10-04 2 Null
2018-10-04 3 Null
2018-10-05 1 1
2018-10-05 2 1
2018-10-05 3 1
2018-10-06 1 1
2018-10-06 2 Null
2018-10-06 3 Null
2018-10-07 1 Null
2018-10-07 2 Null
2018-10-07 3 Null
What I have tried so far is this
DECLARE #startdate DATETIME
,#enddate DATETIME
SET #startdate = convert(varchar, '2018-10-04 00:00:00', 102)
SET #enddate = convert(varchar, '2018-10-07 00:00:00', 102)
;WITH DateArray
AS (
SELECT #startdate DateVal
UNION ALL
SELECT DateVal + 1
FROM DateArray
WHERE DateVal + 1 <= #enddate
)
SELECT * FROM
(SELECT da.DateVal AS DeliveryDate
FROM DateArray da) a
LEFT JOIN
(SELECT ISNULL(COUNT(p.ProductID),0) AS ProductCount,
DeliveryDate,
ISNULL(p.DestinationID,'') AS Destination
FROM Product p
AND p.DestinationID IN(SELECT LocationID FROM Locations WHERE OwnerID = 4)
GROUP BY p.DestinationID, p.DeliveryDate) AS b
ON b.DeliveryDate = a.DeliveryDate
The result ensures that all dates are present even if the ProductCount is null, however, not every Destination is shown if count is null. Shown below:
DeliveryDate Destination ProductCount
2018-10-04 Null Null
2018-10-05 1 1
2018-10-05 2 1
2018-10-05 3 1
2018-10-06 1 1
2018-10-07 Null Null
I have spent two days stubbornly trying to figure this out with many online SQL resources and scouring StackOverFlow but with no luck.
I recommend creating a numbers table or a calendar table, but you recursive query suffices for small sets.
DECLARE
#startdate DATETIME = '2018-10-04 00:00:00',
#enddate DATETIME = '2018-10-07 00:00:00';
WITH
DateArray AS
(
SELECT #startdate DateVal
UNION ALL
SELECT DateVal + 1 FROM DateArray WHERE DateVal + 1 <= #enddate
)
SELECT
DateArray.DateVal AS DeliveryDate,
Locations.LocationID AS Destination,
COUNT(Products.ProductID) AS ProductCount
FROM
DateArray
CROSS JOIN
(
SELECT * FROM Locations WHERE OwnerID = 4
)
Locations
LEFT JOIN
Products
ON Products.DeliveryDate = DateArray.DateVal
AND Products.DestinationID = Locations.LocationID
GROUP BY
DateArray.DateVal,
Locations.LocationID
Or...
SELECT
DateArray.DateVal AS DeliveryDate,
Locations.LocationID AS Destination,
Products.ProductCount AS ProductCount
FROM
DateArray
CROSS JOIN
(
SELECT * FROM Locations WHERE OwnerID = 4
)
Locations
LEFT JOIN
(
SELECT DeliveryDate, DestinationID, COUNT(*) AS ProductCount
FROM Products
GROUP BY DeliveryDate, DestinationID
)
Products
ON Products.DeliveryDate = DateArray.DateVal
AND Products.DestinationID = Locations.LocationID

SQL - Find if column dates include at least partially a date range

I need to create a report and I am struggling with the SQL script.
The table I want to query is a company_status_history table which has entries like the following (the ones that I can't figure out)
Table company_status_history
Columns:
| id | company_id | status_id | effective_date |
Data:
| 1 | 10 | 1 | 2016-12-30 00:00:00.000 |
| 2 | 10 | 5 | 2017-02-04 00:00:00.000 |
| 3 | 11 | 5 | 2017-06-05 00:00:00.000 |
| 4 | 11 | 1 | 2018-04-30 00:00:00.000 |
I want to answer to the question "Get all companies that have been at least for some point in status 1 inside the time period 01/01/2017 - 31/12/2017"
Above are the cases that I don't know how to handle since I need to add some logic of type :
"If this row is status 1 and it's date is before the date range check the next row if it has a date inside the date range."
"If this row is status 1 and it's date is after the date range check the row before if it has a date inside the date range."
I think this can be handled as a gaps and islands problem. Consider the following input data: (same as sample data of OP plus two additional rows)
id company_id status_id effective_date
-------------------------------------------
1 10 1 2016-12-15
2 10 1 2016-12-30
3 10 5 2017-02-04
4 10 4 2017-02-08
5 11 5 2017-06-05
6 11 1 2018-04-30
You can use the following query:
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
ORDER BY company_id, effective_date
to get:
id company_id status_id effective_date grp
-----------------------------------------------
1 10 1 2016-12-15 0
2 10 1 2016-12-30 1
3 10 5 2017-02-04 2
4 10 4 2017-02-08 2
5 11 5 2017-06-05 0
6 11 1 2018-04-30 0
Now you can identify status = 1 islands using:
;WITH CTE AS
(
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
)
SELECT id, company_id, status_id, effective_date,
ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) -
cnt AS grp
FROM CTE
Output:
id company_id status_id effective_date grp
-----------------------------------------------
1 10 1 2016-12-15 1
2 10 1 2016-12-30 1
3 10 5 2017-02-04 1
4 10 4 2017-02-08 2
5 11 5 2017-06-05 1
6 11 1 2018-04-30 2
Calculated field grp will help us identify those islands:
;WITH CTE AS
(
SELECT t.id, t.company_id, t.status_id, t.effective_date, x.cnt
FROM company_status_history AS t
OUTER APPLY
(
SELECT COUNT(*) AS cnt
FROM company_status_history AS c
WHERE c.status_id = 1
AND c.company_id = t.company_id
AND c.effective_date < t.effective_date
) AS x
), CTE2 AS
(
SELECT id, company_id, status_id, effective_date,
ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) -
cnt AS grp
FROM CTE
)
SELECT company_id,
MIN(effective_date) AS start_date,
CASE
WHEN COUNT(*) > 1 THEN DATEADD(DAY, -1, MAX(effective_date))
ELSE MIN(effective_date)
END AS end_date
FROM CTE2
GROUP BY company_id, grp
HAVING COUNT(CASE WHEN status_id = 1 THEN 1 END) > 0
Output:
company_id start_date end_date
-----------------------------------
10 2016-12-15 2017-02-03
11 2018-04-30 2018-04-30
All you want know is those records from above that overlap with the specified interval.
Demo here with somewhat more complicated use case.
Maybe this is what you are looking for? For these kind of questions, you need to join two instance of your table, in this case I am just joining with next record by Id, which probably is not totally correct. To do it better, you can create a new Id using a windowed function like row_number, ordering the table by your requirement criteria
If this row is status 1 and it's date is before the date range check
the next row if it has a date inside the date range
declare #range_st date = '2017-01-01'
declare #range_en date = '2017-12-31'
select
case
when csh1.status_id=1 and csh1.effective_date<#range_st
then
case
when csh2.effective_date between #range_st and #range_en then true
else false
end
else NULL
end
from company_status_history csh1
left join company_status_history csh2
on csh1.id=csh2.id+1
Implementing second criteria:
"If this row is status 1 and it's date is after the date range check
the row before if it has a date inside the date range."
declare #range_st date = '2017-01-01'
declare #range_en date = '2017-12-31'
select
case
when csh1.status_id=1 and csh1.effective_date<#range_st
then
case
when csh2.effective_date between #range_st and #range_en then true
else false
end
when csh1.status_id=1 and csh1.effective_date>#range_en
then
case
when csh3.effective_date between #range_st and #range_en then true
else false
end
else null -- ¿?
end
from company_status_history csh1
left join company_status_history csh2
on csh1.id=csh2.id+1
left join company_status_history csh3
on csh1.id=csh3.id-1
I would suggest the use of a cte and the window functions ROW_NUMBER. With this you can find the desired records. An example:
DECLARE #t TABLE(
id INT
,company_id INT
,status_id INT
,effective_date DATETIME
)
INSERT INTO #t VALUES
(1, 10, 1, '2016-12-30 00:00:00.000')
,(2, 10, 5, '2017-02-04 00:00:00.000')
,(3, 11, 5, '2017-06-05 00:00:00.000')
,(4, 11, 1, '2018-04-30 00:00:00.000')
DECLARE #StartDate DATETIME = '2017-01-01';
DECLARE #EndDate DATETIME = '2017-12-31';
WITH cte AS(
SELECT *
,ROW_NUMBER() OVER (PARTITION BY company_id ORDER BY effective_date) AS rn
FROM #t
),
cteLeadLag AS(
SELECT c.*, ISNULL(c2.effective_date, c.effective_date) LagEffective, ISNULL(c3.effective_date, c.effective_date)LeadEffective
FROM cte c
LEFT JOIN cte c2 ON c2.company_id = c.company_id AND c2.rn = c.rn-1
LEFT JOIN cte c3 ON c3.company_id = c.company_id AND c3.rn = c.rn+1
)
SELECT 'Included' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date BETWEEN #StartDate AND #EndDate
UNION ALL
SELECT 'Following' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date > #EndDate
AND LagEffective BETWEEN #StartDate AND #EndDate
UNION ALL
SELECT 'Trailing' AS RangeStatus, *
FROM cteLeadLag
WHERE status_id = 1
AND effective_date < #EndDate
AND LeadEffective BETWEEN #StartDate AND #EndDate
I first select all records with their leading and lagging Dates and then I perform your checks on the inclusion in the desired timespan.
Try with this, self-explanatory. Responds to this part of your question:
I want to answer to the question "Get all companies that have been at
least for some point in status 1 inside the time period 01/01/2017 -
31/12/2017"
Case that you want to find those id's that have been in any moment in status 1 and have records in the period requested:
SELECT *
FROM company_status_history
WHERE id IN
( SELECT Id
FROM company_status_history
WHERE status_id=1 )
AND effective_date BETWEEN '2017-01-01' AND '2017-12-31'
Case that you want to find id's in status 1 and inside the period:
SELECT *
FROM company_status_history
WHERE status_id=1
AND effective_date BETWEEN '2017-01-01' AND '2017-12-31'

SQL Server: Count days difference between previous date and current date

I've been trying to find a way to count days difference between two dates from previous and current rows which counting only business days.
Example data and criteria here.
ID StartDate EndDate NewDate DaysDifference
========================================================================
0 04/05/2017 null
1 12/06/2017 16/06/2017 12/06/2017 29
2 03/07/2017 04/07/2017 16/06/2017 13
3 07/07/2017 10/07/2017 04/07/2017 5
4 12/07/2017 26/07/2017 10/07/2017 13
My end goal is
I want two new columns; NewDate and DayDifference.
NewDate column is from EndDate from previous row. As you can see that for example, NewDate of ID 2 is 16/06/2017 which come from EndDate of ID 1. But if value in EndDate of previous row is null, use its StartDate instead(ID 1 case).
DaysDifference column is from counting only business days between EndDate and NewDate columns.
Here is script that I am using atm.
select distinct
c.ID
,c.EndDate
,isnull(p.EndDate,c.StartDate) as NewDate
,count(distinct cast(l.CalendarDate as date)) as DaysDifference
from
(select *
from table) c
full join
(select *
from table) p
on c.level = p.level
and c.id-1 = p.id
left join Calendar l
on (cast(l.CalendarDate as date) between cast(p.EndDate as date) and cast(c.EndDate as date)
or
cast(l.CalendarDate as date) between cast(p.EndDate as date) and cast(c.StartDate as date))
and l.Day not in ('Sat','Sun') and l.Holiday <> 'Y'
where c.ID <> 0
group by
c.ID
,c.EndDate
,isnull(p.EndDate,c.StartDate)
And this's the current result :
ID EndDate NewDate DaysDifference
=========================================================
1 16/06/2017 12/06/2017 0
2 04/07/2017 16/06/2017 13
3 10/07/2017 04/07/2017 5
4 26/07/2017 10/07/2017 13
Seems like in the real data, I've got correct DaysDifference for ID 2,3,4 except ID 1 because of the null value from its previous row(ID 0) that printing StartDate instead of null EndDate, so it counts incorrectly.
Hope I've provided enough info. :)
Could you please guide me a way to count DaysDifference correctly.
Thanks in advance!
I think you can use this logic to get the previous date:
select t.*,
lag(coalesce(enddate, startdate), 1) over (order by 1) as newdate
from t;
Then for the difference:
select id, enddate, newdate,
sum(case when c.day not in ('Sat', 'Sun') and c.holiday <> 'Y' then 1 else 0 end) as diff
from (select t.*,
lag(coalesce(enddate, startdate), 1) over (order by 1) as newdate
from t
) t join
calendar c
on c.calendardate >= newdate and c.calendardate <= startdate
group by select id, enddate, newdate;

Query to return all the days of a month

This problem is related to this, which has no solution in sight: here
I have a table that shows me all sessions of an area.
This session has a start date.
I need to get all the days of month of the start date of the session by specific area (in this case)
I have this query:
SELECT idArea, idSession, startDate FROM SessionsPerArea WHERE idArea = 1
idArea | idSession | startDate |
1 | 1 | 01-01-2013 |
1 | 2 | 04-01-2013 |
1 | 3 | 07-02-2013 |
And i want something like this:
date | Session |
01-01-2013 | 1 |
02-01-2013 | NULL |
03-01-2013 | NULL |
04-01-2013 | 1 |
........ | |
29-01-2013 | NULL |
30-01-2013 | NULL |
In this case, the table returns me all the days of January.
The second column is the number of sessions that occur on that day, because there may be several sessions on the same day.
Anyone can help me?
Please try:
DECLARE #SessionsPerArea TABLE (idArea INT, idSession INT, startDate DATEtime)
INSERT #SessionsPerArea VALUES (1,1,'2013-01-01')
INSERT #SessionsPerArea VALUES (1,2,'2013-01-04')
INSERT #SessionsPerArea VALUES (1,3,'2013-07-02')
DECLARE #RepMonth as datetime
SET #RepMonth = '01/01/2013';
WITH DayList (DayDate) AS
(
SELECT #RepMonth
UNION ALL
SELECT DATEADD(d, 1, DayDate)
FROM DayList
WHERE (DayDate < DATEADD(d, -1, DATEADD(m, 1, #RepMonth)))
)
SELECT *
FROM DayList t1 left join #SessionsPerArea t2 on t1.DayDate=startDate and t2.idArea = 1
This will work:
DECLARE #SessionsPerArea TABLE (idArea INT, idSession INT, startDate DATE)
INSERT #SessionsPerArea VALUES
(1,1,'2013-01-01'),
(1,2,'2013-01-04'),
(1,3,'2013-07-02')
;WITH t1 AS
(
SELECT startDate
, DATEADD(MONTH, DATEDIFF(MONTH, '1900-01-01', startDate), '1900-01-01') firstInMonth
, DATEADD(DAY, -1, DATEADD(MONTH, DATEDIFF(MONTH, '1900-01-01', startDate) + 1, '1900-01-01')) lastInMonth
, COUNT(*) cnt
FROM #SessionsPerArea
WHERE idArea = 1
GROUP BY
startDate
)
, calendar AS
(
SELECT DISTINCT DATEADD(DAY, c.number, t1.firstInMonth) d
FROM t1
JOIN master..spt_values c ON
type = 'P'
AND DATEADD(DAY, c.number, t1.firstInMonth) BETWEEN t1.firstInMonth AND t1.lastInMonth
)
SELECT d date
, cnt Session
FROM calendar c
LEFT JOIN t1 ON t1.startDate = c.d
It uses simple join on master..spt_values table to generate rows.
Just an example of calendar table. To return data for a month adjust the number of days between < 32, for a year to 365+1. You can calculate the number of days in a month or between start/end dates with query. I'm not sure how to do this in SQL Server. I'm using hardcoded values to display all dates in Jan-2013. You can adjust start and end dates for diff. month or to get start/end dates with queries...:
WITH data(r, start_date) AS
(
SELECT 1 r, date '2012-12-31' start_date FROM any_table --dual in Oracle
UNION ALL
SELECT r+1, date '2013-01-01'+r-1 FROM data WHERE r < 32 -- number of days between start and end date+1
)
SELECT start_date FROM data WHERE r > 1
/
START_DATE
----------
1/1/2013
1/2/2013
1/3/2013
...
...
1/31/2013

Adding a Date column based on the next row date value

Im using SQL Server 2005. From the tbl_temp table below, I would like to add an EndDate column based on the next row's StartDate minus 1 day until there's a change in AID and UID combination. This calculated EndDate will go to the row above it as the EndDate. The last row of the group of AID and UID will get the system date as its EndDate. The table has to be ordered by AID, UID, StartDate sequence. Thanks for the help.
-- tbl_temp
AID UID StartDate
1 1 2013-02-20
2 1 2013-02-06
1 1 2013-02-21
1 1 2013-02-27
1 2 2013-02-02
1 2 2013-02-04
-- Result needed
AID UID StartDate EndDate
1 1 2013-02-20 2013-02-20
1 1 2013-02-21 2013-02-26
1 1 2013-02-27 sysdate
1 2 2013-02-02 2013-02-03
1 2 2013-02-04 sysdate
2 1 2013-02-06 sysdate
The easiest way to do this is with a correlated subquery:
select t.*,
(select top 1 dateadd(day, -1, startDate )
from tbl_temp t2
where t2.aid = t.aid and
t2.uid = t.uid and
t2.startdate > t.startdate
) as endDate
from tbl_temp t
To get the current date, use isnull():
select t.*,
isnull((select top 1 dateadd(day, -1, startDate )
from tbl_temp t2
where t2.aid = t.aid and
t2.uid = t.uid and
t2.startdate > t.startdate
), getdate()
) as endDate
from tbl_temp t
Normally, I would recommend coalesce() over isnull(). However, there is a bug in some versions of SQL Server where it evaluates the first argument twice. Normally, this doesn't make a difference, but with a subquery it does.
And finally, the use of sysdate makes me think of Oracle. The same approach will work there too.
;WITH x AS
(
SELECT AID, UID, StartDate,
ROW_NUMBER() OVER(PARTITION BY AID, UID ORDER BY StartDate) AS rn
FROM tbl_temp
)
SELECT x1.AID, x1.UID, x1.StartDate,
COALESCE(DATEADD(day,-1,x2.StartDate), CAST(getdate() AS date)) AS EndDate
FROM x x1
LEFT OUTER JOIN x x2 ON x2.AID = x1.AID AND x2.UID = x1.UID
AND x2.rn = x1.rn + 1
ORDER BY x1.AID, x1.UID, x1.StartDate
SQL Fiddle example