SQL breakout date range to rows - sql

I am trying to take given date ranges found in a data set and divide them into unique rows for each day in the range (example below). Doing the opposite in SQL is pretty straight forward, but I am struggling to achieve the desired query output.
Beginning data:
ITEM START_DATE END_DATE
A 1/1/2015 1/5/2015
B 2/5/2015 2/7/2015
Desired query output:
ITEM DATE_COVERED
A 1/1/2015
A 1/2/2015
A 1/3/2015
A 1/4/2015
A 1/5/2015
B 2/5/2015
B 2/6/2015
B 2/7/2015

The fastest way will be some tally table:
DECLARE #t TABLE
(
ITEM CHAR(1) ,
START_DATE DATE ,
END_DATE DATE
)
INSERT INTO #t
VALUES ( 'A', '1/1/2015', '1/5/2015' ),
( 'B', '2/5/2015', '2/7/2015' )
;WITH cte AS(SELECT -1 + ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) d FROM
(VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t1(n) CROSS JOIN
(VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t2(n) CROSS JOIN
(VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t3(n) CROSS JOIN
(VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t4(n))
SELECT t.ITEM, ca.DATE_COVERED FROM #t t
CROSS APPLY(SELECT DATEADD(dd, d, t.START_DATE) AS DATE_COVERED
FROM cte
WHERE DATEADD(dd, d, t.START_DATE) BETWEEN t.START_DATE AND t.END_DATE) ca
ORDER BY t.ITEM, ca.DATE_COVERED

Query:
SQLFiddleExample
SELECT t.ITEM,
DATEADD(day,n.number, t.START_DATE) AS DATE_COVERED
FROM Table1 t,
(SELECT number
FROM master..spt_values
WHERE [type] = 'P') n
WHERE START_DATE <= DATEADD(day,n.number, t.START_DATE)
AND END_DATE >= DATEADD(day,n.number, t.START_DATE)
Result:
| ITEM | DATE_COVERED |
|------|--------------|
| A | 2015-01-01 |
| A | 2015-01-02 |
| A | 2015-01-03 |
| A | 2015-01-04 |
| A | 2015-01-05 |
| B | 2015-02-05 |
| B | 2015-02-06 |
| B | 2015-02-07 |

NOTE: this only works if the difference between your startdate and enddate is a maximum of 2047 days (master..spt_values only allows 0..2047 range of values)
select item, dateadd(d,v.number,d.start_date) adate
from begindata d
join master..spt_values v on v.type='P'
and v.number between 0 and datediff(d, start_date, end_date)
order by adate;
I'd like to say I did this myself but I got the code from this
Here is a fiddle with your expected result

TRY THIS...
CREATE TABLE Table1
([ITEM] varchar(1), [START_DATE] date, [END_DATE] date)
;
INSERT INTO Table1
([ITEM], [START_DATE], [END_DATE])
VALUES ('A', '2015-01-01', '2015-01-05'), ('B', '2015-02-05', 2015-02-07');
WITH Days
AS ( SELECT ITEM, START_DATE AS [Date], 1 AS [level] from Table1
UNION ALL
SELECT TABLE1.ITEM, DATEADD(DAY, 1, [Date]), [level] + 1
FROM Days,Table1
WHERE DAYS.ITEM=TABLE1.ITEM AND [Date] < END_DATE )
SELECT distinct [Date]
FROM Days
DEMO

Related

Split a record to multiple rows of record

I have table as below
Master Table
ID Name
1 Bubble
Child Table
ID MasterTableID StartDate EndDate Qty UnitMeasurement
1 1 1/2/2019 1/6/2019 1000 sqft
2 1 1/2/2019 1/4/2019 3000 sqft
I need to select the record above and show it in 5 rows since 1/2 - 1/6 were 5 months.
Date Qty
1/2/2019 200
1/3/2019 200
1/4/2019 200
1/5/2019 200
1/6/2019 200
Second row record to 3 rows record
Date Qty
1/2/2019 1000
1/3/2019 1000
1/4/2019 1000
I'm using SQL Server.
May I know it is possible to do so?
you can use Recursively + CTE and filter using inner join on id
CREATE TABLE T
([ID] int, [MasterTableID] int, [StartDate] datetime, [EndDate] datetime, [Qty] int, [UnitMeasurement] varchar(4))
;
INSERT INTO T
([ID], [MasterTableID], [StartDate], [EndDate], [Qty], [UnitMeasurement])
VALUES
(1, 1, '2019-01-02 00:00:00', '2019-01-06 00:00:00', 1000, 'sqft'),
(2, 1, '2019-01-02 00:00:00', '2019-01-04 00:00:00', 3000, 'sqft')
;
GO
2 rows affected
with cte as (
select [EndDate] as [Date],ID,datediff(day,[StartDate], [EndDate]) diff , [Qty] / (datediff(day,[StartDate], [EndDate]) + 1) as qty
from T
union all
select dateadd(day,-1,[Date]) [Date],T1.ID,T2.diff - 1 as diff,T2.qty
from T T1
inner join cte T2 on T1.ID = T2.ID
where diff >0
)
select ID,[Date],qty
from cte
order by ID,[Date]
GO
ID | Date | qty
-: | :------------------ | ---:
1 | 02/01/2019 00:00:00 | 200
1 | 03/01/2019 00:00:00 | 200
1 | 04/01/2019 00:00:00 | 200
1 | 05/01/2019 00:00:00 | 200
1 | 06/01/2019 00:00:00 | 200
2 | 02/01/2019 00:00:00 | 1000
2 | 03/01/2019 00:00:00 | 1000
2 | 04/01/2019 00:00:00 | 1000
db<>fiddle here
This is achievable using cte. since your dateformat is ddMMyyy, we need to convert this to MMddyyy so we can use dateadd(month...
CREATE TABLE #Temp
(id int, [StartDate] varchar(30), [EndDate] varchar(30), [Qty] int, [UnitMeasurement] varchar(4))
;
INSERT INTO #Temp
(id, [StartDate], [EndDate], [Qty], [UnitMeasurement])
VALUES
(1, '1/2/2019', '1/6/2019', 1000, 'sqft'),
(2, '1/2/2019', '1/4/2019', 3000, 'sqft')
;
GO
with cte as
(
Select id, cast(convert(varchar
, convert(datetime, [StartDate], 103), 101) as date) as startdate
, cast(convert(varchar
, convert(datetime, [EndDate], 103), 101) as date) as enddate
, [Qty]
, 1 as ctr from #Temp
union all
Select id, dateadd(month, 1, startdate), enddate, qty, ctr + 1
From cte
Where startdate < enddate
)
Select t1.id, qty/t2.ct, startdate from cte t1
cross apply (select count(1) ct, id from cte group by id) t2
where t2.id = t1.id
order by t1.id asc
Option (MaxRecursion 0)
drop table #Temp
output:
try like below for generating date
DECLARE #StartDate DATE = '1/2/2019'
, #EndDate DATE = '1/6/2019'
SELECT DATEADD(DAY, nbr - 1, #StartDate)
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.object_id ) AS Nbr
FROM sys.columns c
) nbrs
WHERE nbr - 1 <= DATEDIFF(DAY, #StartDate, #EndDate)
or you can use recursion
Declare #FromDate Date = '1/2/2019',
#ToDate Date = '1/6/2019'
;With DateCte (Date) As
(
Select #FromDate Union All
Select DateAdd(Day, 1, Date)
From DateCte
Where Date <= #ToDate
)
Select Date
From DateCte
Option (MaxRecursion 0)

Display data for all date ranges including missing dates

I'm having a issue with dates. I have a table with given from and to dates for an employee. For an evaluation, I'd like to display each date of the month with corresponding values from the second sql table.
SQL Table:
EmpNr | datefrom | dateto | hours
0815 | 01.01.2019 | 03.01.2019 | 15
0815 | 05.01.2019 | 15.01.2019 | 15
0815 | 20.01.2019 | 31.12.9999 | 40
The given employee (0815) worked during 01.01.-15.01. 15 hours, and during 20.01.-31.01. 40 hours
I'd like to have the following result:
0815 | 01.01.2019 | 15
0815 | 02.01.2019 | 15
0815 | 03.01.2019 | 15
0815 | 04.01.2019 | NULL
0815 | 05.01.2019 | 15
...
0815 | 15.01.2019 | 15
0815 | 16.01.2019 | NULL
0815 | 17.01.2019 | NULL
0815 | 18.01.2019 | NULL
0815 | 19.01.2019 | NULL
0815 | 20.01.2019 | 40
0815 | 21.01.2019 | 40
...
0815 | 31.01.2019 | 40
as for the dates, we have:
declare #year int = 2019, #month int = 1;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value + 1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year,#month,1)))
)
SELECT b.empnr, b.hours, datefromparts(#year,#month,numbers.value) Datum FROM numbers left outer join
emptbl b on b.empnr = '0815' and (datefromparts(#year,#month,numbers.value) >= b.datefrom and datefromparts(#year,#month,numbers.value) <= case b.dateto )
which is working quite well, yet I have the odd issue, that this code is only shoes the dates between 01.01.2019 and 03.01.2019
thank you very much in advance!
Did you check, if datefrom and dateto is in correct range?
Minimum value of DateTime field is 1753-01-01 and maximum value is 9999-12-31.
Look at your source table to check initial values.
The recursive CTE needs to begin with MIN(datefrom) and MAX(dateto):
DECLARE #t TABLE (empnr INT, datefrom DATE, dateto DATE, hours INT);
INSERT INTO #t VALUES
(815, '2019-01-01', '2019-01-03', 15),
(815, '2019-01-05', '2019-01-15', 15),
(815, '2019-01-20', '9999-01-01', 40),
-- another employee
(999, '2018-01-01', '2018-01-31', 15),
(999, '2018-03-01', '2018-03-31', 15),
(999, '2018-12-01', '9999-01-01', 40);
WITH rcte AS (
SELECT empnr
, MIN(datefrom) AS refdate
, ISNULL(NULLIF(MAX(dateto), '9999-01-01'), CURRENT_TIMESTAMP) AS maxdate -- clamp year 9999 to today
FROM #t
GROUP BY empnr
UNION ALL
SELECT empnr
, DATEADD(DAY, 1, refdate)
, maxdate
FROM rcte
WHERE refdate < maxdate
)
SELECT rcte.empnr
, rcte.refdate
, t.hours
FROM rcte
LEFT JOIN #t AS t ON rcte.empnr = t.empnr AND rcte.refdate BETWEEN t.datefrom AND t.dateto
ORDER BY rcte.empnr, rcte.refdate
OPTION (MAXRECURSION 1000) -- approx 3 years
Demo on db<>fiddle
It could be in your select, try:
SELECT b.empnr, b.hours, datefromparts(#year,#month,numbers.value) Datum
FROM numbers
LEFT OUTER JOIN emptbl b ON b.empnr = '0815' AND
datefromparts(#year,#month,numbers.value) BETWEEN b.datefrom AND b.dateto
Your CTE produces only 31 number and therefore it is showing only January dates.
declare #year int = 2019, #month int = 1;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value + 1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year,#month,1)))
)
SELECT *
FROM numbers
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=a24e58ef4ce522d3ec914f90907a0a9e
You can try below code,
with t0 (i) as (select 0 union all select 0 union all select 0),
t1 (i) as (select a.i from t0 a ,t0 b ),
t2 (i) as (select a.i from t1 a ,t1 b ),
t3 (srno) as (select row_number()over(order by a.i) from t2 a ,t2 b ),
tbldt(dt) as (select dateadd(day,t3.srno-1,'01/01/2019') from t3)
select tbldt.dt
from tbldt
where tbldt.dt <= b.dateto -- put your condition here
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=b16469908b323b8d1b98d77dd09bab3d

SQL Query Expanding Each Row Every Int. In Range Between COL_A and COL_B

My SQL Server has a TABLE. Both columns are integers, but represent dates in the form YYYYMM. I'd like to query this table and return a third column which, for every row, includes an integer in the form of YYYYMM for every year/month in the range of the two columns.
Here's the TABLE:
+------------+------------+
| beg_YYYYMM | end_YYYYMM |
+------------+------------+
| 201802 | 201805 |
| 201711 | 201801 |
+------------+------------+
Desired output:
+------------+------------+----------------+
| beg_YYYYMM | end_YYYYMM | month_in_range |
+------------+------------+----------------+
| 201802 | 201805 | 201802 |
| 201802 | 201805 | 201803 |
| 201802 | 201805 | 201804 |
| 201802 | 201805 | 201805 |
| 201711 | 201801 | 201711 |
| 201711 | 201801 | 201712 |
| 201711 | 201801 | 201801 |
+------------+------------+----------------+
Use a recursive CTE:
with cte as (
select beg_YYYYMM, end_YYYYMM,
convert(date, convert(varchar(255), beg_YYYYMM) + '01') as dte,
convert(date, convert(varchar(255), end_YYYYMM) + '01') as end_dte
from t
union all
select beg_YYYYMM, end_YYYYMM,
dateadd(month, 1, dte),
end_dte
from cte
where dte < end_dte
)
select beg_yyyymm, end_yyyymm,
year(dte) * 100 + month(dte) as yyyymm
from cte
order by dte;
Here is a db<>fiddle.
I really don't think I would actually deploy this but I wanted to know what it would look like. If you create a TVF that does the ugly work it isn't too bad. I used Aaron Bertrand's DateDim code with a quick modification to just get first of the month dates between the two dates that are passed in.
CREATE OR ALTER FUNCTION dbo.tvf_MonthRange (#beg_YYYYMM int, #end_YYYYMM int)
RETURNS #Results TABLE
(month_in_range int)
AS
BEGIN
--Have to convert ints to dates
DECLARE #BegDate DATE;
SET #BegDate = DATEFROMPARTS(CAST(SUBSTRING(CAST(#beg_YYYYMM AS varchar(6)),1,4) AS INT), CAST(SUBSTRING(CAST(#beg_YYYYMM AS varchar(6)),5,2) AS INT), 1);
--This needs to be the second day of the month for the code below to work.
DECLARE #EndDate DATE;
SET #EndDate = DATEFROMPARTS(CAST(SUBSTRING(CAST(#end_YYYYMM AS varchar(6)),1,4) AS INT), CAST(SUBSTRING(CAST(#end_YYYYMM AS varchar(6)),5,2) AS INT), 2);
INSERT INTO #Results
SELECT (DATEPART(YEAR, d) *100) + DATEPART(MONTH, d)
FROM
(
SELECT d = DATEADD(DAY, rn - 1, #BegDate)
FROM
(
SELECT TOP (DATEDIFF(DAY, #BegDate, #EndDate))
rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
-- on my system this would support > 5 million days
ORDER BY s1.[object_id]
) AS x
) AS y
WHERE DATEPART(DAY, d) = 1;
RETURN;
END
Then you can call it like this.
DECLARE #Months TABLE (beg_YYYYMM int, end_YYYYMM int)
INSERT INTO #MONTHS SELECT 201802, 201805
INSERT INTO #MONTHS SELECT 201711, 201801
SELECT *
FROM #Months m
CROSS APPLY dbo.tvf_MonthRange(m.beg_YYYYMM, m.end_YYYYMM) mr ;
It is bad idea Monday right?
A solution using a tally table which may be faster on larger data sets.
with dates as(
select
201501 as beg_YYYYMM
,201504 as end_YYYYMM
union all
select '201711', '201801'
union all
select '201807', '201812'
),
--Tally table
ctedaterange AS (
SELECT top 15
rn = Row_number() OVER(ORDER BY (SELECT NULL)) -1
FROM sys.objects a
)
SELECT
dates.*
,months_in_range = convert(varchar(6), Dateadd(mm, rn, cast(cast(dates.beg_YYYYMM as varchar(8)) +'01' as date)), 112)
FROM dates
cross join ctedaterange
WHERE
rn <= Datediff(mm, cast(cast(dates.beg_YYYYMM as varchar(8)) +'01' as date), cast(cast(dates.end_YYYYMM as varchar(8)) +'01' as date))
ORDER BY
beg_YYYYMM
,Dateadd(mm, rn, cast(cast(dates.beg_YYYYMM as varchar(8)) +'01' as date))
Here's the db<>fiddle

How to aggregate (counting distinct items) over a sliding window in SQL Server?

I am currently using this query (in SQL Server) to count the number of unique item each day:
SELECT Date, COUNT(DISTINCT item)
FROM myTable
GROUP BY Date
ORDER BY Date
How can I transform this to get for each date the number of unique item over the past 3 days (including the current day)?
The output should be a table with 2 columns:
one columns with all dates in the original table. On the second column, we have the number of unique item per date.
for instance if original table is:
Date Item
01/01/2018 A
01/01/2018 B
02/01/2018 C
03/01/2018 C
04/01/2018 C
With my query above I currently get the unique count for each day:
Date count
01/01/2018 2
02/01/2018 1
03/01/2018 1
04/01/2018 1
and I am looking to get as result the unique count over 3 days rolling window:
Date count
01/01/2018 2
02/01/2018 3 (because items ABC on 1st and 2nd Jan)
03/01/2018 3 (because items ABC on 1st,2nd,3rd Jan)
04/01/2018 1 (because only item C on 2nd,3rd,4th Jan)
Using an apply provides a convenient way to form sliding windows
CREATE TABLE myTable
([DateCol] datetime, [Item] varchar(1))
;
INSERT INTO myTable
([DateCol], [Item])
VALUES
('2018-01-01 00:00:00', 'A'),
('2018-01-01 00:00:00', 'B'),
('2018-01-02 00:00:00', 'C'),
('2018-01-03 00:00:00', 'C'),
('2018-01-04 00:00:00', 'C')
;
CREATE NONCLUSTERED INDEX IX_DateCol
ON MyTable([Date])
;
Query:
select distinct
t1.dateCol
, oa.ItemCount
from myTable t1
outer apply (
select count(distinct t2.item) as ItemCount
from myTable t2
where t2.DateCol between dateadd(day,-2,t1.DateCol) and t1.DateCol
) oa
order by t1.dateCol ASC
Results:
| dateCol | ItemCount |
|----------------------|-----------|
| 2018-01-01T00:00:00Z | 2 |
| 2018-01-02T00:00:00Z | 3 |
| 2018-01-03T00:00:00Z | 3 |
| 2018-01-04T00:00:00Z | 1 |
There may be some performance gains by reducing the date column prior to using the apply, like so:
select
d.date
, oa.ItemCount
from (
select distinct t1.date
from myTable t1
) d
outer apply (
select count(distinct t2.item) as ItemCount
from myTable t2
where t2.Date between dateadd(day,-2,d.Date) and d.Date
) oa
order by d.date ASC
;
Instead of using select distinct in that subquery you could use group by instead but the execution plan will remain the same.
Demo at SQL Fiddle
The most straight forward solution is to join the table with itself based on dates:
SELECT t1.DateCol, COUNT(DISTINCT t2.Item) AS C
FROM testdata AS t1
LEFT JOIN testdata AS t2 ON t2.DateCol BETWEEN DATEADD(dd, -2, t1.DateCol) AND t1.DateCol
GROUP BY t1.DateCol
ORDER BY t1.DateCol
Output:
| DateCol | C |
|-------------------------|---|
| 2018-01-01 00:00:00.000 | 2 |
| 2018-01-02 00:00:00.000 | 3 |
| 2018-01-03 00:00:00.000 | 3 |
| 2018-01-04 00:00:00.000 | 1 |
GROUP BY should be faster then DISTINCT (make sure to have an index on your Date column)
DECLARE #tbl TABLE([Date] DATE, [Item] VARCHAR(100))
;
INSERT INTO #tbl VALUES
('2018-01-01 00:00:00', 'A'),
('2018-01-01 00:00:00', 'B'),
('2018-01-02 00:00:00', 'C'),
('2018-01-03 00:00:00', 'C'),
('2018-01-04 00:00:00', 'C');
SELECT t.[Date]
--Just for control. You can take this part away
,(SELECT DISTINCT t2.[Item] AS [*]
FROM #tbl AS t2
WHERE t2.[Date]<=t.[Date]
AND t2.[Date]>=DATEADD(DAY,-2,t.[Date]) FOR XML PATH('')) AS CountedItems
--This sub-select comes back with your counts
,(SELECT COUNT(DISTINCT t2.[Item])
FROM #tbl AS t2
WHERE t2.[Date]<=t.[Date]
AND t2.[Date]>=DATEADD(DAY,-2,t.[Date])) AS ItemCount
FROM #tbl AS t
GROUP BY t.[Date];
The result
Date CountedItems ItemCount
2018-01-01 AB 2
2018-01-02 ABC 3
2018-01-03 ABC 3
2018-01-04 C 1
This solution is different from other solutions. Can you check performance of this query on real data with comparison to other answers?
The basic idea is that each row can participate in the window for its own date, the day after, or the day after that. So this first expands the row out into three rows with those different dates attached and then it can just use a regular COUNT(DISTINCT) aggregating on the computed date. The HAVING clause is just to avoid returning results for dates that were solely computed and not present in the base data.
with cte(Date, Item) as (
select cast(a as datetime), b
from (values
('01/01/2018','A')
,('01/01/2018','B')
,('02/01/2018','C')
,('03/01/2018','C')
,('04/01/2018','C')) t(a,b)
)
select
[Date] = dateadd(dd, n, Date), [Count] = count(distinct Item)
from
cte
cross join (values (0),(1),(2)) t(n)
group by dateadd(dd, n, Date)
having max(iif(n = 0, 1, 0)) = 1
option (force order)
Output:
| Date | Count |
|-------------------------|-------|
| 2018-01-01 00:00:00.000 | 2 |
| 2018-01-02 00:00:00.000 | 3 |
| 2018-01-03 00:00:00.000 | 3 |
| 2018-01-04 00:00:00.000 | 1 |
It might be faster if you have many duplicate rows:
select
[Date] = dateadd(dd, n, Date), [Count] = count(distinct Item)
from
(select distinct Date, Item from cte) c
cross join (values (0),(1),(2)) t(n)
group by dateadd(dd, n, Date)
having max(iif(n = 0, 1, 0)) = 1
option (force order)
Use GETDATE() function to get current date, and DATEADD() to get the last 3 days
SELECT Date, count(DISTINCT item)
FROM myTable
WHERE [Date] >= DATEADD(day,-3, GETDATE())
GROUP BY Date
ORDER BY Date
SQL
SELECT DISTINCT Date,
(SELECT COUNT(DISTINCT item)
FROM myTable t2
WHERE t2.Date BETWEEN DATEADD(day, -2, t1.Date) AND t1.Date) AS count
FROM myTable t1
ORDER BY Date;
Demo
Rextester demo: http://rextester.com/ZRDQ22190
Since COUNT(DISTINCT item) OVER (PARTITION BY [Date]) is not supported you can use dense_rank to emulate that:
SELECT Date, dense_rank() over (partition by [Date] order by [item])
+ dense_rank() over (partition by [Date] order by [item] desc)
- 1 as count_distinct_item
FROM myTable
One thing to note is that dense_rank will count null as whereas COUNT will not.
Refer this post for more details.
Here is a simple solution that uses myTable itself as the source of grouping dates (edited for SQLServer dateadd). Note that this query assumes there will be at least one record in myTable for every date; if any date is absent, it will not appear in the query results, even if there are records for the 2 days prior:
select
date,
(select
count(distinct item)
from (select distinct date, item from myTable) as d2
where
d2.date between dateadd(day,-2,d.date) and d.date
) as count
from (select distinct date from myTable) as d
I solve this question with Math.
z (any day) = 3x + y (y is mode 3 value)
I need from 3 * (x - 1) + y + 1 to 3 * (x - 1) + y + 3
3 * (x- 1) + y + 1 = 3* (z / 3 - 1) + z % 3 + 1
In that case; I can use group by (between 3* (z / 3 - 1) + z % 3 + 1 and z)
SELECT iif(OrderDate between 3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1
and orderdate, Orderdate, 0)
, count(sh.SalesOrderID) FROM Sales.SalesOrderDetail shd
JOIN Sales.SalesOrderHeader sh on sh.SalesOrderID = shd.SalesOrderID
group by iif(OrderDate between 3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1
and orderdate, Orderdate, 0)
order by iif(OrderDate between 3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1
and orderdate, Orderdate, 0)
If you need else day group, you can use;
declare #n int = 4 (another day count)
SELECT iif(OrderDate between #n * (cast(OrderDate as int) / #n - 1) + (cast(OrderDate as int) % #n) + 1
and orderdate, Orderdate, 0)
, count(sh.SalesOrderID) FROM Sales.SalesOrderDetail shd
JOIN Sales.SalesOrderHeader sh on sh.SalesOrderID = shd.SalesOrderID
group by iif(OrderDate between #n * (cast(OrderDate as int) / #n - 1) + (cast(OrderDate as int) % #n) + 1
and orderdate, Orderdate, 0)
order by iif(OrderDate between #n * (cast(OrderDate as int) / #n - 1) + (cast(OrderDate as int) % #n) + 1
and orderdate, Orderdate, 0)

Split date range to year-month rows on SQL Server 2005

I need to create an output where I got one row per year-month.
Assume the dataset is:
id | dateStart | dateEnd
1 | 2015-01-01 00:00:00.000 | 2015-03-31 00:00:00.000
2 | 2014-07-01 00:00:00.000 | 2014-08-31 00:00:00.000
...
I need the following output:
id | year-month
1 | 2015-01
1 | 2015-02
1 | 2015-03
2 | 2014-07
2 | 2014-08
The output can be any datatype since I can just change that later.
That is for 2015-01 the following is ok, "2015-01-01 00:00:00.000", "2015-01-01", "201501", "2015 | jan" ect.
Note I'm using SQL Server 2005.
Here is a method that uses recursive CTEs:
with CTE as (
select id, dateStart as dte, dateEnd
from t
union all
select id, dateadd(month, 1, dte), dateEnd
from CTE
where dateadd(month, 1, dte) < dateEnd
)
select id, dte
from CTE;
You can convert the final result into any format you like. For instance:
select id, year(dte) * 10000 + month(dte) as yyyymm_int
or
select id, cast(year(dte) * 10000 + month(dte) as varchar(255)) as yyyymm
Generate tally table(just make sure you get enough rows there). tally will contain values 0,1,2,.....n. Then you do a join with condition adding thise values as months to startDate until it is greater then endDate:
DECLARE #t TABLE
(
id INT ,
dateStart DATETIME ,
dateEnd DATETIME
)
INSERT INTO #t
VALUES ( 1, '2015-01-01 00:00:00.000', '2015-03-31 00:00:00.000' ),
( 2, '2014-07-01 00:00:00.000', '2014-08-31 00:00:00.000' )
;WITH cte AS(SELECT -1 + ROW_NUMBER() OVER(ORDER BY t1.m) m
FROM(VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))t1(m) CROSS JOIN
(VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))t2(m))
SELECT t.id,
DATEADD(mm, c.m, t.dateStart) AS year_month
FROM cte c
JOIN #t t ON DATEADD(mm, c.m, t.dateStart) <= t.dateEnd
ORDER BY t.id, year_month
Output:
id year_month
1 2015-01-01 00:00:00.000
1 2015-02-01 00:00:00.000
1 2015-03-01 00:00:00.000
2 2014-07-01 00:00:00.000
2 2014-08-01 00:00:00.000
In an ideal world you would have a calendar table, then your query would simply be:
SELECT t.id,
c.FirstDayOfMonth
FROM YourTable AS t
INNER JOIN dbo.Calendar c
ON c.FirstDayOfMonth >= t.DateStart
AND c.FirstDayOfMonth <= t.DateEnd
AND c.DayOfMonth = 1;
Assuming that you don't have a calendar table then you can do it with a list of numbers generated on the fly (Read this article for more on this). The following will generate a list from 1-10,000:
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N3.N) FROM N3)
SELECT * FROM Numbers;
Then you can join this to your original table:
DECLARE #T TABLE (id INT, DateStart DATE, DateEnd DATE);
INSERT #T (ID, DateStart, DateEnd)
VALUES (1, '20150101', '20150331'), (2, '20140701', '20140831');
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N3.N) FROM N3)
SELECT t.ID,
[year-month] = DATEADD(MONTH, n.Number + DATEDIFF(MONTH, 0, t.DateStart), 0)
FROM #T AS t
INNER JOIN Numbers AS N
ON N.Number - 1 <= DATEDIFF(MONTH, t.DateStart, t.DateEnd);