Get dates missing from multiple date ranges - sql

I have one table that stores when a customer support employee is in a particular location and for what date. Each separate date is its own record.
I have a second table that stores a range of dates that customers have asked for onsite support.
I need to extract a list of dates that a given location does NOT have any support representation. All I need is the location and the date(s). I don't care which employee in that location or which customer has requested the support.
So in the sample data below, I need to see as my query results:
+--------+------------+
| London | 04/01/2019 |
| London | 07/01/2019 |
| Paris | 05/01/2019 |
+--------+------------+
Table: Employee_Location
+----------+----------+------------+
| Employee | Location | Date |
+----------+----------+------------+
| 1111 | London | 01/01/2019 |
| 1111 | London | 02/01/2019 |
| 1111 | London | 03/01/2019 |
| 2222 | Paris | 01/01/2019 |
| 2222 | Paris | 02/01/2019 |
| 2222 | Paris | 03/01/2019 |
| 2222 | Paris | 04/01/2019 |
| 3333 | London | 05/01/2019 |
| 3333 | Paris | 06/01/2019 |
| 3333 | Paris | 07/01/2019 |
| 4444 | London | 06/01/2019 |
+----------+----------+------------+
Table: Customer_Request
+----------+----------+---------------+------------+
| Customer | Location | Request From | Request To |
+----------+----------+---------------+------------+
| AAAA | London | 01/01/2019 | 06/01/2019 |
| BBBB | Paris | 01/01/2019 | 06/01/2019 |
| CCCC | London | 05/01/2019 | 07/01/2019 |
+----------+----------+---------------+------------+
Here is my current code ...
select c.CALENDARDTM
from CALENDAR c, Employee_Location el
join Customer_Request cron el.location = cr.location
where c.CALENDARDTM NOT BETWEEN cr.RequestFrom and cr.RequestTo
and c.CALENDARDTM between '2019-01-01' AND '2019-01-07'

The key to solving this problem is to create a recordset that contains all dates between your nominated start and end dates.
There are a variety of methods you can use to do this, in the below example I have used a recursive CTE, for larger datasets you will need to tweak this slightly.
Once you have a list of all the dates, you combine it with a list of all locations, so you have all dates at all locations.?
Then you remove all records which match the records that already exists, in the example below a 'Not Exists' is used, but you can use a variety of approaches to get the desired outcome.
CREATE TABLE #Employee_Location (Employee int, [Location] varchar(100), [date] date)
INSERT INTO #Employee_Location (Employee, [Location], [Date])
VALUES (1111,'London','2019-01-01')
,(1111,'London','2019-01-02')
,(1111,'London','2019-01-03')
,(2222,'Paris','2019-01-01')
,(2222,'Paris','2019-01-02')
,(2222,'Paris','2019-01-03')
,(2222 ,'Paris','2019-01-04')
,(3333,'London','2019-01-05')
,(3333,'Paris','2019-01-06')
,(3333,'Paris','2019-01-07')
,(4444,'London','2019-01-06')
DECLARE #StartDate date = '2019-01-01'
DECLARE #EndDate date = '2019-01-07'
;WITH Dates AS (
SELECT #StartDate as d
UNION ALL
SELECT DateAdd(d, 1, d) as d
FROM Dates
WHERE d < #EndDate
)
,Locations AS (
SELECT DISTINCT [Location]
FROM #Employee_Location
)
,AllRecords AS (
SELECT d
,[Location]
FROM Dates
FULL OUTER JOIN Locations
ON 1=1
)
SELECT *
FROM AllRecords
WHERE NOT EXISTS (SELECT 1
FROM #Employee_Location e
WHERE e.[date] = Allrecords.d
AND e.[Location] = Allrecords.[Location])

Related

How to join two tables in one view?

I am trying to create a view where I can see the items that have been planned to be shipped and have not been shipped, and the items have have been shipped but were not planned to.
In order to do this I have 2 tables with different data in them.
Table SC (actually shipped):
+---------+-----------------+----------------------+-------------+
| item_id | source_location | destination_location | shipped_qty |
+---------+-----------------+----------------------+-------------+
| 001 | California | South_Carolina | 80 |
+---------+-----------------+----------------------+-------------+
| 001 | California | South_Carolina | 0 |
+---------+-----------------+----------------------+-------------+
| 001 | California | Texas | 20 |
+---------+-----------------+----------------------+-------------+
| 003 | Texas | South_Carolina | 200 |
+---------+-----------------+----------------------+-------------+
| 004 | South_Carolina | Texas | 30 |
+---------+-----------------+----------------------+-------------+
| 004 | South_Carolina | Texas | 10 |
+---------+-----------------+----------------------+-------------+
Table SO (plan to ship items):
+---------+-----------------+----------------------+---------------+
| item_id | source_location | destination_location | planned_order |
+---------+-----------------+----------------------+---------------+
| 001 | California | South_Carolina | 100 |
+---------+-----------------+----------------------+---------------+
| 001 | California | South_Carolina | 100 |
+---------+-----------------+----------------------+---------------+
| 001 | California | Texas | 10 |
+---------+-----------------+----------------------+---------------+
| 003 | Texas | South_Carolina | 200 |
+---------+-----------------+----------------------+---------------+
| 004 | South_Carolina | Texas | 300 |
+---------+-----------------+----------------------+---------------+
| 004 | South_Carolina | Texas | 50 |
+---------+-----------------+----------------------+---------------+
So in this case, for example, since the item 001 has three different planned orders from California to South Carolina, I don't want it to show all the three orders in the view, I want it to be only in one row, but sum all the planned orders together, as showed below.
Desired Outcome:
+---------+----------------+-----------------+-------------+-------------+
| item_id | source_loc | destination_loc | shipped_qty | planned_qty |
+---------+----------------+-----------------+-------------+-------------+
| 001 | California | South_Carolina | 80 | 200 |
+---------+----------------+-----------------+-------------+-------------+
| 001 | California | Texas | 20 | 10 |
+---------+----------------+-----------------+-------------+-------------+
| 003 | Texas | South_Carolina | 200 | 200 |
+---------+----------------+-----------------+-------------+-------------+
| 004 | South_Carolina | Texas | 40 | 350 |
+---------+----------------+-----------------+-------------+-------------+
I have tried this so far:
SELECT o.source_location,
o.destination_location,
o.item_id,
o.planned_order,
c.shipped_qty
FROM SO_TRANSFER o, SC_TRANSFER c
But this hasn't worked since the shipped_qty does not match the item and this code also does not add the orders together.
By the way, I am using Microsoft SQL Server 2012.
Thank you!
I think you want:
select coalesce(s.item_id, p.item_id) as item_id,
coalesce(s.source_location, p.source_location) as source_location,
coalesce(s.destination_location, p.destination_location) as destination_location,
coalesce(s.shipped_qty, 0) as shipped_qty,
coalesce(planned_qty, 0) as planned_qty
from (select item_id, source_location, destination_location, sum(shipped_qty) as shipped_qty
from sc
group by item_id, source_location, destination_location
) s full join
(select item_id, source_location, destination_location, sum(planned_qty) as planned_qty
from so
group by item_id, source_location, destination_location
) p
on s.item_id = p.item_id and
s.source_location = p.source_location and
s.destination_location = p.destination_location;
You can try this:
SELECT A.item_id,
A.source_location AS source_loc,
A.destination_location AS destination_loc,
A.shipped_qty,
B.planned_order
FROM
(SELECT item_id,source_location,destination_location,SUM(shipped_qty)
AS shipped_qty
FROM SC GROUP BY item_id,source_location,destination_location) A,
(SELECT item_id,source_location,destination_location,SUM(planned_order)
AS planned_order
FROM SO GROUP BY item_id,source_location,destination_location) B
WHERE A.item_id = B.item_id AND
A.source_location= B.source_location AND
A.destination_location= B.destination_location
EDIT: I just realize my answer is similar to Gordon Linoff's answer, and his answer got more feature such as handling data that exists in one table only using COALESCE in T-SQL and FULL JOIN property. Since I worked 1 hour for this answer, so I will just leave it here.
The point is you should SUM(quantity) from each table first, then you can easy JOIN 2 table with condition:
ON so.item_id = sc.item_id AND so.source_loc = sc.source_loc AND so.destination_loc = sc.destination_loc
The following query will do what you are looking for
Create table #SC(
item_id varchar(50),
source_location varchar(max),
Destination_location varchar(max),
Shipped_qty int)
insert into #SC values('001','california','sourth_carolian',80)
insert into #SC values('001','california','sourth_carolian',0)
insert into #SC values('001','california','Texas',20)
insert into #SC values('003','Texas','sourth_carolian',200)
insert into #SC values('004','sourth_carolian','Texas',30)
insert into #SC values('004','sourth_carolian','Texas',10)
--select * from #SC
Create table #SO(
item_id varchar(50),
source_location varchar(max),
Destination_location varchar(max),
Planned_order int)
insert into #SO values('001','california','sourth_carolian',100)
insert into #SO values('001','california','sourth_carolian',100)
insert into #SO values('001','california','Texas',10)
insert into #SO values('003','Texas','sourth_carolian',200)
insert into #SO values('004','sourth_carolian','Texas',300)
insert into #SO values('004','sourth_carolian','Texas',50)
--select * from #SO
select C.item_id,C.source_location,C.Destination_location, sum(C.Shipped_qty) as Shipped_qty, po.planned_order from #SC C
outer apply
(select sum(Planned_order) as planned_order from #SO
where source_location+Destination_location=C.source_location+C.Destination_location
group by item_id,source_location,Destination_location ) as PO
group by C.item_id,C.source_location,C.Destination_location,po.planned_order
You can create a SELECT statement, and use that as a table in the FROM clause:
SELECT o.source_location,
o.destination_location,
o.item_id,
o.planned_order,
c.shipped_qty_sum
FROM SO_TRANSFER o
INNER JOIN (SELECT SUM(shipped_qty) AS shipped_qty_sum,
source_location,
item_id
FROM SC_TRANSFER
GROUP BY source_location, item_id) c
ON o.item_id = c.item_id AND o.source_location = c.source_location

Measure population on several dates

I want to measure the population of our manucipality (which contains out of several places). I've got two tables in: my first dataset is a calender table with a row for each first day of every month.
My second table contains alle the people that live and have lived in the manucipality.
What I want is the population of each place on every first day of the month from my calender table. I've put some raw data below (just a few records of the persons table because it contains 100.000 records)
Calender table:
+----------+
| Date |
+----------+
| 1-1-2018 |
+----------+
| 1-2-2018 |
+----------+
| 1-3-2018 |
+----------+
| 1-4-2018 |
+----------+
Persons table
+-----+-----------+-----------+---------------+-------+
| BSN | Startdate | Enddate | Date of death | Place |
+-----+-----------+-----------+---------------+-------+
| 1 | 12-1-2000 | null | null | A |
+-----+-----------+-----------+---------------+-------+
| 2 | 10-5-2011 | null | 22-1-2018 | B |
+-----+-----------+-----------+---------------+-------+
| 3 | 16-12-2011| 10-2-2018 | null | B |
+-----+-----------+-----------+---------------+-------+
| 4 | 9-11-2012 | null | null | B |
+-----+-----------+-----------+---------------+-------+
| 5 | 8-9-2013 | null | 27-3-2018 | A |
+-----+-----------+-----------+---------------+-------+
| 6 | 7-10-2017 | 28-3-2018 | null | B |
+-----+-----------+-----------+---------------+-------+
My expected result:
+----------+-------+------------+
| Date | Place | Population |
+----------+-------+------------+
| 1-1-2018 | A | 2 |
+----------+-------+------------+
| 1-1-2018 | B | 4 |
+----------+-------+------------+
| 1-2-2018 | A | 2 |
+----------+-------+------------+
| 1-2-2018 | B | 3 |
+----------+-------+------------+
| 1-3-2018 | A | 2 |
+----------+-------+------------+
| 1-3-2018 | B | 2 |
+----------+-------+------------+
| 1-4-2018 | A | 1 |
+----------+-------+------------+
| 1-4-2018 | B | 1 |
+----------+-------+------------+
What I've done so far but doesnt seems to work:
SELECT a.Place
,c.Date
,(SELECT COUNT(DISTINCT(b.BSN))
FROM Person as b
WHERE b.Startdate < c.Date
AND (b.Enddate > c.Date OR b.Enddate is null)
AND (b.Date of death > c.Date OR b.Date of death is null)
AND a.Place = b.Place) as Population
FROM Person as a
JOIN Calender as c
ON a.Startdate <= c.Date
AND a.Enddate >= c.Date
GROUP BY Place, Date
I hope someone can help finding out the problem. Thanks in advance
First cross join Calender and the places to get the date/place pairs. Then left join the persons on the place and the date. Finally group by date and place to get the count of people for that day and place.
SELECT [ca].[Date],
[pl].[Place],
count([pe].[Place]) [Population]
FROM [Calender] [ca]
CROSS JOIN (SELECT DISTINCT
[pe].[Place]
FROM [Persons] [pe]) [pl]
LEFT JOIN [Persons] [pe]
ON [pe].[Place] = [pl].[Place]
AND [pe].[Startdate] <= [ca].[Date]
AND (colaesce([pe].[Enddate],
[pe].[Date of death]) IS NULL
OR coalesce([pe].[Enddate],
[pe].[Date of death]) > [ca].[Date])
GROUP BY [ca].[Date],
[pl].[Place]
ORDER BY [ca].[Date],
[pl].[Place];
Some notes and assumptions:
If you have a table listing the places use that instead of the subquery aliases [pl]. I just had no other option with the given tables.
I believe the Date of death also implies an Enddate for the same day. You might want to consider a trigger, that sets the Enddate automatically to the Date of death if it isn't null. That would make things easier and probably more consistent.

Repeating ID based on

I have a very simple requirement but I'm struggling to find a way around this.
I have a very simple query:
SELECT
ServiceCode,
StartDate,
Available,
Nights,
BookingID
FROM #tmpAvailability
LEFT JOIN vwRSBooking B
ON B.Depart = A.StartDate
AND B.ServiceCode = A.SupplierCode
AND B.StatusID IN (2640, 2621)
ORDER BY StartDate;
Made up of 2 tables
#tmpAvailability which consists of the following fields:
SupplierCode
StartDate
Available
vwRSBooking which consists of the following fields
BookingID
DepartDate
Code
Nights
StatusID
Departure and startdate can be joined to link the first day, and the servicecode and suppliercode can be joined to make sure that the availability is linked to the same supplier.
Which produces an output like this:
Code | Dates | Available | Nights | BookingID
TEST | 2018-01-04 | 1 | NULL | NULL
TEST | 2018-01-05 | 1 | NULL | NULL
TEST | 2018-01-06 | 0 | 4 | 123456
TEST | 2018-01-07 | 0 | NULL | NULL
TEST | 2018-01-08 | 0 | NULL | NULL
TEST | 2018-01-09 | 0 | NULL | NULL
TEST | 2018-01-10 | 1 | NULL | NULL
TEST | 2018-01-11 | 1 | NULL | NULL
TEST | 2018-01-12 | 1 | NULL | NULL
TEST | 2018-01-13 | 0 | NULL | 234567
TEST | 2018-01-14 | 0 | NULL | NULL
TEST | 2018-01-15 | 0 | NULL | NULL
What I need is when the BookingID in for 4 days that the bookingID and the nights are spread across those days, for example:
Code | Dates | Available | Nights | BookingID
TEST | 2018-01-04 | 1 | NULL | NULL
TEST | 2018-01-05 | 1 | NULL | NULL
TEST | 2018-01-06 | 0 | 4 | 123456
TEST | 2018-01-07 | 0 | 4 | 123456
TEST | 2018-01-08 | 0 | 4 | 123456
TEST | 2018-01-09 | 0 | 4 | 123456
TEST | 2018-01-10 | 1 | NULL | NULL
TEST | 2018-01-11 | 1 | NULL | NULL
TEST | 2018-01-12 | 1 | NULL | NULL
TEST | 2018-01-13 | 0 | 3 | 234567
TEST | 2018-01-14 | 0 | 3 | 234567
TEST | 2018-01-15 | 0 | 3 | 234567
TEST | 2018-01-16 | 1 | NULL | NULL
If anyone has any ideas on how to solve it would be most appreciated.
Andrew
You could replace your vwRSBooking with another view which uses a CTE to obtain all the dates the booking covers. Then use the view's coverdate for joining to the #tmpAvailability table:
CREATE VIEW vwRSBookingFull
AS
WITH cte ( bookingid, nights, depart, code, coverdate)
AS (SELECT bookingid,
nights,
depart,
code,
depart
FROM vwRSBooking
UNION ALL
SELECT c.bookingid,
c.nights,
c.depart,
c.code,
DATEADD(d, 1, c.coverdate)
FROM cte c
WHERE DATEDIFF(d, c.depart, c.coverdate) < (c.nights - 1))
SELECT c.bookingid,
c.nights,
c.depart,
c.code,
c.coverdate
FROM cte c
GO
You will need a calendar table with all the dates in the date range your dates may fall into. For this example, I build one for January 2018. We can then join onto this table to create the additional rows.
Here is the sample code I used. You can see it at SQL Fiddle.
CREATE TABLE code (
code varchar(max),
dates date,
available int,
nights int,
bookingid int
)
INSERT INTO code VALUES
('TEST','2018-01-04','1',NULL,NULL),
('TEST','2018-01-05','1',NULL,NULL),
('TEST','2018-01-06','0',4,123456),
('TEST','2018-01-07','0',NULL,NULL),
('TEST','2018-01-08','0',NULL,NULL),
('TEST','2018-01-09','0',NULL,NULL),
('TEST','2018-01-10','1',NULL,NULL),
('TEST','2018-01-11','1',NULL,NULL),
('TEST','2018-01-12','1',NULL,NULL),
('TEST','2018-01-13','0',3,234567),
('TEST','2018-01-14','0',NULL,NULL),
('TEST','2018-01-15','0',NULL,NULL)
CREATE TABLE dates (
dates date
)
INSERT INTO dates VALUES
('2018-01-01'),('2018-01-02'),('2018-01-03'),('2018-01-04'),('2018-01-05'),('2018-01-06'),('2018-01-07'),('2018-01-08'),('2018-01-09'),('2018-01-10'),('2018-01-11'),('2018-01-12'),('2018-01-13'),('2018-01-14'),('2018-01-15'),('2018-01-16'),('2018-01-17'),('2018-01-18'),('2018-01-19'),('2018-01-20'),('2018-01-21'),('2018-01-22'),('2018-01-23'),('2018-01-24'),('2018-01-25'),('2018-01-26'),('2018-01-27'),('2018-01-28'),('2018-01-29'),('2018-01-30'),('2018-01-31')
Here is the query based on this dataset:
SELECT
code.code,
dates.dates,
code.available,
code.nights,
code.bookingid
FROM code
LEFT JOIN dates ON
dates.dates >= code.dates
AND dates.dates < DATEADD(DAY,nights,code.dates)
Edit: Here is an example using your initial query as a subquery to join your result set onto the dates table if you want a copy & paste. Still requires creating the dates table.
SELECT
ServiceCode,
StartDate,
Available,
Nights,
BookingID
FROM (
SELECT
ServiceCode,
StartDate,
Available,
Nights,
BookingID
FROM #tmpAvailability
LEFT JOIN vwRSBooking B
ON B.Depart = A.StartDate
AND B.ServiceCode = A.SupplierCode
AND B.StatusID IN (2640, 2621)
) code
LEFT JOIN dates ON
dates.dates >= code.dates
AND dates.dates < DATEADD(DAY,nights,code.dates)
ORDER BY StartDate;

Merge Two Rows When DATEDIFF>3

I have a temp table which has the results of a main query where all records have been pivoted out. However, there are two date fields, that when they do not match, cannot pivot into a single row.
I am checking if they have greater than a 3 day difference between them, and if there is, then I need to delete the oldest date and merge the rest of the columns together.
I am using SQL Server 2014
Example Table
+--------------+-------------+-------------------------------+
| Lname | Date1 | idCode1 | idCode2 |
+--------------+-------------+-------------------------------+
| Higgins | 11/30/16 | 9008 2172 | NULL |
| Higgins | 12/31/16 | NULL | 4007 3589 |
| Shaffer | 11/15/16 | 9000 1541 | NULL |
| Shaffer | 11/21/16 | NULL | 7889 9412 |
+--------------+-------------+-------------------------------+
Needs to look like this.
+--------------+-------------+-------------------------------+
| Lname | Date1 | idCode1 | idCode2 |
+--------------+-------------+-------------------------------+
| Higgins | 12/31/16 | 9008 2172 | 4007 3589 |
| Shaffer | 11/21/16 | 9000 1541 | 7889 9412 |
+--------------+-------------+-------------------------------+
Unless I'm missing something here, a simple group by should do it (Assuming you are only going to get max 2 rows for each Lname):
Create and populate sample table (Please save us this step in your future questions)
DECLARE #T AS TABLE
(
Lname varchar(10),
Date1 date,
idCode1 varchar(20),
idCode2 varchar(20)
)
INSERT INTO #T VALUES
('Higgins', '11/30/16', '9008 2172', NULL ),
('Higgins', '12/31/16', NULL , '4007 3589'),
('Shaffer', '11/15/16', '9000 1541', NULL ),
('Shaffer', '11/21/16', NULL , '7889 9412')
The query:
SELECT LName,
MAX(Date1) As Date1,
MAX(IdCode1) As IdCode1,
Max(IdCode2) As IdCode2
FROM #T
GROUP BY LName
HAVING DATEDIFF(DAY, MIN(Date1), MAX(Date1)) > 3
Results:
LName Date1 IdCode1 IdCode2
Higgins 31.12.2016 00:00:00 9008 2172 4007 3589
Shaffer 21.11.2016 00:00:00 9000 1541 7889 9412

SQL output based on a date range

Given the following two table scenario, how would I go about outputting the commission percentage based on the date range:
Commission Percentages
| User ID | Start Date | End Date | Percentage
| -------- | ---------- | ----------- | ----------
| 1 | 11/11/2014 | 11/30/2014 | 10%
| 1 | 11/30/2014 | NULL | 20%
| 2 | 10/10/2014 | NULL | 15%
Sales
| User ID | Sale Date |
| -------- | ---------- |
| 1 | 11/24/2014 |
| 1 | 12/1/2014 |
| 2 | 12/30/2014 |
I would like to end up with a join between the two like so (a null value in the end date field represents present - and the dates will also include a time stamp):
| User ID | Sales Date | Start Date | End Date | Percentage
| -------- | ---------- | ---------- | ---------- | ----------
| 1 | 11/24/2014 | 11/11/2014 | 11/30/2014 | 10%
| 1 | 12/1/2014 | 11/30/2014 | NULL | 20%
| 2 | 12/30/2014 | 10/10/2014 | NULL | 15%
I am using SQL Server 2012
Thanks
Something like this might work for you, however you need to figure your date logic (i.e. whether it should be greater than, or greater than/equal to) depending on how your system works:
select S.UserID, S.SalesDate, C.StartDate, C.EndDate, C.Percentage
from Sales AS S
inner join Commission AS C
on C.UserID = S.UserID
AND S.SalesDate > C.StartDate
AND S.SalesDate <= coalesce(C.EndDate, S.SalesDate)
I'm assuming the end date is the first date the percentage does not apply based on the data. User ID 1 has a vector overlap.
SELECT s.User_ID,
s.Sales_Date,
cp.Start_Date,
cp.End_Date,
cp.Pecrcentage
FROM Commission_Percentages cp
INNER JOIN Sales s
ON s.User_ID = cp.User_ID
AND s.Sale_Date >= cp.Start_Date
AND (s.Sale_Date < cp.End_Date OR cp.End_Date IS NULL)