Calculating days of therapy in a sql query - sql

I'm attempting to calculate days of therapy by month from an oracle database. The (vastly simplified) data is as follows:
Therapies
+-----------+-----------+----------+
| Rx Number | StartDate | StopDate |
|-----------+-----------+----------|
| 1 | 12-29-14 | 1-10-15 |
| 2 | 1-2-15 | 1-14-15 |
| 3 | 1-29-15 | 2-15-15 |
+-----------+-----------+----------+
For the purposes of this example, all times are assumed to be midnight. The total days of therapy in this table is (10-1 + 32-29) + (14-2) + (15-1 + 32-29) = 41. The total days of therapy in January in this table is (10-1) + (14-2) + (32-29) = 24.
If I wanted to calculate days of therapy for the month of January , my best effort is the following query:
SELECT SUM(stopdate - startdate)
FROM therapies
WHERE startdate > to_date('01-JAN-15')
AND stopdate < to_date ('01-FEB-15');
However, rx's 1 and 3 are not captured at all. I could try the following instead:
SELECT SUM(stopdate - startdate)
FROM therapies
WHERE stopdate > to_date('01-JAN-15')
AND startdate < to_date ('01-FEB-15');
But that would include the full duration of the first and third therapies, not just the portion in January. To make the matter more complex, I need these monthly summaries over a period of two years. So my questions are:
How do I include overhanging therapies such that only the portion within the target time period is included, and
How do I automatically generate these monthly summaries over a two year period?

How do I include overhanging therapies such that only the portion
within the target time period is included?
select sum(
greatest(least(stopdate, date '2015-01-31' + 1)
- greatest(startdate, date '2015-01-01'), 0)) suma
from therapies
How do I automatically generate these monthly summaries over a two
year period?
with period as (select date '2014-01-01' d1, date '2015-12-31' d2 from dual),
months as (select trunc(add_months(d1, level-1), 'Month') dt
from period connect by add_months(d1, level-1)<d2)
select to_char(dt, 'yyyy-mm') mth,
sum(greatest(least(stopdate, add_months(dt, 1)) - greatest(startdate, dt), 0)) suma
from therapies, months
group by to_char(dt, 'yyyy-mm') order by mth
Above queries produced desired output. Please insert your dates in proper places to change analyzed periods.
In second SQL inner subquery months gives 24 dates, one for each month. The rest is only maneuvering
with functions greatest(),least() and some math.

Use a case statement to set the start date and stop date. Like the below:
select sum(
Stopdate -
(case Startdate
when startdate < to_date(#YourBeginingDate) then To_date(#YourBeginingDate)
else startdate
end)
FROM therapies
WHERE stopdate > to_date(#YourBeginingDate)
AND StartDate < to_date(#YourEndingDate)

I would do something like the following:
WITH t1 AS (
SELECT 1 AS rx, DATE'2014-12-29' AS start_date
, DATE'2015-01-10' AS stop_date
FROM dual
UNION ALL
SELECT 2, DATE'2015-01-02', DATE'2015-01-14'
FROM dual
UNION ALL
SELECT 3, DATE'2015-01-29', DATE'2015-02-15'
FROM dual
)
SELECT TRUNC(rx_dt, 'MONTH') AS rx_month, SUM(rx_cnt) AS rx_day_cnt
FROM (
SELECT rx_dt, COUNT(*) AS rx_cnt
FROM (
SELECT rx, start_date + LEVEL - 1 AS rx_dt
FROM t1
CONNECT BY start_date + LEVEL - 1 < stop_date
AND PRIOR rx = rx
AND PRIOR DBMS_RANDOM.VALUE IS NOT NULL
) GROUP BY rx_dt
) GROUP BY TRUNC(rx_dt, 'MONTH')
ORDER BY rx_month
Results:
12/1/2014 12:00:00 AM 2
1/1/2015 12:00:00 AM 24
2/1/2015 12:00:00 AM 15
See SQL Fiddle here.
What I am doing is using LEVEL and CONNECT BY to get all the days of therapy based on start_date and stop_date (not inclusive). I then GROUP BY the therapy date (rx_dt) to handle the overlapping therapies. Then I GROUP BY the month of the therapy using the TRUNC() function.
This should work just fine over a two-year period (or more); just add that filter before the last GROUP BY:
WHERE rx_dt >= DATE'2014-01-01'
AND rx_dt < DATE'2016-01-01'
GROUP BY TRUNC(rx_dt, 'MONTH')
Note that if your primary key is composite, you should include all the columns in the CONNECT BY clause:
CONNECT BY start_date + LEVEL - 1 < stop_date
AND PRIOR rx = rx
AND PRIOR patient_id = patient_id
--etc.

This is a bit tricky, as you need to capture days from sessions that:
Begin before the month and end after the month
Begin before the month and end during the month
Begin during the month and end after the month
Begin during the month and end during the month
To get those sessions, you can use a WHERE statement like this (the # symbol means that those are variables being passed in):
*examples are in TSQL, PLSQL might have somewhat different syntax
WHERE startdate < #endDate AND stopdate > #startDate
That should capture all four of those scenarios that I listed.
Then you only need to capture days that occurred during the month. I do this with a query that replaces the startdate/enddate with the date range limits if they exceed the range, like this:
SELECT
CASE WHEN enddate > #endDate then #endDate ELSE enddate END -
CASE WHEN startdate < #startDate THEN #startDate ELSE startdate END
So your whole query should look like this:
SELECT
SUM(
CASE WHEN enddate > #endDate then #endDate ELSE enddate END -
CASE WHEN startdate < #startDate THEN #startDate ELSE startdate END
)
FROM therapies
WHERE startdate < #endDate AND stopdate > #startDate
If you want to run that for two years, toss that code in a function that accepts #startDate and #endDate parameters, then call it from a query that gives you two years worth of months, like this:
WITH dateCTE AS (
SELECT
GETDATE() AS StartDate,
DATEADD(Month, 1, GETDATE()) AS EndDate
UNION ALL
SELECT
DATEADD(MONTH, -1, StartDate),
DATEADD(MONTH, -1, EndDate)
FROM dateCTE
WHERE StartDate > DATEADD(YEAR, -2, GETDATE())
)
SELECT
StartDate,
EndDate,
SomeFunction(StartDate, EndDate)
FROM dateCTE

Related

How to return values of two date ranges, from the same date column and value column, in two different columns as a result?

I have following data in my table,
Table = BillHeader
Sales column = Sales
Date column = CreateDate
Location name = Location
Result needed:
Location
Sum_of_Sale_1
Sum_of_Sale_2
Sum_of_Sale_1 = Sum of Sales up to yesterday for this month.
Sum_of_Sale_2 = Sum of Sales up to same date range as Sum_of_Sale_1 during last month.
For example, if today is 20th of June, Sum_of_Sale_1 = Sum of sales from 1st June to 19th of June
and Sum_of_Sale_2 = Sum of sales from 1st May to 19th of May.
Basically what I need is these two results of different date ranges, which should be selected form the same three columns, should appear next to each other in the result. I want to know how the sales performance was last month's same date range as to this month's date range (up to yesterday for this month).
Thanks!!
EDIT - 1
Here is the actual current working code:
SET #FDM = DATEADD(mm, DATEDIFF(mm, 0, GETDATE()), 0)
SELECT sum ([LAB_TRN_BillHeader].[AmountToBePaid]) as Total_Sale
,LAB.dbo.[LAB_TRN_BillHeader].[CollectingCenterCode]
,LAB.dbo.[LAB_Comm_MST_CollectingCenter].[Name]
,LAB.dbo.[LAB_Comm_MST_Branch].[BranchName]
FROM Lab.dbo.[LAB_TRN_BillHeader]
INNER JOIN LAB.dbo.[LAB_Comm_MST_CollectingCenter] on LAB.dbo.[LAB_TRN_BillHeader].[CollectingCenterCode] = LAB.dbo.[LAB_Comm_MST_CollectingCenter].[CollectingCenterCode]
INNER JOIN LAB.dbo.[LAB_Comm_MST_Branch] on LAB.dbo.[LAB_TRN_BillHeader].[BranchCode] = LAB.dbo.[LAB_Comm_MST_Branch].[BranchCode]
WHERE Date between #FDM and DATEADD(day,0, CAST(GETDATE() AS date)) and {{select_Laboratory}} and LAB.dbo.[LAB_TRN_BillHeader].[IsVoid] = '0' and LAB.dbo.[LAB_TRN_BillHeader].[CollectingCenterCode] in ('URCR022','MRPMC','KUCC','KOCC','EHECC')
GROUP BY LAB.dbo.[LAB_TRN_BillHeader].[CollectingCenterCode], LAB.dbo.[LAB_Comm_MST_CollectingCenter].[Name], LAB.dbo.[LAB_Comm_MST_Branch].[BranchName]
Current Result:
|Total_Sale|CollectingCenterCode|Name|BranchName|
|xxx |xxx |x |xx |
Required Result:
|Total_Sale|Total_Sale2|CollectingCenterCode|Name|BranchName|
|xxx |xxx |xx |x |xx |
Total_Sale = Sale of current month up to yesterday
Total_Sale2 = Sale of Last month up to current month's yesterday's date.
-- MSSQL Version - 2014
-- <Create_Date> is a time stamp in the table in <Create_Date> column. The date/time is obtained from that timestamp. Each transaction is saved with a respective timestamp at it's time of occurrence.
-- {{select_Laboratory}} is a field filter alias in Metabase (this code was copied from a Metabase dashboard). The actual code is LAB.dbo.[LAB_TRN_BillHeader].[BranchCode] = '001'
Considering a sales CreateDate is likely of type Datetime or Datetime2, a safe approach would be:
DECLARE #yesterday DATE = GETDATE();
DECLARE #lastMonth DATE = DATEADD(MONTH, -1, #yesterday);
DECLARE #firstDayOfThisMonth DATE = DATEADD(DAY, 1 - DAY(#yesterday), #yesterday);
DECLARE #firstDayOfLastMonth DATE = DATEADD(DAY, 1 - DAY(#lastMonth), #lastMonth);
SELECT #yesterday,
#firstDayOfThisMonth,
#lastMonth,
#firstDayOfLastMonth;
SELECT [locationId],
SUM( CASE
WHEN CreateDate >= #firstDayOfThisMonth
AND CreateDate < #yesterday THEN
AmountToBePaid
END
) AS Sum_of_Sale_1,
SUM( CASE
WHEN CreateDate >= #firstDayOfLastMonth
AND CreateDate < #lastMonth THEN
AmountToBePaid
END
) AS Sum_of_Sale_2
FROM BillHeader
GROUP BY [locationId];
EDIT: Note that in dates like March 31,30 previous month's end date could be Feb 28, 29.
You could use conditional aggregation with the following date functions:
DATEADD(Day, 1, EOMONTH(GETDATE(), -1)) gets the first date of the current month, i.e. current month is Jan-2023 it will return '2023-01-01'.
CAST(GETDATE() AS DATE) gets today's date.
DATEADD(Day, 1, EOMONTH(GETDATE(), -2)) gets the first date of the previous month, i.e. current month is Jan-2023 it will return '2022-12-01'.
DATEADD(Month, -1, CAST(GETDATE() AS DATE) gets the date of the day one-month pre today's date.
SELECT Location,
SUM(CASE
WHEN CreateDate >= DATEADD(Day, 1, EOMONTH(GETDATE(), -1)) AND
CreateDate < CAST(GETDATE() AS DATE)
THEN Sales END) Sum_of_Sale_1,
SUM(CASE
WHEN CreateDate >= DATEADD(Day, 1 ,EOMONTH(GETDATE(), -2)) AND
CreateDate < DATEADD(Month, -1, CAST(GETDATE() AS DATE))
THEN Sales END) Sum_of_Sale_2
FROM BillHeader
GROUP BY Location
See demo
There are different ways of doing the calculations and I don't know if this logic will seem more straightforward or as concise. Also you might not be able to use variables. Lastly, you didn't specify which version you're running.
Although you can incorporate this into an existing query that covers a wider range of dates, here is a stand-alone option that should restrict the execution to a narrower range of dates. The case logic, as written, does assume that rows have already been filtered so the only thing left to determine is whether the sale comes from current month or prior month.
select Location,
sum(case when month(CreateDate) <> month(getdate()) then Sales end) as Sales1,
sum(case when month(CreateDate) = month(getdate()) then Sales end) as Sales2
from BillHeader
where
-- go back enough days to guarantee covering last two months
-- this may be able to utilize an index
SalesDate between dateadd(day, -30 - day(getdate()), cast(getdate() as date))
and getdate()
-- now eliminate extra dates that are not relevant
and month(getdate()) - month(CreateDate) in (0, 1, -11) /* year might roll over */
and day(getdate()) > day(CreateDate)
group by Location;
For a YOY comparison over the same calendar month:
select Location,
sum(case when year(CreateDate) <> year(getdate()) then Sales end) as Sales1,
sum(case when year(CreateDate) = year(getdate()) then Sales end) as Sales2
from BillHeader
where
-- go back enough days to guarantee covering last 13 months
-- (or rewind as 396 days via parallel logic from earlier)
SalesDate between
dateadd(year, -1, dateadd(day, 1 - day(getdate()), cast(getdate() as date)))
and getdate()
and month(getdate()) = month(CreateDate)
and day(getdate()) > day(CreateDate)
group by Location;
This might be better done as a union with two different date ranges combined together:
-- ...
where
SalesDate between
dateadd(year, -1, dateadd(day, 1 - day(getdate()), cast(getdate() as date)))
and dateadd(year, -1, getdate())
-- ... union all ...
where
SalesDate between
dateadd(day, 1 - day(getdate()), cast(getdate() as date))
and getdate()
-- ...

Get difference days between two date without specific days

I am trying to write a query which will work out the difference between two dates but without counting specific days.
select ((DATEPART(weekday,(DATEDIFF(DAY,StartDate,EndDate)))) NOT IN (1,5)) FROM MyTable
In this example I'm trying to remove Monday and Friday from difference days (StartDate EndDate)
Ideally you need calendar table, to do these kinds of calculations easier. Below, I am generating the dates for the range and then counting the days accordingly.
DECLARE #startDate DATE = '2020-07-01'
DECLARE #EndDate DATE = '2020-07-13'
;with AllDates AS
(
SELECT #StartDate AS DateOf
UNION ALL
SELECT DateAdd(day,1,DateOf)
FROM AllDates
WHERE DateOf<#EndDate
)
select SUM(CASE WHEN DATENAME(weekday,dateof) in ('Monday','Friday') THEN 0 ELSE 1 END) as SumOfDays
FROM AllDates
+-----------+
| SumOfDays |
+-----------+
| 9 |
+-----------+

SQL Server select date year/month portion only without day and with comparison

I have a list in a table with a StartDate (not null) and an EndDate (null) (both of type date without time).
An active entry is one with StartDate <= querydate and EndDate with null or > querydate.
So my query would basically be like
SELECT *
FROM MyDataTable
WHERE StartDate <= mydate
AND (EndDate IS NULL OR EndDate >= mydate )
For a statistic in a diagram, I want to know which ones are active in a specific month for the last months. I loop (programatically) over the year and month (2017-10, 2017-11, 2017-12, 2018-1, ... )
How can I select all active entries using year and month of the program loop but ignoring the day part?
If you want actives for the entire month:
SELECT *
FROM MyDataTable
WHERE StartDate <= '2017-10-01' AND
(EndDate IS NULL OR EndDate >= '2017-11-01');
If you want actives at any time during the month:
SELECT *
FROM MyDataTable
WHERE StartDate < '2017-11-01' AND
(EndDate IS NULL OR EndDate > '2017-10-01');
I think you can achieve this by using only months by using 'DATEDIFF ( datepart , startdate , enddate )':
DECLARE #startDate DATE = '2017-11-01';
DECLARE #numberOfMonth INT;
SELECT #numberOfMonth = datediff(month, #startDate, GETDATE());
SELECT *
FROM MyDataTable
WHERE datediff(month, StartDate, GETDATE()) >= #numberOfMonth
AND (EndDate IS NULL OR datediff(month, EndDate, GETDATE()) <=
#numberOfMonth)

Unexpected behaviour of DATEPART

Script that lists dates and week number of date:
DECLARE #initDate DATE = '2014-01-01';
DECLARE #endDate DATE = '2014-12-31';
WITH dates (date, week)
AS (SELECT #initDate date,
Datepart(ww, #initDate) week
UNION ALL
SELECT Dateadd(ww, 1, t.date),
Datepart(ww, t.date)
FROM dates t
WHERE t.date < #endDate)
SELECT dates.date,
dates.week
FROM dates
The first three rows are:
date: week number
---------- -----------
2014-01-01 1
2014-01-08 1
2014-01-15 2
..... ..
..... ..
I guess it should be 2 for the second row and 3 for the first, isn't it?
Is it some kind of bug in DATEPART? Even if something is depends on first day of the year, first row date differs from second on one week with any settings.
Could you clarify this, please?
It is because of Recursive CTE not because of Datepart.
In the Recursive part of CTE week is still holding the previous week not the new date that generated in recursive part
Try changing your query like this.
DECLARE #initDate DATE = '2014-01-01';
DECLARE #endDate DATE = '2014-12-31';
WITH dates (date, week)
AS (SELECT #initDate date,
Datepart(ww, #initDate) week
UNION ALL
SELECT Dateadd(ww, 1, t.date),
Datepart(ww, Dateadd(ww, 1, t.date))
FROM dates t
WHERE t.date < #endDate)
SELECT dates.date,
dates.week
FROM dates

How to find all fridays and holidays between two dates

The table:
hDate Holiday
17/12/2011 National Day
01/01/2012 New Year
....
From the table, i want to find the total number of holidays between two dates:
A query like:
select count(hdate)
from table1
where hdate between '" & start_date & "' and '" & end_date & "'
User input:
start_date = '16/12/2011'
end_date = '15/01/2012'
and also I want to find the friday between 2 dates.
For finding fridays, how to create a query?
Expected output:
Holiday Friday
2 5
[2] - 2 days holiday from table1, [5] - 5 days friday
How to do this?
This counts the fridays between 2 dates:
declare #from datetime= '2012-01-26'
declare #to datetime = '2012-01-28'
select datediff(day, -3, #to)/7-datediff(day, -2, #from)/7
The holidays are easy to find, it seems like you have that part covered already.
I sort of answered this earlier. But didn't get any credit:
How to calculate the number of "Tuesdays" between two dates in TSQL?
That select help you:
DECLARE #FROMDATE DATE = '2009-01-07'
DECLARE #TODATE DATE = '2012-01-26'
SELECT COUNT(*) holidays,(select COUNT(*) from table1 where DATEPART(DW, hdate) = 5
AND DT BETWEEN #FROMDATE AND #TODATE ) fridays FROM table1
WHERE hdate BETWEEN #FROMDATE AND #TODATE
See:
Why should I consider using an auxiliary calendar table?
A calendar table can make it much
easier to develop solutions around any
business model which involves dates.
Last I checked, this encompasses
pretty much any business model you can
think of, to some degree. Constant
problems that end up requiring
verbose, complicated and inefficient
methods include the following
questions:
How many business days between x and y?
What are all of the dates between the second Tuesday of March and the first Friday in April? 
On what date should I expect this shipment to arrive? 
What were the dates of all the Fridays in this quarter? 
...
 
For the holiday, your SQL looks fine, you just seem to have trouble plugging the parameters into the SQL. If you specify which programming language you are using, we might be able to help here. If you use .NET, you should use Parameterized Queries instead of string substitution.
For the fridays, see this question:
Get number of weekdays (Sundays, Mondays, Tuesdays) between two dates SQL
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate ='16/12/2011'
SET #EndDate = '15/01/2012'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Friday' THEN 1 ELSE 0 END)
select count(Holiday) as holiday
from Table1
where date between start_date AND end_date
We solving the problem with an extra time table. This looks like this
ID | Date | Holiday | Year | CalendarWeek | DayName
1 | 17/12/2011 | 1 | 2011 | 50 | Monday
2 | 18/12/2011 | 0 | 2011 | 50 | Thursday
3 | 19/12/2011 | 0 | 2011 | 50 | Wendsday
With this table you could resolve your question like this
select
(select count(d.DayName) from date_table as d
where d.DayName = 'Friday' and date >= start_date and date <= end_date ),
(select sum(d.Holiday) from date_table as d
where date >= start_date and date <= end_date )
This should also be SQL Server 2000 compatible. And this for SQL Server 2005 and above:
with tmp(id) as
(
select id from from date_table where date >= start_date and date <= end_date
)
select
(select count(d.DayName) from date_table inner join tmp on tmp.id = id
where DayName = 'Friday' ),
(select sum(d.Holiday) from date_table inner join tmp on tmp.id = id )