Field correlation with date constraint - sql

We're stuck in a huge challenge here. Let's assume the tables of one db were not properly planned in first hand. That's what it is and I need a solution for that.
There's a table A with 2 fields. Let's think that I have an assistant that supports my job every day, but i just registered when he/she started to assist me. It means the 'Stop Date' (not existent in the table) of each Assistant is the day before the Start Date of the next one.
Assistant | Start Date
James | 07/01/17
Frank | 01/03/18
Erika | 01/06/18
There's a second table B with that registers how many hours my assistant worked:
Date | Worked Hours
12/31/17 | 7.5
01/01/18 | 7.5
01/02/18 | 9
01/03/18 | 8
01/04/18 | 9
01/05/18 | 7.5
01/06/18 | 9
01/07/18 | 10
Given the information above, I need to write a SQL to return a table like below, considering the Start Dates of each person:
Assistant | Date | Worked Hours
Basically I need to correlate somehow the Date and Start Date to return the Assistant, but it involve's date comparisons that I have no idea how to do.
Any ideas how to solve this?

You can use a correlated subquery:
select b.*,
(select a.assistant
from a
where a.date <= b.date
order by a.date desc
fetch first 1 row only
) as assistant
from b;
Note all databases support the ANSI standard fetch first 1 row only, so you may need to use limit or top or whatever is appropriate for your database.

You can try this.
DECLARE #TableA TABLE (Assistant VARCHAR(10), [Start Date] DATE)
INSERT INTO #TableA VALUES
('James','07/01/17'),
('Frank','01/03/18'),
('Erika','01/06/18')
DECLARE #TableB TABLE ([Date] DATE, [Worked Hours] DECIMAL(18,2))
INSERT INTO #TableB VALUES
('12/31/17', 7.5),
('01/01/18', 7.5),
('01/02/18', 9 ),
('01/03/18', 8 ),
('01/04/18', 9 ),
('01/05/18', 7.5),
('01/06/18', 9 ),
('01/07/18', 10 )
;WITH CTE AS (
SELECT *, RN = ROW_NUMBER() OVER( PARTITION BY [Date] ORDER BY [Start Date] DESC)
FROM
#TableA A
INNER JOIN #TableB B ON A.[Start Date] <= B.Date
)
select Assistant, Date, [Worked Hours] FROM CTE WHERE RN = 1
Result:
Assistant Date Worked Hours
---------- ---------- ---------------------------------------
James 2017-12-31 7.50
James 2018-01-01 7.50
James 2018-01-02 9.00
Frank 2018-01-03 8.00
Frank 2018-01-04 9.00
Frank 2018-01-05 7.50
Erika 2018-01-06 9.00
Erika 2018-01-07 10.00

use 'lead()' to find the next record
use infinity to keep the final interval unclosed
CREATE TABLE a
( assistant text primary key
, startdate date
);
SET datestyle = 'mdy';
insert into a(assistant,startdate) VALUES
('James', '07/01/17' )
,('Frank', '01/03/18' )
,('Erika', '01/06/18' )
;
CREATE TABLE b
( ddate DATE NOT NULL primary key
, workedhours DECIMAL(4,1)
);
insert into b(ddate,workedhours) VALUES
('12/31/17', 7.5)
,('01/01/18', 7.5)
,('01/02/18', 9)
,('01/03/18', 8)
,('01/04/18', 9)
,('01/05/18', 7.5)
,('01/06/18', 9)
,('01/07/18', 10)
;
WITH aa AS (
SELECT a.assistant
, a.startdate
, lead(a.startdate, 1, 'infinity'::date) OVER (ORDER BY a.startdate)
AS enddate
FROM a
)
-- SELECT * FROM a ;
SELECT aa.startdate, aa.enddate, aa.assistant
, SUM(b.workedhours) AS workedhours
FROM aa
LEFT JOIN b ON b.ddate >= aa.startdate
AND b.ddate < aa.enddate
GROUP BY 1,2,3
;
Output:
CREATE TABLE
SET
INSERT 0 3
CREATE TABLE
INSERT 0 8
startdate | enddate | assistant | workedhours
------------+------------+-----------+-------------
2017-07-01 | 2018-01-03 | James | 24.0
2018-01-03 | 2018-01-06 | Frank | 24.5
2018-01-06 | infinity | Erika | 19.0
(3 rows)

Related

How to apply Excel operation into SQL server Query?

I am currently using SSMS 2008.
I would like to complete the operation, using SSMS and described in the Excel screenshot.
I have two tables joined, one having a positive count for when an employee's start working and one with a negative count for when the employee's leave. I am looking to have a column showing the count of employee's per hour.
I appreciate any help on this matter,
Thank you,
It is running total and could be implemented using windowed SUM:
SELECT *, SUM(Employee) OVER(ORDER BY [Date], [Time]) as Total_available
FROM tab
ORDER BY [Date], [Time];
An alternative method to SUM OVER is a self-join, with an aggregation on the lower or equal values.
Sample data:
CREATE TABLE TestEmployeeRegistration (
[Date] DATE,
[Time] TIME,
[Employees] INT NOT NULL DEFAULT 0,
PRIMARY KEY ([Date], [Time])
);
INSERT INTO TestEmployeeRegistration
([Date], [Time], [Employees]) VALUES
('2019-11-01', '08:00', 2),
('2019-11-01', '09:00', 5),
('2019-11-01', '10:00', 3),
('2019-11-01', '12:00',-5),
('2019-11-01', '13:00', 2),
('2019-11-01', '14:00',-5);
Query:
SELECT t.[Date], t.[Time], t.[Employees]
, SUM(t2.[Employees]) AS [Total available]
FROM [TestEmployeeRegistration] t
JOIN [TestEmployeeRegistration] t2
ON t2.[Date] = t.[Date]
AND t2.[Time] <= t.[Time]
GROUP BY t.[Date], t.[Time], t.[Employees]
ORDER BY t.[Date], t.[Time];
When using the window function of SUM, then I advice a partition by the "Date".
SELECT *
, SUM([Employees]) OVER (PARTITION BY [Date] ORDER BY [Time]) AS [Total available]
FROM [TestEmployeeRegistration]
ORDER BY [Date], [Time];
A test on rextester here
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE MyTable (Dates Date,Times Time, EmployeesAvailable int)
INSERT INTO MyTable (Dates,Times,EmployeesAvailable) VALUES('2019-11-01','08:00',2)
INSERT INTO MyTable (Dates,Times,EmployeesAvailable) VALUES('2019-11-01','09:00',5)
INSERT INTO MyTable (Dates,Times,EmployeesAvailable) VALUES('2019-11-01','10:00',3)
INSERT INTO MyTable (Dates,Times,EmployeesAvailable) VALUES('2019-11-01','12:00',-5)
INSERT INTO MyTable (Dates,Times,EmployeesAvailable) VALUES('2019-11-01','13:00',2)
INSERT INTO MyTable (Dates,Times,EmployeesAvailable) VALUES('2019-11-01','14:00',-5)
Query 1:
SELECT Dates,Times,EmployeesAvailable,
SUM(EmployeesAvailable) OVER(ORDER BY Dates,Times) AS 'Total Available'
FROM MyTable
Results:
| Dates | Times | EmployeesAvailable | Total Available |
|------------|------------------|--------------------|-----------------|
| 2019-11-01 | 08:00:00.0000000 | 2 | 2 |
| 2019-11-01 | 09:00:00.0000000 | 5 | 7 |
| 2019-11-01 | 10:00:00.0000000 | 3 | 10 |
| 2019-11-01 | 12:00:00.0000000 | -5 | 5 |
| 2019-11-01 | 13:00:00.0000000 | 2 | 7 |
| 2019-11-01 | 14:00:00.0000000 | -5 | 2 |

Number of Shifts betweens two times

Can you please help me with the following.
I am trying to calculate how may shifts a patient was in the hospital. The shifts timings start from 7:00 AM to 6:59 PM and 7:00 PM to 6.59 AM.
If a patient was admitted to a location after the start of the shift, we ignore that shift in the total calculation.
Here is the sample data and how the end result should look like :
DECLARE #T AS TABLE
(
ID INT,
LOCATION VARCHAR(10),
Date_entered DATETIME,
date_left datetime
);
DECLARE #endresult AS TABLE
(
ID INT,
LOCATION VARCHAR(10),
Date_entered DATETIME,
date_left datetime,
Total_shifts int
)
insert into #T VALUES
(1,'AB','01/01/2019 07:10','01/01/2019 20:30'),
(2,'CD','01/01/2019 20:30','01/04/2019 02:30'),
(3,'EF','01/04/2019 02:30','01/07/2019 19:30'),
(4,'GH','01/07/2019 19:30','01/08/2019 13:30')
insert into #endresult VALUES
(1,'AB','01/01/2019 07:10','01/01/2019 20:30',1),
(2,'CD','01/01/2019 20:30','01/04/2019 02:30',4),
(3,'EF','01/04/2019 02:30','01/07/2019 19:30',8),
(4,'GH','01/07/2019 19:30','01/08/2019 13:30',1)
SELECT * FROM #t
select * from #endresult
I tried using a recursive CTE, but the query is taking too much time to complete. Any simple way to calculate the timings?
Here is a query that returns the correct results for your sample data :
select
t.*,
DATEDIFF(DAY, date_entered, date_left) * 2
- CASE WHEN DATEPART(HOUR, date_entered) < 7 THEN 0 WHEN DATEPART(HOUR, date_entered) < 19 THEN 1 ELSE 2 END
+ CASE WHEN DATEPART(HOUR, date_left) < 7 THEN 0 WHEN DATEPART(HOUR, date_left) < 19 THEN 1 ELSE 2 END
AS Total_shifts
from #t t;
The logic is to first count how many days occured between entrance and exit, then multiply it by two to get a raw number of shifts ; then adjust the raw count by checking the times when the patient entered and exited.
This demo on DB Fiddle with your sample data returns :
ID | LOCATION | Date_entered | date_left | Total_shifts
-: | :------- | :------------------ | :------------------ | -----------:
1 | AB | 01/01/2019 07:10:00 | 01/01/2019 20:30:00 | 1
2 | CD | 01/01/2019 20:30:00 | 04/01/2019 02:30:00 | 4
3 | EF | 04/01/2019 02:30:00 | 07/01/2019 19:30:00 | 8
4 | GH | 07/01/2019 19:30:00 | 08/01/2019 13:30:00 | 1
Here's a query that will give you the results you want. It uses 3 CTEs, the first simply selects the minimum Date_entered and maximum date_left values, the second aligns the minimum Date_entered value to a shift start time (7AM or 7PM), and the third recursive CTE generates a list of all the shift start times between the minimum Date_entered and maximum date_left values. Finally we JOIN that CTE to the admissions table and count the number of shift start times between Date_entered and date_left:
WITH cte AS
(SELECT MIN(Date_entered) AS min_date, MAX(date_left) AS max_date
FROM T),
cte1 AS
(SELECT CASE WHEN DATEPART(hour, min_date) < 7 THEN DATEADD(hour, 19, DATEADD(day, -1, CONVERT(DATE, min_date)))
ELSE DATEADD(hour, 7, CONVERT(DATETIME, CONVERT(DATE, min_date)))
END AS min_shift
FROM cte),
shifts AS
(SELECT min_shift AS shift_start
FROM cte1
UNION ALL
SELECT DATEADD(hour, 12, shift_start)
FROM shifts
WHERE shift_start < (SELECT max_date FROM cte))
SELECT T.ID, T.LOCATION, T.Date_Entered, T.date_left, COUNT(s.shift_start) AS Total_shifts
FROM T
JOIN shifts s ON s.shift_start BETWEEN T.Date_Entered AND T.date_left
GROUP BY T.ID, T.LOCATION, T.Date_Entered, T.date_left
Output:
ID LOCATION Date_Entered date_left Total_shifts
1 AB 01/01/2019 07:10:00 01/01/2019 20:30:00 1
2 CD 01/01/2019 20:30:00 04/01/2019 06:59:00 4
3 EF 04/01/2019 07:00:00 07/01/2019 19:30:00 8
4 GH 07/01/2019 19:30:00 08/01/2019 13:30:00 1
Demo on dbfiddle

Fill in missing timestamp values in SQL

SQL newby here looking for a bit of help in writing a query.
Some sample data
Time Value
9:00 1.2
9:01 2.3
9:05 2.4
9:06 2.5
I need to fill in those missing times with zero - so the query would return
Time Value
9:00 1.2
9:01 2.3
9:02 0
9:03 0
9:04 0
9:05 2.4
9:06 2.5
Is this possible in T-SQL?
Thanks for any help / advice ...
One method uses a recursive CTE to generate the list of times and then use left join to bring in the values:
with cte as (
select min(s.time) as time, max(s.time) as maxt
from sample s
union all
select dateadd(minute, 1, cte.time), cte.maxt
from cte
where cte.time < cte.maxt
)
select cte.time, coalesce(s.value, 0)
from cte left join
sample s
on cte.time = s.time
order by cte.time;
Note that if you have more than 100 minutes, you will need option (maxrecursion 0) at the end of the query.
You can try to use recursive CTE make calendar table and OUTER JOIN base on that.
CREATE TABLE T(
[Time] Time,
Value float
);
insert into T values ('9:00',1.2);
insert into T values ('9:01',2.3);
insert into T values ('9:05',2.4);
insert into T values ('9:06',2.5);
Query 1:
with cte as (
SELECT MIN([Time]) minDt,MAX([Time] ) maxDt
FROM T
UNION ALL
SELECT dateadd(minute, 1, minDt) ,maxDt
FROM CTE
WHERE dateadd(minute, 1, minDt) <= maxDt
)
SELECT t1.minDt 'Time',
ISNULL(t2.[Value],0) 'Value'
FROM CTE t1
LEFT JOIN T t2 on t2.[Time] = t1.minDt
Results:
| Time | Value |
|------------------|-------|
| 09:00:00.0000000 | 1.2 |
| 09:01:00.0000000 | 2.3 |
| 09:02:00.0000000 | 0 |
| 09:03:00.0000000 | 0 |
| 09:04:00.0000000 | 0 |
| 09:05:00.0000000 | 2.4 |
| 09:06:00.0000000 | 2.5 |

Get a list of dates between few dates

There are some quite similar questions, but not the same.
I have to solve the next problem:
From table with such structure
| DATE_FROM | DATE_TO |
|------------|------------|
| 2010-05-17 | 2010-05-19 |
| 2017-01-02 | 2017-01-04 |
| 2017-05-01 | NULL |
| 2017-06-12 | NULL |
I need to get a list like the one below
| DATE_LIST |
|------------|
| 2010-05-17 |
| 2010-05-18 |
| 2010-05-19 |
| 2017-01-02 |
| 2010-01-03 |
| 2010-01-04 |
| 2017-05-01 |
| 2017-06-12 |
How can I get it with SQL? SQL Server 2016.
Another option is with a CROSS APPLY and an ad-hoc tally table
Select Date_List=B.D
from YourTable A
Cross Apply (
Select Top (DateDiff(DAY,[DATE_FROM],IsNull([DATE_TO],[DATE_FROM]))+1) D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),[DATE_FROM])
From master..spt_values n1,master..spt_values n2
) B
Returns
Date_List
2010-05-17
2010-05-18
2010-05-19
2017-01-02
2017-01-03
2017-01-04
2017-05-01
2017-06-12
One method uses a recursive CTE:
with cte as (
select date_from as date_list, date_to
from t
union all
select dateadd(day, 1, date_from), date_to
from cte
where date_from < date_to
)
select date_list
from cte;
By default, the recursive CTE is limited to a recursive depth of 100 (and then it returns an error). That works for spans of up to 100 days. You can remove the limit with OPTION (MAXRECURSION 0).
Although you could create the date range on the fly in your query, consider creating a permanent calendar table. This will provide better performance and can be extended with other attributes like day of week, fiscal quarter, etc. You can find many examples of loading such a table with an internet search.
Below is an example with 40 years of dates.
--example calendar table load script
CREATE TABLE dbo.Calendar(
CalendarDate date NOT NULL
CONSTRAINT PK_Calendar PRIMARY KEY
);
WITH
t4 AS (SELECT n FROM (VALUES(0),(0),(0),(0)) t(n))
,t256 AS (SELECT 0 AS n FROM t4 AS a CROSS JOIN t4 AS b CROSS JOIN t4 AS c CROSS JOIN t4 AS d)
,t64k AS (SELECT ROW_NUMBER() OVER (ORDER BY (a.n)) AS num FROM t256 AS a CROSS JOIN t256 AS b)
INSERT INTO dbo.Calendar WITH(TABLOCKX)
SELECT DATEADD(day, num, '20000101')
FROM t64k
WHERE DATEADD(day, num, '20000101') < '20400101'
GO
DECLARE #example TABLE(
DATE_FROM date NOT NULL
,DATE_TO date NULL
);
GO
--example query
INSERT INTO #example VALUES
('2010-05-17', '2010-05-19')
, ('2017-01-02', '2017-01-04')
, ('2017-05-01', NULL)
, ('2017-06-12', NULL)
SELECT
c.CalendarDate
FROM #example AS e
JOIN dbo.Calendar AS c ON
c.CalendarDate BETWEEN e.DATE_FROM AND COALESCE(e.DATE_TO, e.DATE_FROM);

SQL duration between two dates in different rows

I would really appreciate some assistance if somebody could help me construct a MSSQL Server 2000 query that would return the duration between a customer's A entry and their B entry.
Not all customers are expected to have a B record and so no results would be returned.
Customers Audit
+---+---------------+---+----------------------+
| 1 | Peter Griffin | A | 2013-01-01 15:00:00 |
| 2 | Martin Biggs | A | 2013-01-02 15:00:00 |
| 3 | Peter Griffin | C | 2013-01-05 09:00:00 |
| 4 | Super Mario | A | 2013-01-01 15:00:00 |
| 5 | Martin Biggs | B | 2013-01-03 18:00:00 |
+---+---------------+---+----------------------+
I'm hoping for results similar to:
+--------------+----------------+
| Martin Biggs | 1 day, 3 hours |
+--------------+----------------+
Something like the below (don't know your schema, so you'll need to change names of objects) should suffice.
SELECT ABS(DATEDIFF(HOUR, CA.TheDate, CB.TheDate)) AS HoursBetween
FROM dbo.Customers CA
INNER JOIN dbo.Customers CB
ON CB.Name = CA.Name
AND CB.Code = 'B'
WHERE CA.Code = 'A'
SELECT A.CUSTOMER, DATEDIFF(HOUR, A.ENTRY_DATE, B.ENTRY_DATE) DURATION
FROM CUSTOMERSAUDIT A, CUSTOMERSAUDIT B
WHERE B.CUSTOMER = A.CUSTOMER AND B.ENTRY_DATE > A.ENTRY_DATE
This is Oracle query but all features available in MS Server as far as I know. I'm sure I do not have to tell you how to concatenate the output to get desired result. All values in output will be in separate columns - days, hours, etc... And it is not always easy to format the output here:
SELECT id, name, grade
, NVL(EXTRACT(DAY FROM day_time_diff), 0) days
, NVL(EXTRACT(HOUR FROM day_time_diff), 0) hours
, NVL(EXTRACT(MINUTE FROM day_time_diff), 0) minutes
, NVL(EXTRACT(SECOND FROM day_time_diff), 0) seconds
FROM
(
SELECT id, name, grade
, (begin_date-end_date) day_time_diff
FROM
(
SELECT id, name, grade
, CAST(start_date AS TIMESTAMP) begin_date
, CAST(end_date AS TIMESTAMP) end_date
FROM
(
SELECT id, name, grade, start_date
, LAG(start_date, 1, to_date(null)) OVER (ORDER BY id) end_date
FROM stack_test
)
)
)
/
Output:
ID NAME GRADE DAYS HOURS MINUTES SECONDS
------------------------------------------------------------
1 Peter Griffin A 0 0 0 0
2 Martin Biggs A 1 1 0 0
3 Peter Griffin C 2 17 0 0
4 Super Mario A -3 -18 0 0
5 Martin Biggs A 2 3 0 0
The table structure/columns I used - it would be great if you took care of this and data in advance:
CREATE TABLE stack_test
(
id NUMBER
,name VARCHAR2(50)
,grade VARCHAR2(3)
,start_date DATE
)
/