sql query group by date range - sql

I have table with below data -
StartDate EndDate Amount
2/1/2016 4/30/2016 2.265
2/1/2016 12/31/2099 16.195
5/1/2016 12/31/2099 37.75
I am trying to write a query to sum the amount on date range and give me below result
StartDate EndDate Amount
2/1/2016 4/30/2016 18.46
5/1/2016 12/31/2099 53.945
The result needs to be distinct date range with amount summed for that date range. As in above example, row 2 has dates that overlap row 1 and 3. So the row 2 amount needs to be added in row 1 and row 3.
I am writing this query on sql server 2012, Please advise on what approach I should take.
Below is query to generate sample data
SELECT * INTO #tmp_GridResults_1
FROM (
SELECT N'2016-02-01 00:00:00.000' AS [StartDate], N'2016-04-30 00:00:00.000' AS [EndDate], N'2.265' AS [Amount] UNION ALL
SELECT N'2016-02-01 00:00:00.000' AS [StartDate], N'2099-12-31 00:00:00.000' AS [EndDate], N'16.195' AS [Amount] UNION ALL
SELECT N'2016-05-01 00:00:00.000' AS [StartDate], N'2099-12-31 00:00:00.000' AS [EndDate], N'37.75' AS [Amount] ) t;
SELECT [StartDate], [EndDate], [Amount]
FROM #tmp_GridResults_1

I don't think you'll be able to group with both StartDate and EndDate. However, if you use just StartDate and Amount you can try this:
select StartDate,sum(Amount) as Amount from #tmp_GridResults_1 group by StartDate
This will give you the grouping of Amount by StartDate.

From my previews comment i think you want to group by StartDate, in that case you can use:
SELECT
mt.StartDate,
SUM(Amount) AS 'Amount'
FROM MyTable mt
WHERE mt.StartDate BETWEEN
'2016-05-18 00:00:00.000' AND '2016-06-20 00:00:00.000' --your date range
GROUP BY StartDate
However you need to specify in your question how you want to group them, because with 2 date fields you can group the following ways:
GROUP BY StartDate --groups records that have the same StartDate
GROUP BY EndDate --groups records that have the same EndDate
GROUP BY StartDate, EndDate --groups records that have same StartDate and EndDate
Also with 2 date fields your date range can vary a lot.
WHERE StartDate BETWEEN #sd AND #ed --will get records whose start date is inside the provided range
WHERE EndDate BETWEEN #sd AND #ed --will get records whose end date is inside the provided range
WHERE StartDate >= #sd AND EndDate <= #ed -- will get records that started and ended inside the provided date range
WHERE StartDate BETWEEN #sd AND #ed OR EndDate BETWEEN #sd AND #ed
--this last one will get records whose StartDate or EndDate are inside the provided date range
based on this you can build the query that you need, but with the provided data in your question its still ambiguous what you want to achieve, you need to be more specific and provide more data in order to produce an exact answer to your question.
One more thing to add is that if you take into account time it will not group them if the date is the same but time is different i.e.
'2016-05-18 00:00:00.000'
'2016-05-18 01:01:01.001'
--these dates will not be grouped

Below query will give the required result :-
select StartDate,EndDate,SUM(Amount) over (partition by StartDate) AS Amount into #t1
from table_name
select StartDate,EndDate,SUM(Amount) over (partition by EndDate) AS Amount into #t2
from table_name
select * from (
select distinct P.StartDate,P.EndDate,P.Amount from (
select StartDate,MIN(EndDate) EndDate,Amount from #t1
group by StartDate,Amount) P
JOIN (
select MIN(StartDate) StartDate,EndDate,Amount from #t2
group by EndDate,Amount) Q on P.StartDate=Q.StartDate OR P.EndDate=Q.EndDate
where P.Amount>=Q.Amount
UNION
select distinct P.StartDate,P.EndDate,P.Amount from (
select MIN(StartDate) StartDate,EndDate,Amount from #t2
group by EndDate,Amount) P
JOIN (
select StartDate,MIN(EndDate) EndDate,Amount from #t1
group by StartDate,Amount) Q on P.StartDate=Q.StartDate OR P.EndDate=Q.EndDate
where P.Amount>=Q.Amount
) A
Just Replace table_name by the table for which you want result, Let me know in case of any confusion

Related

Insert dates between startdate and enddate in SQL (ssms)

Hi I need to insert all dates between startdate and enddate of a casenumber is SQL (ssms). So instead of having one row per casenumber the casenumber will have a row for each day. So if there is 10 days between the case started and ended it will have 10 rows. If the case has not been ended it will have a NULL in the original table but should be replaced with getdate. See attach image.
image of tables
This achieves your picture:
Test Data Setup
create table #test (CaseNumber int, StartDate date, Enddate date)
insert into #test (CaseNumber, StartDate, Enddate)
values(1,'2019-09-18','2019-09-24'),(2,'2019-09-23',NULL)
The Code
;with dt_seq as (
select t.CaseNumber, t.StartDate, isnull(t.Enddate,cast (getdate() as date)) EndDate, t.StartDate [Date]
from #test t
union all
select z.CaseNumber, z.StartDate, z.EndDate, case when z.[Date] < z.EndDate then dateadd(day,1,z.[Date]) else z.EndDate end [Date]
from dt_seq z
where z.[Date] <= dateadd(day,-1,z.EndDate)
)
select c.*, ROW_NUMBER() over (partition by c.CaseNumber order by c.[Date]) [Duration]
from dt_seq c
order by c.CaseNumber, c.[Date]
option ( MaxRecursion 0 )
Reuslts

How to count number of work days and hours extracting public holidays between two dates in SQL

I am new to SQL and stuck in some complex query.
What am I trying to achieve?
I want to calculate following two types of total days between two timestamp fields.
Number of Working Days (Excluding Weekend & Public Holidays)
Number of Total Days (Including Weekend & Public Holidays)
Calculation Condition
If OrderDate time is <= 12:00 PM then start count from 0
If OrderDate Time is > 12:00 PM then start count from -1
If Delivery Date is NULL then count different till Today's Date
Data Model
OrderDate & DeliveryDate resides in 'OrderTable'
PublicHolidayDate resides 'PublicHolidaysTable'
As with many tasks in SQL, this could be solved in multiple ways.
You can use COUNT aggregate operations on the date range with the BETWEEN operator to give you aggregate totals of the weekend days and holidays from a start date (OrderDate) to an end date (DeliveryDate).
This functionality can be combined with CTEs (Common Table Expressions) to give you the end result set you are looking for.
I've put together a query that illustrates one way you could go about doing it. I've also put together some test data and results to illustrate how the query works.
DECLARE #DateRangeBegin DATETIME = '2016-01-01'
, #DateRangeEnd DATETIME = '2016-07-01'
DECLARE #OrderTable TABLE
(OrderNum INT, OrderDate DATETIME, DeliveryDate DATETIME)
INSERT INTO #OrderTable VALUES
(1, '2016-02-12 09:30', '2016-03-01 13:00')
, (2, '2016-03-15 13:00', '2016-03-30 13:00')
, (3, '2016-03-22 14:00', NULL)
, (4, '2016-05-06 10:00', '2016-05-19 13:00')
DECLARE #PublicHolidaysTable TABLE
(PublicHolidayDate DATETIME, Description NVARCHAR(255))
INSERT INTO #PublicHolidaysTable VALUES
('2016-02-15', 'President''s Day')
, ('2016-03-17', 'St. Patrick''s Day')
, ('2016-03-25', 'Good Friday')
, ('2016-03-27', 'Easter Sunday')
, ('2016-05-05', 'Cinco de Mayo')
Some considerations you may of already thought of are that you don't want to count both weekend days and holidays that occur on a weekend, unless your company observes the holiday on the next Monday. For simplicity, I've excluded any holiday that occurs on a weekend day in the query.
You'll also want to limit this type of query to a specific date range.
The first CTE (cteAllDates) gets all dates between the start and end date range.
The second CTE (cteWeekendDates) gets all weekend dates from the first CTE (cteAllDates).
The third CTE (ctePublicHolidays) gets all holidays that occur on weekdays from your PublicHolidaysTable.
The last CTE (cteOrders) fulfills the requirement that the count of total days and working days must begin from the next day if the OrderDate is after 12:00PM and the requirement that the DeliveryDate should use today's date if it is null.
The select statement at the end of the CTE statements gets your total day count, weekend count, holiday count, and working days for each order.
;WITH cteAllDates AS (
SELECT 1 [DayID]
, #DateRangeBegin [CalendarDate]
, DATENAME(dw, #DateRangeBegin) [NameOfDay]
UNION ALL
SELECT cteAllDates.DayID + 1 [DayID]
, DATEADD(dd, 1 ,cteAllDates.CalendarDate) [CalenderDate]
, DATENAME(dw, DATEADD(d, 1 ,cteAllDates.CalendarDate)) [NameOfDay]
FROM cteAllDates
WHERE DATEADD(d,1,cteAllDates.CalendarDate) < #DateRangeEnd
)
, cteWeekendDates AS (
SELECT CalendarDate
FROM cteAllDates
WHERE NameOfDay IN ('Saturday','Sunday')
)
, ctePublicHolidays AS (
SELECT PublicHolidayDate
FROM #PublicHolidaysTable
WHERE DATENAME(dw, PublicHolidayDate) NOT IN ('Saturday', 'Sunday')
)
, cteOrders AS (
SELECT OrderNum
, OrderDate
, CASE WHEN DATEPART(hh, OrderDate) >= 12 THEN DATEADD(dd, 1, OrderDate)
ELSE OrderDate
END [AdjustedOrderDate]
, CASE WHEN DeliveryDate IS NOT NULL THEN DeliveryDate
ELSE GETDATE()
END [DeliveryDate]
FROM #OrderTable
)
SELECT o.OrderNum
, o.OrderDate
, o.DeliveryDate
, DATEDIFF(DAY, o.AdjustedOrderDate, o.DeliveryDate) [TotalDayCount]
, (SELECT COUNT(*) FROM cteWeekendDates w
WHERE w.CalendarDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate) [WeekendDayCount]
, (SELECT COUNT(*) FROM ctePublicHolidays h
WHERE h.PublicHolidayDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate) [HolidayCount]
, DATEDIFF(DAY, o.AdjustedOrderDate, o.DeliveryDate)
- (SELECT COUNT(*) FROM cteWeekendDays w
WHERE w.CalendarDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate)
- (SELECT COUNT(*) FROM ctePublicHolidays h
WHERE h.PublicHolidayDate BETWEEN o.AdjustedOrderDate AND o.DeliveryDate) [WorkingDays]
FROM cteOrders o
WHERE o.OrderDate BETWEEN #DateRangeBegin AND #DateRangeEnd
OPTION (MaxRecursion 500)
Results from the above query using the test data...
What I'd probably do is simplify the above by adding a Calendar table populated with sufficiently wide date ranges. Then I'd take some of the CTE statements and turn them into views.
I think specifically valuable to you would be a view that gets you the work days without weekends or holidays. Then you could just simply get the date difference between the two dates and count the work days in the same range.

To club the rows for week days

I have data like below:
StartDate EndDate Duration
----------
41890 41892 3
41898 41900 3
41906 41907 2
41910 41910 1
StartDate and EndDate are respective ID values for any dates from calendar. I want to calculate the sum of duration for consecutive days. Here I want to include the days which are weekends. E.g. in the above data, let's say 41908 and 41909 are weekends, then my required result set should look like below.
I already have another proc that can return me the next working day, i.e. if I pass 41907 or 41908 or 41909 as DateID in that proc, it will return 41910 as the next working day. Basically I want to check if the DateID returned by my proc when I pass the above EndDateID is same as the next StartDateID from above data, then both the rows should be clubbed. Below is the data I want to get.
ID StartDate EndDate Duration
----------
278457 41890 41892 3
278457 41898 41900 3
278457 41906 41910 3
Please let me know in case the requirement is not clear, I can explain further.
My Date Table is like below:
DateId Date Day
----------
41906 09-04-2014 Thursday
41907 09-05-2014 Friday
41908 09-06-2014 Saturdat
41909 09-07-2014 Sunday
41910 09-08-2014 Monday
Here is the SQL Code for setup:
CREATE TABLE Table1
(
StartDate INT,
EndDate INT,
LeaveDuration INT
)
INSERT INTO Table1
VALUES(41890, 41892, 3),
(41898, 41900, 3),
(41906, 41907, 3),
(41910, 41910, 1)
CREATE TABLE DateTable
(
DateID INT,
Date DATETIME,
Day VARCHAR(20)
)
INSERT INTO DateTable
VALUES(41907, '09-05-2014', 'Friday'),
(41908, '09-06-2014', 'Saturday'),
(41909, '09-07-2014', 'Sunday'),
(41910, '09-08-2014', 'Monday'),
(41911, '09-09-2014', 'Tuesday')
This is rather complicated. Here is an approach using window functions.
First, use the date table to enumerate the dates without weekends (you can also take out holidays if you want). Then, expand the periods into one day per row, by using a non-equijoin.
You can then use a trick to identify sequential days. This trick is to generate a sequential number for each id and subtract it from the sequential number for the dates. This is a constant for sequential days. The final step is simply an aggregation.
The resulting query is something like this:
with d as (
select d.*, row_number() over (order by date) as seqnum
from dates d
where day not in ('Saturday', 'Sunday')
)
select t.id, min(t.date) as startdate, max(t.date) as enddate, sum(duration)
from (select t.*, ds.seqnum, ds.date,
(d.seqnum - row_number() over (partition by id order by ds.date) ) as grp
from table t join
d ds
on ds.date between t.startdate and t.enddate
) t
group by t.id, grp;
EDIT:
The following is the version on this SQL Fiddle:
with d as (
select d.*, row_number() over (order by date) as seqnum
from datetable d
where day not in ('Saturday', 'Sunday')
)
select t.id, min(t.date) as startdate, max(t.date) as enddate, sum(duration)
from (select t.*, ds.seqnum, ds.date,
(ds.seqnum - row_number() over (partition by id order by ds.date) ) as grp
from (select t.*, 'abc' as id from table1 t) t join
d ds
on ds.dateid between t.startdate and t.enddate
) t
group by grp;
I believe this is working, but the date table doesn't have all the dates in it.

SSRS Report per month with 3 Columns count

I am working on SSRS report where I have 3 dates to deal with and get the count of each column per month starting September. Here is an image of what I am trying to achieve and I am not sure of what exactly I am missing in the Groupings. Any help would be really appreciated.
Query -
SELECT CONVERT(NVARCHAR(10), TableA.DueDate, 101) AS DueDate ,
CONVERT(NVARCHAR(10), TableB.DateFrom, 101) AS DateFrom ,
CONVERT(NVARCHAR(10), TableB.DateTo, 101) AS DateTo
FROM dbo.TableA
INNER JOIN dbo.TableB ON dbo.TableA.Id = dbo.TableB.TableAid
WHERE ( TableA.DueDate BETWEEN '2015-08-01'
AND '2016-07-30' )
AND ( TableB >= '08/01/2013' )
AND ( TableB <= '07/30/2014' )
Its a little ugly but will get you the intended results - Just paste your query inside the definition of the CTE -
WITH DateCTE AS (SELECT
DueDate, DateFrom, DateTo FROM DateTable)
SELECT MONTH, SUM(DueDate), SUM(DateFROM), SUM(DateTo)
FROM (
SELECT DateName(month,DueDate) MONTH, Count(*) AS DueDate, 0 DateFROM , 0 DateTo
FROM DateCTE
GROUP BY DateName(month,DueDate)
UNION
SELECT DateName(month,DateFrom) MOntH, 0 AS DueDate, COUNT(*) DateFROM , 0 DateTo
FROM DateCTE
GROUP BY DateName(month,DateFrom)
UNION
SELECT DateName(month,DateTo) Month, 0 AS DueDate, 0 DateFROM , COUNT(*) Dateto
FROM DateCTE
GROUP BY DateName(month,DateTo)) UnionTable
GROUP BY MONTH
Heres the SQL fiddle - http://sqlfiddle.com/#!6/0a639/10
Ankit Khetan,
What you're displaying in your report layout is an SSRS MATRIX format (like excel). See if this works you instead of using a another query .... In your Select Statements extract the month number for each date variable. Then try to use these numbers as your columns. and use your Date variables as your rows in the matrix layout.

SQL Server 2008 - Enumerate multiple date ranges

How can I enumerate multiple date ranges in SQL Server 2008? I know how to do this if my table contains a single record
StartDate EndDate
2014-01-01 2014-01-03
;WITH DateRange
AS (
SELECT #StartDate AS [Date]
UNION ALL
SELECT DATEADD(d, 1, [Date])
FROM DateRange
WHERE [Date] < #EndDate
)
SELECT * FROM DateRange
OUTPUT
2014-01-01, 2014-01-02, 2014-01-03
I am however lost as how to do it if my table contains multiple records. I could possibly use the above logic in a cursor but want to know if there is a set based solution instead.
StartDate EndDate
2014-01-01 2014-01-03
2014-01-05 2014-01-06
DESIRED OUTPUT:
2014-01-01, 2014-01-02, 2014-01-03, 2014-01-05, 2014-01-06
Well, let's see. Define the ranges as a table. Then generate the full range of dates from the first to the last date. Finally, select the dates that are in the range:
with dateranges as (
select cast('2014-01-01' as date) as StartDate, cast('2014-01-03' as date) as EndDate union all
select '2014-01-05', '2014-01-06'
),
_dates as (
SELECT min(StartDate) AS [Date], max(EndDate) as enddate
FROM dateranges
UNION ALL
SELECT DATEADD(d, 1, [Date]), enddate
FROM _dates
WHERE [Date] < enddate
),
dates as (
select [date]
from _dates d
where exists (select 1 from dateranges dr where d.[date] >= dr.startdate and d.[date] <= dr.enddate)
)
select *
from dates
. . .
You can see this work here.
You could grab the min and max dates first, like so:
SELECT #startDate = MIN(StartDate), #endDate = MAX(EndDate)
FROM YourTable
WHERE ...
And then pass those variables into your date range enumerator.
Edit... Whoops, I missed an important requirement. See the accepted answer.
As GordonLinoff mentioned, you should:
Store your ranges in a table
Generate a range of dates that encompasses your ranges
Filter down to only those dates that fall within the range
The following query builds up a collection of numbers, and then uses that to quickly generate all of the dates that fall within each range.
-- Create a table of digits (0-9)
DECLARE #Digits TABLE (digit INT NOT NULL PRIMARY KEY);
INSERT INTO #Digits(digit)
VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
WITH
-- Store our ranges in a common table expression
CTE_DateRanges(StartDate, EndDate) AS (
SELECT '2014-01-01', '2014-01-03'
UNION ALL
SELECT '2014-01-05', '2014-01-06'
)
SELECT DATEADD(DAY, NUMBERS.num, RANGES.StartDate) AS Date
FROM
(
-- Create the list of all 3-digit numbers (0-999)
SELECT D3.digit * 100 + D2.digit * 10 + D1.digit AS num
FROM #Digits AS D1
CROSS JOIN #Digits AS D2
CROSS JOIN #Digits AS D3
-- Add more CROSS JOINs to #Digits if your ranges span more than 999 days
) NUMBERS
-- Join to our ranges table to generate the dates and filter them
-- down to those that fall within a range
INNER JOIN CTE_DateRanges RANGES
ON DATEADD(DAY, NUMBERS.num, RANGES.StartDate) <= RANGES.EndDate
ORDER BY
Date
The date creation is done by joining our number list with our date ranges, using the number as a number of days to add to the StartDate of the range. We then filter out any results where the generated date for a given range falls beyond that range's EndDate. Since we're adding a non-negative number of days to the StartDate to generate the date, we know that our date will always be greater-than-or-equal-to the StartDate of the range, so we don't need to include StartDate in the WHERE clause.
This query will return DATETIME values. If you need a DATE value, rather than a DATETIME value, you can simply cast the value in the SELECT clause.
Credit goes to Itzik Ben-Gan for the digits table.