SQL Server: DATEADD bypass dates based on the other table - sql

I have two tables
tbl_date1
date_str | int_add
12/1/2016 | 5
12/2/2016 | 2
12/4/2016 | 4
tbl_date2 (Dates to be bypassed)
date_bpass
12/3/2016
12/4/2016
Result:
date_str | int_add | final_date
12/1/2016 | 5 | 12/8/2016
12/2/2016 | 2 | 12/6/2016
12/4/2016 | 4 | 12/8/2016
I want the query to return the final_date column value wherein the final_date is the DATEADD of the date_str and int_add but should not include
the dates present in tbl_date2.
Thank You

Here is one way
SELECT date_str,
int_add,
Dateadd(dd, int_add + add_count, date_str)
FROM tbl_date1 a
OUTER apply (SELECT Count(1)
FROM tbl_date2 b
WHERE b.date_bpass BETWEEN Dateadd(dd, 1, a.date_str) AND Dateadd(dd, a.int_add, a.date_str)) cs (add_count)
Live Demo

Related

How to list uniques entries in a column and in the corresponding times of their repition in next column?

I have a hospital database which looks something like this:
id | patient_name | admitDate | DischargeDate
1 | john | 3/01/2011 08:50 | 5/01/2011 12:50
2 | lisa | 3/01/2011 09:50 | 4/01/2011 13:50
3 | ron | 5/01/2012 10:40 | 10/01/2012 03:50
4 | howard | 6/02/2013 08:05 | 10/02/2013 08:50
5 | john | 6/02/2013 12:04 | 7/02/2013 01:50
The admitDate is same for many entries (time may be different). I want to find out how many patients were admitted on any particular day so if I do this:
select distinct left(admitDate,10),
(select count(distinct left(admitDate,10) ) from hospital)
from hospital
I get output as all distinct admit dates in 1st column and same value 5 in all rows of second column. How do I make it so that only corresponding repetition count is found in 2nd column and not the count of entire admitDate set.
datatype of admitdate is varchar(50)
I am using left function because I only have to find out uniqueness in dates not in time.
Expected result:
admitDate | Count
3/01/2011 | 2
5/01/2012 | 1
6/02/2013 | 2
Current result:
admitDate | Count
3/01/2011 | 5
5/01/2012 | 5
6/02/2013 | 5
If your admitDate Column has Time too, you need use Convert() function to eliminate the time to group by your data per each day:
Select CONVERT(date, admitDate), count(*)
from hospital
group by CONVERT(date, admitDate);
If you use Varchar instead of Date data type for your admitDate column you can try this:
SELECT LEFT(admitDate, charindex(' ', admitDate) - 1) as ADMITDATE , count(*) as COUNTER
from hospital
group by LEFT(admitDate, charindex(' ', admitDate) - 1) ;
or:
SELECT convert(date, (convert(datetime2, admitDate,103)) ), count(*)
from hospital
group by convert(date, (convert(datetime2, admitDate,103)) )

SQL: Selecting data from multiple tables with multiple records to select and join

I have three tables: VolunteerRelationships, Organizations, and CampaignDates. I'm trying to write a query that will give me the organization id and name, and the org's start and end campaign dates for the current campaign year <#CampaignYear>, based on the selected volunteer <#SelectedInd>.
Dates are stored as separate column values for day, month and year which I'm trying to cast into a more an formatted date value. If I can get this, I'd also like to use a case statement to get the status of the campaign based on whether the date campaign dates are upcoming, currently running, or already closed, but need to get the first part of the query first.
Sorry if I'm leaving a lot of needed info out, this is my first time posting a question to this forumn. Thank you!
VolunteerRelationships
id | name | managesId |expiryDate
1 | john | 1 |
2 | jack | 2 |6/30/2020
3 | jerry| 3 |12/31/2021
Organizations
id | name1
1 | ACME
CampaignDates
orgId | dateDay | dateMonth | dateYear | dateType | Campaign Year
1 | 5 | 11 | 2020 | Start | 2020
1 | 15 | 11 | 2020 | End | 2020
Result
orgId | orgName | startDate | endDate | Status
1 | ACME | 2020-01-01| 2020-01-15 | Closed
select
v.MANAGEDACCOUNT,
o.Name1,
select * from
(select cast(cast dateyear*1000 + datemonth*100 + dateday as varchar(255)) as date as date1 from <#Schema>.CampaignDates where datetype = 'Start' and campaignyear = <#CampaignYear> and orgaccountnumber = v.MANAGEDACCOUNT) d1,
(select cast(cast dateyear*1000 + datemonth*100 + dateday as varchar(255)) as date as date2 from <#Schema>.CampaignDates where datetype = 'End' and campaignyear = <#CampaignYear> and orgaccountnumber = v.MANAGEDACCOUNT) d2
from <#Schema>.VolunteerRelationships v
inner join <#Schema>.organizations o
on o.accountnumber=v.MANAGEDACCOUNT
where v.VOLUNTEERACCOUNT = <#SelectedInd> and ( v.EXPIRYDATE IS NULL OR v.EXPIRYDATE > <#Today> )

Calculating working minutes for Normal and Night Shift

I am making a query to fetch the working minutes for employees. The problem I have is the Night Shift. I know that I need to subtract the "ShiftStartMinutesFromMidnight" but I can't find the right logic.
NOTE: I can't changing the database, I only can use the data from it.
Let's say I have these records.
+----+--------------------------+----------+
| ID | EventTime | ReaderNo |
-----+--------------------------+----------+
| 1 | 2019-12-04 11:28:46.000 | In |
| 1 | 2019-12-04 12:36:17.000 | Out |
| 1 | 2019-12-04 12:39:23.000 | In |
| 1 | 2019-12-04 12:51:21.000 | Out |
| 1 | 2019-12-05 07:37:49.000 | In |
| 1 | 2019-12-05 08:01:22.000 | Out |
| 2 | 2019-12-04 22:11:46.000 | In |
| 2 | 2019-12-04 23:06:17.000 | Out |
| 2 | 2019-12-04 23:34:23.000 | In |
| 2 | 2019-12-05 01:32:21.000 | Out |
| 2 | 2019-12-05 01:38:49.000 | In |
| 2 | 2019-12-05 06:32:22.000 | Out |
-----+--------------------------+----------+
WITH CT AS (SELECT
EIn.PSNID, EIn.PSNNAME
,CAST(DATEADD(minute, -0, EIn.EventTime) AS date) AS dt
,EIn.EventTime AS LogIn
,CA_Out.EventTime AS LogOut
,DATEDIFF(minute, EIn.EventTime, CA_Out.EventTime) AS WorkingMinutes
FROM
VIEW_EVENT_EMPLOYEE AS EIn
CROSS APPLY
(
SELECT TOP(1) EOut.EventTime
FROM VIEW_EVENT_EMPLOYEE AS EOut
WHERE
EOut.PSNID = EIn.PSNID
AND EOut.ReaderNo = 'Out'
AND EOut.EventTime >= EIn.EventTime
ORDER BY EOut.EventTime
) AS CA_Out
WHERE
EIn.ReaderNo = 'In'
)
SELECT
PSNID
,PSNNAME
,dt
,LogIn
,LogOut
,WorkingMinutes
FROM CT
WHERE dt BETWEEN '2019-11-29' AND '2019-12-05'
ORDER BY LogIn
;
OUTPUT FROM QUERY
+----+------------+-------------------------+-------------------------+----------------+
| ID | date | In | Out | WorkingMinutes |
-----+------------+-------------------------+-------------------------+----------------+
| 1 | 2019-12-04 | 2019-12-04 11:28:46.000 | 2019-12-04 12:36:17.000 | 68 |
| 1 | 2019-12-04 | 2019-12-04 12:39:23.000 | 2019-12-04 12:51:21.000 | 12 |
| 1 | 2019-12-05 | 2019-12-05 07:37:49.000 | 2019-12-05 08:01:22.000 | 24 |
-----+------------+-------------------------+-------------------------+----------------+
I was thinking something like this. When Out is between 06:25 - 6:40. But I also need to check If employee, previous day has In between 21:50 - 22:30. I need that second condition because some employee from first shift maybe can Out, for example at 6:30.
*(1310 is the ShiftStartMinutesFromMidnight
Line 3 of Query
CAST(DATEADD(minute, -0, EIn.EventTime) AS date) AS dt
Updating the Line 3 with this code.
CASE
WHEN CAST(CA_Out.LogDate AS time) BETWEEN '06:25:00' AND '06:40:00'
AND CAST(EIn.LogDate AS time) BETWEEN '21:50:00' AND '22:30:00' THEN CAST(DATEADD(minute, -1310, EIn.LogDate) AS date)
ELSE CAST(DATEADD(minute, -0, EIn.LogDate) AS date)
END as dt
Expected Output
+----+------------+-------------------------+-------------------------+----------------+
| ID | date | In | Out | WorkingMinutes |
-----+------------+-------------------------+-------------------------+----------------+
| 2 | 2019-12-04 | 2019-12-04 22:11:46.000 | 2019-12-04 23:06:17.000 | 55 |
| 2 | 2019-12-04 | 2019-12-04 23:34:23.000 | 2019-12-05 01:32:21.000 | 118 |
| 2 | 2019-12-04 | 2019-12-05 01:38:49.000 | 2019-12-05 06:32:22.000 | 294 |
-----+------------+-------------------------+-------------------------+----------------+
Assuming that total minutes per separate date is enough:
WITH
/* enumerate pairs */
cte1 AS ( SELECT *,
COUNT(CASE WHEN ReaderNo = 'In' THEN 1 END)
OVER (PARTITION BY ID
ORDER BY EventTime) pair
FROM test ),
/* divide by pairs */
cte2 AS ( SELECT ID, MIN(EventTime) starttime, MAX(EventTime) endtime
FROM cte1
GROUP BY ID, pair ),
/* get dates range */
cte3 AS ( SELECT CAST(MIN(EventTime) AS DATE) minDate,
CAST(MAX(EventTime) AS DATE) maxDate
FROM test),
/* generate dates list */
cte4 AS ( SELECT minDate theDate
FROM cte3
UNION ALL
SELECT DATEADD(dd, 1, theDate)
FROM cte3, cte4
WHERE theDate < maxDate ),
/* add overlapped dates to pairs */
cte5 AS ( SELECT ID, starttime, endtime, theDate
FROM cte2, cte4
WHERE theDate BETWEEN CAST(starttime AS DATE) AND CAST(endtime AS DATE) ),
/* adjust borders */
cte6 AS ( SELECT ID,
CASE WHEN starttime < theDate
THEN theDate
ELSE starttime
END starttime,
CASE WHEN CAST(endtime AS DATE) > theDate
THEN DATEADD(dd, 1, theDate)
ELSE endtime
END endtime,
theDate
FROM cte5 )
/* calculate total minutes per date */
SELECT ID,
theDate,
SUM(DATEDIFF(mi, starttime, endtime)) workingminutes
FROM cte6
GROUP BY ID,
theDate
ORDER BY 1,2
fiddle
The solution is specially made detailed, step by step, so that you can easily understand the logic.
You may freely combine some CTEs into one. You may also use pre-last cte5 combined with cte2 if you need the output strongly as shown.
The solution assumes that none records are lost in source data (each 'In' matches strongly one 'Out' and backward, and no adjacent or overlapped pairs).
Don't know where you stopped but here is how I do,
Night shift 20:00 - 05:00 so in one day 00:00 - 5:00; 22:00 - 24:00
day shift 5:00 - 22:00
To get easier overlapping checking you need to change all dates to unix timestamp. so you don't have to split time intervals like shown above
So generate map of each period work for fetch period date_from and date_till, make sure to add holiday and pre-holiday exceptions where periods are different
something like:
Unix values is only for understanding.
unix_from_tim, unix_till_tim, shift_type
1580680800, 1580680800, 1 => example 02-02-2020:22:00:00, 03-02-2020:05:00:00, 1
1580680800, 1580680800, 0 => example 03-02-2020:05:00:00, 03-02-2020:22:00:00, 0
1580680800, 1580680800, 1 => example 03-02-2020:22:00:00, 04-02-2020:05:00:00, 1
...
Make sure you don't calculate overlapping minutes on period start/end..
And there is worker one row
with unix_from_tim, unix_from_tim
1580680800, 1580680800=> something like 02-02-2020:16:30:00, 03-02-2020:07:10:00
When you check overlapping you can get ms like this:
MIN(work_period:till,worker_period:till) - MAX(work_period:from, worker_period:from);
example in simple numbers:
work_period 3 - 7
worker_period 5 - 12
MIN(7,12) - MAX(3,5) = 7 - 5 = 2 //overlap
work_period 3 - 7
worker_period 8 - 12
MIN(7,12) - MAX(3,8) = 7 - 8 = -1 //if negative not overlap!
work_period 3 - 13
worker_period 8 - 12
MIN(13,12) - MAX(3,8) = 13 - 8 = 5 //full overlap!
And you have to check each worker period on all overlaping time generated work intervals.
May be someone can make select where you don't have to generate work_shift overlapping but its not a easy task if you add more holidays, transferred days, reduced time days etc.
Hope it helps

How to write a SQL statement to sum data using group by the same day of every two neighboring months

I have a data table like this:
datetime data
-----------------------
...
2017/8/24 6.0
2017/8/25 5.0
...
2017/9/24 6.0
2017/9/25 6.2
...
2017/10/24 8.1
2017/10/25 8.2
I want to write a SQL statement to sum the data using group by the 24th of every two neighboring months in certain range of time such as : from 2017/7/20 to 2017/10/25 as above.
How to write this SQL statement? I'm using SQL Server 2008 R2.
The expected results table is like this:
datetime_range data_sum
------------------------------------
...
2017/8/24~2017/9/24 100.9
2017/9/24~2017/10/24 120.2
...
One conceptual way to proceed here is to redefine a "month" as ending on the 24th of each normal month. Using the SQL Server month function, we will assign any date occurring after the 24th as belonging to the next month. Then we can aggregate by the year along with this shifted month to obtain the sum of data.
WITH cte AS (
SELECT
data,
YEAR(datetime) AS year,
CASE WHEN DAY(datetime) > 24
THEN MONTH(datetime) + 1 ELSE MONTH(datetime) END AS month
FROM yourTable
)
SELECT
CONVERT(varchar(4), year) + '/' + CONVERT(varchar(2), month) +
'/25~' +
CONVERT(varchar(4), year) + '/' + CONVERT(varchar(2), (month + 1)) +
'/24' AS datetime_range,
SUM(data) AS data_sum
FROM cte
GROUP BY
year, month;
Note that your suggested ranges seem to include the 24th on both ends, which does not make sense from an accounting point of view. I assume that the month includes and ends on the 24th (i.e. the 25th is the first day of the next accounting period.
Demo
I would suggest dynamically building some date range rows so that you can then join you data to those for aggregation, like this example:
+----+---------------------+---------------------+----------------+
| | period_start_dt | period_end_dt | your_data_here |
+----+---------------------+---------------------+----------------+
| 1 | 24.04.2017 00:00:00 | 24.05.2017 00:00:00 | 1 |
| 2 | 24.05.2017 00:00:00 | 24.06.2017 00:00:00 | 1 |
| 3 | 24.06.2017 00:00:00 | 24.07.2017 00:00:00 | 1 |
| 4 | 24.07.2017 00:00:00 | 24.08.2017 00:00:00 | 1 |
| 5 | 24.08.2017 00:00:00 | 24.09.2017 00:00:00 | 1 |
| 6 | 24.09.2017 00:00:00 | 24.10.2017 00:00:00 | 1 |
| 7 | 24.10.2017 00:00:00 | 24.11.2017 00:00:00 | 1 |
| 8 | 24.11.2017 00:00:00 | 24.12.2017 00:00:00 | 1 |
| 9 | 24.12.2017 00:00:00 | 24.01.2018 00:00:00 | 1 |
| 10 | 24.01.2018 00:00:00 | 24.02.2018 00:00:00 | 1 |
| 11 | 24.02.2018 00:00:00 | 24.03.2018 00:00:00 | 1 |
| 12 | 24.03.2018 00:00:00 | 24.04.2018 00:00:00 | 1 |
+----+---------------------+---------------------+----------------+
DEMO
declare #start_dt date;
set #start_dt = '20170424';
select
period_start_dt, period_end_dt, sum(1) as your_data_here
from (
select
dateadd(month,m.n,start_dt) period_start_dt
, dateadd(month,m.n+1,start_dt) period_end_dt
from (
select #start_dt start_dt ) seed
cross join (
select 0 n union all
select 1 union all
select 2 union all
select 3 union all
select 4 union all
select 5 union all
select 6 union all
select 7 union all
select 8 union all
select 9 union all
select 10 union all
select 11
) m
) r
-- LEFT JOIN YOUR DATA
-- ON yourdata.date >= r.period_start_dt and data.date < r.period_end_dt
group by
period_start_dt, period_end_dt
Please don't be tempted to use "between" when it comes to joining to your data. Follow the note above and use yourdata.date >= r.period_start_dt and data.date < r.period_end_dt otherwise you could double count information as between is inclusive of both lower and upper boundaries.
I think the simplest way is to subtract 25 days and aggregate by the month:
select year(dateadd(day, -25, datetime)) as yr,
month(dateadd(day, -25, datetime)) as mon,
sum(data)
from t
group by dateadd(day, -25, datetime);
You can format yr and mon to get the dates for the specific ranges, but this does the aggregation (and the yr/mon columns might be sufficient).
Step 0: Build a calendar table. Every database needs a calendar table eventually to simplify this sort of calculation.
In this table you may have columns such as:
Date (primary key)
Day
Month
Year
Quarter
Half-year (e.g. 1 or 2)
Day of year (1 to 366)
Day of week (numeric or text)
Is weekend (seems redundant now, but is a huge time saver later on)
Fiscal quarter/year (if your company's fiscal year doesn't start on Jan. 1)
Is Holiday
etc.
If your company starts its month on the 24th, then you can add a "Fiscal Month" column that represents that.
Step 1: Join on the calendar table
Step 2: Group by the columns in the calendar table.
Calendar tables sound weird at first, but once you realize that they are in fact tiny even if they span a couple hundred years they quickly become a major asset.
Don't try to cheap out on disk space by using computed columns. You want real columns because they are much faster and can be indexed if necessary. (Though honestly, usually just the PK index is enough for even wide calendar tables.)

How to update a table with 2 random values, check in and check out dates/columns

In SQL Server how can I update a LibraryTtable with 3 random values, that follow a sequence of books in a lib.
The library system has to calculate fines, based on 3 factors checkout, allowed duration (books 30 days, mags 7 days), and finally checkin date.
For e.g.
First a Col1 -> Check-Out Date then based on a random NEWID()) % 30 days col2 -> Check-in Date and Col3 -> Originally allowed Duration (can be 7 to 30)
I can do this,
UPDATE [dbo].LibraryTtable
SET Check-Out = DATEADD(DAY, ABS(CHECKSUM(NEWID()) % 370), '2015-02-01')
but how do I hold that in a temp variable and add a random range within a span of 30 days for the check-in date
I can get the random values independently, but I don't know how to store and use them sequentially together.
I would write an UPDATE query via the CTE.
Sample data
DECLARE #LibraryTable TABLE (CheckOut date, CheckIn date, AllowedDuration int, ActualDuration int);
INSERT INTO #LibraryTable (CheckOut, CheckIn, AllowedDuration, ActualDuration) VALUES
(GETDATE(), GETDATE(), 30, 0),
(GETDATE(), GETDATE(), 30, 0),
(GETDATE(), GETDATE(), 30, 0),
(GETDATE(), GETDATE(), 30, 0),
(GETDATE(), GETDATE(), 30, 0);
Query
WITH
CTE
AS
(
SELECT
CheckOut
,CheckIn
,ActualDuration
,ABS(CHECKSUM(NEWID())) % 370 AS RandomNumber370
,ABS(CHECKSUM(NEWID())) % 30 AS RandomNumber30
FROM #LibraryTable
)
UPDATE CTE
SET
CheckOut = DATEADD(day, RandomNumber370, '2015-02-01')
,CheckIn = DATEADD(day, RandomNumber370 + RandomNumber30, '2015-02-01')
,ActualDuration = RandomNumber30
;
Result
SELECT *
FROM #LibraryTable;
+------------+------------+-----------------+----------------+
| CheckOut | CheckIn | AllowedDuration | ActualDuration |
+------------+------------+-----------------+----------------+
| 2015-09-07 | 2015-09-10 | 30 | 3 |
| 2015-11-25 | 2015-12-16 | 30 | 21 |
| 2015-06-16 | 2015-06-24 | 30 | 8 |
| 2015-11-25 | 2015-12-07 | 30 | 12 |
| 2015-03-25 | 2015-03-29 | 30 | 4 |
+------------+------------+-----------------+----------------+