Create data ranges based on list of dates - sql-server

I have a list of dates like this (no gaps, each calendar date):
DateKey
Valid
2021-01-01
1
2021-01-02
1
2021-01-03
1
2021-01-04
0
2021-01-05
0
2021-01-06
1
2021-01-07
1
I would like to convert them using T-SQL to date ranges considering valid dates only.
So the results would be:
ValidFrom
ValidTo
2021-01-01
2021-01-03
2021-01-06
2021-01-07
Grouping simply by Valid flag rtutns wrong results:
select min(dateKey),max(dateKey)
from #t
group by Valid
If I knew how to assign a unique value for each continuous segment of valid dates, that would solve my problem. Is there anyone that can help me with this?

Just another option using the window function sum() over()
Select ValidFrom = min(DateKey)
,ValidTo = max(DateKey)
From (
Select *
,Grp = sum(case when Valid=0 then 1 else 0 end) over (order by DateKey)
from YourTable
) A
Where Valid=1
Group By Grp
Returns
ValidFrom ValidTo
2021-01-01 2021-01-03
2021-01-06 2021-01-07

Something like the following may work for you:
DECLARE #Dates TABLE (Dt DATE, Valid BIT)
INSERT #Dates
VALUES('2021-01-01', 1),
('2021-01-02', 1),
('2021-01-03', 1),
('2021-01-04', 0),
('2021-01-05', 0),
('2021-01-06', 1),
('2021-01-07', 1)
SELECT MIN(dt.Dt) AS BeginRange,
MAX(dt.Dt) AS EndRange
FROM (
SELECT d.Dt,
DATEDIFF(D, ROW_NUMBER() OVER(ORDER BY d.Dt), d.Dt) AS DtRange
FROM #Dates d
WHERE Valid = 1
) AS dt
GROUP BY dt.DtRange;

I think I've just found the solution of my problem:
https://dba.stackexchange.com/questions/197972/convert-list-of-dates-in-a-date-range-in-sql-server
DECLARE #t TABLE (dt DATE);
INSERT INTO #t (dt)
VALUES ('20180202')
,('20180203')
,('20180204')
,('20180205')
,('20180209')
,('20180212')
,('20180213');
WITH c
AS (
SELECT dt
,dateadd(day, - 1 * dense_rank() OVER (orderby dt), dt) AS grp
FROM #t
)
SELECT min(dt) AS start_range
,max(dt) AS end_range
FROM c
GROUP BY grp;

Related

How to display tabular record of dates from date range

I have Request table with 3 records having structure: Id, DateFrom, DateTo
Id DateFrom DateTo
1 15/01/2019 15/01/2019
2 21/01/2019 28/01/2019
3 04/02/2019 09/02/2019
And I want an output like this:
Id Date
1 15/01/2019
2 21/01/2019
2 22/01/2019
2 23/01/2019
2 24/01/2019
2 25/01/2019
2 26/01/2019
2 27/01/2019
2 28/01/2019
3 04/02/2019
3 05/02/2019
3 06/02/2019
3 07/02/2019
3 08/02/2019
3 09/02/2019
I have created a table valued function to display the series of date based DateFrom and DateTo.
CREATE FUNCTION [dbo].[tvfhrms_Calendar_DateRange](#DateFrom date, #DateTo date)
RETURNS #DateOfTheYear Table(Level int,SysDate date)
AS
BEGIN
WITH AllDays
AS (
SELECT [Level] = 1
,[Date] = #DateFrom
UNION ALL
SELECT [Level] = [Level] + 1
,[Date] = DATEADD(DAY, 1, [Date])
FROM AllDays
WHERE [Date] < #DateTo
)
INSERT #DateOfTheYear
SELECT [Level]
,[SysDate]=[Date]
FROM AllDays OPTION (MAXRECURSION 0)
RETURN
END
Then when used in select query,
SELECT sysdate from [dbo].[tvfhrms_Calendar_DateRange]('2019-01-10', '2019-02-09')
This will give the results of the sequence of Datefrom to DateTo.
How can I integrate this to my table so that I can have the output as my expectations?
You can use APPLY :
SELECT tt.*
FROM table t CROSS APPLY
(SELECT tt.*
FROM [dbo].[tvfhrms_Calendar_DateRange] (t.datefrom, t.dateto) AS tt
) tt;
No need to have extra table with dates. Note that my dates in different format.
DECLARE #t TABLE (Id INT, DateFrom DATE, DateTo DATE)
INSERT INTO #t VALUES
(1,'01/15/2019','01/15/2019'),
(2,'01/21/2019','01/28/2019'),
(3,'02/04/2019','02/09/2019')
;WITH cte as (
SELECT ID, [Date] = DateFrom FROM #t
UNION ALL
SELECT t.ID, DATEADD(DAY,1,[Date]) FROM #t as t
INNER JOIN cte ON t.ID = cte.ID and cte.[Date] < t.DateTo
)
SELECT * FROM cte
ORDER BY ID

Select count with 0 count

Lets say I have following query:
SELECT top (5) CAST(Created AS DATE) as DateField,
Count(id) as Counted
FROM Table
GROUP BY CAST(Created AS DATE)
order by DateField desc
Lets say it will return following data set
DateField Counted
2016-01-18 34
2016-01-17 99
2016-01-14 1
2015-12-28 1
2015-12-27 6
But when I have Counted = 0 for certain Date I would like to get that in result set. So for example it should look like following
DateField Counted
2016-01-18 34
2016-01-17 99
2016-01-16 0
2016-01-15 0
2016-01-14 1
Thank you!
Expanding upon KM's answer, you need a date table which is like a numbers table.
There are many examples on the web but here's a simple one.
CREATE TABLE DateList (
DateValue DATE,
CONSTRAINT PK_DateList PRIMARY KEY CLUSTERED (DateValue)
)
GO
-- Insert dates from 01/01/2015 and 12/31/2015
DECLARE #StartDate DATE = '01/01/2015'
DECLARE #EndDatePlus1 DATE = '01/01/2016'
DECLARE #CurrentDate DATE = #StartDate
WHILE #EndDatePlus1 > #CurrentDate
BEGIN
INSERT INTO DateList VALUES (#CurrentDate)
SET #CurrentDate = DATEADD(dd,1,#CurrentDate)
END
Now you have a table
then you can rewrite your query as follows:
SELECT top (5) DateValue, isnull(Count(id),0) as Counted
FROM DateList
LEFT OUTER JOIN Table
on DateValue = CAST(Created AS DATE)
GROUP BY DateValue
order by DateValue desc
Two notes:
You'll need a where clause to specify your range.
A join on a cast isn't ideal. The type in your date table should match the type in your regular table.
One more solution as a single query:
;WITH dates AS
(
SELECT CAST(DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY [object_id]) - 1, '2016-01-14') as date) 'date'
FROM sys.all_objects
)
SELECT TOP 5
[date] AS 'DateField',
SUM(CASE WHEN Created IS NULL THEN 0 ELSE 1 END) AS 'Counted'
FROM dates
LEFT JOIN Table ON [date]=CAST(Created as date)
GROUP BY [date]
ORDER BY [date]
For a more edgy solution, you could use a recursive common table expression to create the date list. PLEASE NOTE: do not use recursive common table expressions in your day job! They are dangerous because it is easy to create one that never terminates.
DECLARE #StartDate date = '1/1/2016';
DECLARE #EndDate date = '1/15/2016';
WITH DateList(DateValue)
AS
(
SELECT DATEADD(DAY, 1, #StartDate)
UNION ALL
SELECT DATEADD(DAY, 1, DateValue)
FROM DateList
WHERE DateList.DateValue < #EndDate
)
SELECT DateValue, isnull(Count(id),0) as Counted
FROM DateList
LEFT OUTER JOIN [Table]
ON DateValue = CAST(Created AS DATE)
GROUP BY DateValue
ORDER BY DateValue DESC

create a statistics table using datetime MSSQL Query

I have little table which gives me a very hard time:
Person datetime1 datetime2
Eric 2012-10-01 09:00:05.000 2012-10-01 22:00:00.000
Anna 2012-10-02 06:00:05.000 2012-10-03 12:00:05.000
Richard 2012-10-03 09:00:05.000 2012-10-04 02:00:05.000
Chuck 2012-10-01 12:00:05.000 2012-10-01 23:00:05.000
I am trying to write a query, which gives me statistics table. This table contains information about when a user logged in and out (daily granularity):
Date logged_in logged_off
2012-10-01 2 2
2012-10-02 1 0
2012-10-03 1 1
2012-10-04 0 1
According to my research, a pivot command could solve the problem?
select Person,
SUM(case when datetime1 = '2012-10-01' then 1 else 0 end) as [loggeed_in],
SUM(case when datetime2 = '2012-10-01' then 1 else 0 end) as [logged_of]
from table
group by Person
This is not working... Do you have any ideas?
This will fix the current query, but don't know if it will solve the whole problem...
select Person,
SUM(case when convert(varchar(10), datetime1, 111) = '2012/10/01' then 1 else 0 end) as [loggeed_in],
SUM(case when convert(varchar(10), datetime2, 111) = '2012/10/01' then 1 else 0 end) as [logged_of]
from table
group by Person
EDIT: I believe this will better suit requirements...
SELECT
[Date] = dt,
logged_in = (
SELECT COUNT(*)
FROM table1
WHERE convert(varchar(10), datetime1, 111) = convert(varchar(10), dt, 111)),
logged_off = (
SELECT COUNT(*)
FROM table1
WHERE convert(varchar(10), datetime2, 111) = convert(varchar(10), dt, 111))
FROM (
SELECT TOP 1000
row_number() OVER(ORDER BY (SELECT 0)) AS N
FROM master.dbo.syscolumns sc1, master.dbo.syscolumns sc2) tally
CROSS APPLY(
SELECT dt = DATEADD(dd, tally.N - 1, '2012-10-1')) tallydt
WHERE dt BETWEEN (SELECT MIN(dateadd(dd, -1, datetime1)) FROM table1) AND (SELECT MAX(datetime2) FROM table1)
GROUP BY dt
ORDER BY dt
Here is the working solution:
WITH O AS (
SELECT
CAST([login Date & Time] AS DATE) loginDate
,COUNT(*) logined
FROM table
GROUP BY CAST([login Date & Time] AS DATE)
), C AS (
SELECT
CAST([Close Date & Time] AS DATE) CloseDate
,COUNT(*) Closed
FROM table
WHERE [Close Date & Time] IS NOT NULL
GROUP BY CAST([Close Date & Time] AS DATE)
)
SELECT
COALESCE(C.CloseDate, O.loginDate) TheDate
--,O.loginDate
--,C.CloseDate
,O.logined
,C.Closed
FROM O
FULL JOIN C
ON O.loginDate = C.CloseDate
ORDER BY TheDate

Concatenate date ranges in SQL (T/SQL preferred)

I need to concatenate rows with a date and a code into a date range
Table with two columns that are a composite primary key (date and a code )
Date Code
1/1/2011 A
1/2/2011 A
1/3/2011 A
1/1/2011 B
1/2/2011 B
2/1/2011 A
2/2/2011 A
2/27/2011 A
2/28/2011 A
3/1/2011 A
3/2/2011 A
3/3/2011 A
3/4/2011 A
Needs to be converted to
Start Date End Date Code
1/1/2011 1/3/2011 A
2/1/2011 2/2/2011 A
1/1/2011 1/2/2011 B
2/27/2011 3/4/2011 A
Is there any other way or is a cursor loop the only way?
declare #T table
(
[Date] date,
Code char(1)
)
insert into #T values
('1/1/2011','A'),
('1/2/2011','A'),
('1/3/2011','A'),
('1/1/2011','B'),
('1/2/2011','B'),
('3/1/2011','A'),
('3/2/2011','A'),
('3/3/2011','A'),
('3/4/2011','A')
;with C as
(
select *,
datediff(day, 0, [Date]) - row_number() over(partition by Code
order by [Date]) as rn
from #T
)
select min([Date]) as StartDate,
max([Date]) as EndDate,
Code
from C
group by Code, rn
sql server 2000 has it limitations. Rewrote the solution to make it more readable.
declare #t table
(
[Date] datetime,
Code char(1)
)
insert into #T values
('1/1/2011','A'),
('1/2/2011','A'),
('1/3/2011','A'),
('1/1/2011','B'),
('1/2/2011','B'),
('3/1/2011','A'),
('3/2/2011','A'),
('3/3/2011','A'),
('3/4/2011','A')
select a.code, a.date, min(b.date)
from
(
select *
from #t t
where not exists (select 1 from #t where t.code = code and t.date -1 = date)
) a
join
(
select *
from #t t
where not exists (select 1 from #t where t.code = code and t.date = date -1)
) b
on a.code = b.code and a.date <= b.date
group by a.code, a.date
Using a DatePart function for month will get you the "groups" you want
SELECT Min(Date) as StartDate, Max(Date) as EndDate, Code
FROM ThisTable Group By DatePart(m, Date), Code

SQL Server: How to select all days in a date range even if no data exists for some days

I have an app that needs to show a bar graph for activity over the last 30 days. The graph needs to show all days even if there is no activity for the day.
for example:
DATE COUNT
==================
1/1/2011 5
1/2/2011 3
1/3/2011 0
1/4/2011 4
1/5/2011 0
etc....
I could do post processing after the query to figure out what dates are missing and add them but was wondering if there is an easier way to do it in SQL Server. Thanks much
You can use a recursive CTE to build your list of 30 days, then join that to your data
--test
select cast('05 jan 2011' as datetime) as DT, 1 as val into #t
union all select CAST('05 jan 2011' as datetime), 1
union all select CAST('29 jan 2011' as datetime), 1
declare #start datetime = '01 jan 2011'
declare #end datetime = dateadd(day, 29, #start)
;with amonth(day) as
(
select #start as day
union all
select day + 1
from amonth
where day < #end
)
select amonth.day, count(val)
from amonth
left join #t on #t.DT = amonth.day
group by amonth.day
>>
2011-01-04 00:00:00.000 0
2011-01-05 00:00:00.000 2
2011-01-06 00:00:00.000 0
2011-01-07 00:00:00.000 0
2011-01-08 00:00:00.000 0
2011-01-09 00:00:00.000 0
...
Using CTE:
WITH DateTable
AS
(
SELECT CAST('20110101' AS Date) AS [DATE]
UNION ALL
SELECT DATEADD(dd, 1, [DATE])
FROM DateTable
WHERE DATEADD(dd, 1, [DATE]) < cast('20110201' as Date)
)
SELECT dt.[DATE], ISNULL(md.[COUNT], 0) as [COUNT]
FROM [DateTable] dt
LEFT JOIN [MyData] md
ON md.[DATE] = dt.[DATE]
This is assuming everything's a Date; if it's DateTime, you'll have to truncate (with DATEADD(dd, 0, DATEDIFF(dd, 0, [DATE]))).
#Alex K.'s answer is completely correct, but it doesn't work for versions that do not support Recursive common table expressions (like the version I'm working with). In this case the following would do the job.
DECLARE #StartDate datetime = '2015-01-01'
DECLARE #EndDate datetime = SYSDATETIME()
;WITH days AS
(
SELECT DATEADD(DAY, n, DATEADD(DAY, DATEDIFF(DAY, 0, #StartDate), 0)) as d
FROM ( SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate) + 1)
n = ROW_NUMBER() OVER (ORDER BY [object_id]) - 1
FROM sys.all_objects ORDER BY [object_id] ) AS n
)
select days.d, count(t.val)
FROM days LEFT OUTER JOIN yourTable as t
ON t.dateColumn >= days.d AND t.dateColumn < DATEADD(DAY, 1, days.d)
GROUP BY days.d
ORDER BY days.d;
My scenario was a bit more complex than the OP example, so thought I'd share to help others who have similar issues. I needed to group sales orders by date taken, whereas the orders are stored with datetime.
So in the "days" lookup table I could not really store as a date time with the time being '00:00:00.000' and get any matches. Therefore I stored as a string and I tried to join on the converted value directly.
That did not return any zero rows, and the solution was to do a sub-query returning the date already converted to a string.
Sample code as follows:
declare #startDate datetime = convert(datetime,'09/02/2016')
declare #curDate datetime = #startDate
declare #endDate datetime = convert(datetime,'09/09/2016')
declare #dtFormat int = 102;
DECLARE #null_Date varchar(24) = '1970-01-01 00:00:00.000'
/* Initialize #days table */
select CONVERT(VARCHAR(24),#curDate, #dtFormat) as [Period] into #days
/* Populate dates into #days table */
while (#curDate < #endDate )
begin
set #curDate = dateadd(d, 1, #curDate)
insert into #days values (CONVERT(VARCHAR(24),#curDate, #dtFormat))
end
/* Outer aggregation query to group by order numbers */
select [Period], count(c)-case when sum(c)=0 then 1 else 0 end as [Orders],
sum(c) as [Lines] from
(
/* Inner aggregation query to sum by order lines */
select
[Period], sol.t_orno, count(*)-1 as c
from (
/* Inner query against source table with date converted */
select convert(varchar(24),t_dldt, #dtFormat) as [shipdt], t_orno
from salesorderlines where t_dldt > #startDate
) sol
right join #days on shipdt = #days.[Period]
group by [Period], sol.t_orno
) as t
group by Period
order by Period desc
drop table #days
Sample Results:
Period Orders Lines
2016.09.09 388 422
2016.09.08 169 229
2016.09.07 1 1
2016.09.06 0 0
2016.09.05 0 0
2016.09.04 165 241
2016.09.03 0 0
2016.09.02 0 0
Either define a static table containing dates or create a temp table \ table variable on the fly to store each date between (and including) the min and max dates in the activity table you're working with.
Use an outer join between the two tables to make sure that each date in your dates table is reflected in the output.
If you use a static dates table you will likely want to limit the date range that is output to only the range needed in the graph.
Without Transact-SQL: MS SQL 2005 - Get a list of all days of a Month:
In my case '20121201' is a predefined value.
SELECT TOp (Select Day(DateAdd(day, -Day(DateAdd(month, 1,
'20121201')),
DateAdd(month, 1, '20121201')))) DayDate FROM ( SELECT DATEADD(DAY,ROW_NUMBER() OVER (ORDER BY (SELECT
NULL))-1,'20121201') as DayDate FROM sys.objects s1 CROSS JOIN
sys.objects s2 ) q
Recursive CTE works for max 80 years which is good enough:
DECLARE #dStart DATE,
#dEnd DATE
SET #dStart = GETDATE ()
SET #dEnd = DATEADD (YEAR, 80, #dStart)
;WITH CTE AS
(
SELECT #dStart AS dDay
UNION ALL
SELECT DATEADD (DAY, 1, dDay)
FROM CTE
WHERE dDay < #dEnd
)
SELECT * FROM CTE
OPTION (MaxRecursion 32767)
create a numbers table and use it like:
declare #DataTable table (DateColumn datetime)
insert #DataTable values ('2011-01-09')
insert #DataTable values ('2011-01-10')
insert #DataTable values ('2011-01-10')
insert #DataTable values ('2011-01-11')
insert #DataTable values ('2011-01-11')
insert #DataTable values ('2011-01-11')
declare #StartDate datetime
SET #StartDate='1/1/2011'
select
#StartDate+Number,SUM(CASE WHEN DateColumn IS NULL THEN 0 ELSE 1 END)
FROM Numbers
LEFT OUTER JOIN #DataTable ON DateColumn=#StartDate+Number
WHERE Number>=1 AND Number<=15
GROUP BY #StartDate+Number
OUTPUT:
----------------------- -----------
2011-01-02 00:00:00.000 0
2011-01-03 00:00:00.000 0
2011-01-04 00:00:00.000 0
2011-01-05 00:00:00.000 0
2011-01-06 00:00:00.000 0
2011-01-07 00:00:00.000 0
2011-01-08 00:00:00.000 0
2011-01-09 00:00:00.000 1
2011-01-10 00:00:00.000 2
2011-01-11 00:00:00.000 3
2011-01-12 00:00:00.000 0
2011-01-13 00:00:00.000 0
2011-01-14 00:00:00.000 0
2011-01-15 00:00:00.000 0
2011-01-16 00:00:00.000 0
(15 row(s) affected)
Maybe something like this:
Create DaysTable countaining the 30 days.
And DataTable containing "day" column and "count" column.
And then left join them.
WITH DaysTable (name) AS (
SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 -- .. And so on to 30
),
DataTable (name, value) AS (
SELECT DATEPART(DAY, [Date]), [Count]
FROM YourExampleTable
WHERE [Date] < DATEADD (day , -30 , getdate())
)
SELECT DaysTable.name, DataTable.value
FROM DaysTable LEFT JOIN
DataTable ON DaysTable.name = DataTable.name
ORDER BY DaysTable.name
For those with a recursion allergy
select SubQ.TheDate
from
(
select DATEADD(day, a.a + (10 * b.a) + (100 * c.a), DATEADD(day, DATEDIFF(day, 0, GETDATE()), 0) - 30) AS TheDate
from
(
(select 0 as a 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) as a
cross join (select 0 as a 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) as b
cross join (select 0 as a 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) as c
)
WHERE a.a + (10 * b.a) + (100 * c.a) < 30
) AS SubQ
ORDER BY TheDate
Try it.
DECLARE #currentDate DATETIME = CONVERT(DATE, GetDate())
DECLARE #startDate DATETIME = DATEADD(DAY, -DAY(#currentDate)+1, #currentDate)
;WITH fnDateNow(DayOfDate) AS
(
SELECT #startDate AS DayOfDate
UNION ALL
SELECT DayOfDate + 1 FROM fnDateNow WHERE DayOfDate < #currentDate
) SELECT fnDateNow.DayOfDate FROM fnDateNow
DECLARE #StartDate DATE = '20110101', #NumberOfYears INT = 1;
DECLARE #CutoffDate DATE = DATEADD(YEAR, #NumberOfYears, #StartDate);
CREATE TABLE Calender
(
[date] DATE
);
INSERT Calender([date])
SELECT d
FROM
(
SELECT d = DATEADD(DAY, rn - 1, #StartDate)
FROM
(
SELECT TOP (DATEDIFF(DAY, '2011-01-01', '2011-12-31'))
rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
ORDER BY s1.[object_id]
) AS x
) AS y;
create table test(a date)
insert into test values('1/1/2011')
insert into test values('1/1/2011')
insert into test values('1/1/2011')
insert into test values('1/1/2011')
insert into test values('1/1/2011')
insert into test values('1/2/2011')
insert into test values('1/2/2011')
insert into test values('1/2/2011')
insert into test values('1/4/2011')
insert into test values('1/4/2011')
insert into test values('1/4/2011')
insert into test values('1/4/2011')
select c.date as DATE,count(t.a) as COUNT from calender c left join test t on c.date = t.a group by c.date

Resources