Alex
Alex

Reputation: 1084

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

Upvotes: 34

Views: 91987

Answers (12)

Tone Škoda
Tone Škoda

Reputation: 1523

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)

Upvotes: 2

Shane Kenyon
Shane Kenyon

Reputation: 5381

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

Upvotes: 2

Partha
Partha

Reputation: 75

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

Upvotes: 0

pedram bashiri
pedram bashiri

Reputation: 1386

@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;

Upvotes: 7

Tony Hung
Tony Hung

Reputation: 54

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

Upvotes: 0

Stefan Brendle
Stefan Brendle

Reputation: 1564

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

Upvotes: 1

sav
sav

Reputation: 2150

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

Upvotes: 0

Ian Pugsley
Ian Pugsley

Reputation: 1062

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]))).

Upvotes: 11

Alex K.
Alex K.

Reputation: 175876

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
...

Upvotes: 39

Oleg Grishko
Oleg Grishko

Reputation: 4281

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

Upvotes: 0

KM.
KM.

Reputation: 103637

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)

Upvotes: 0

Tim Lentine
Tim Lentine

Reputation: 7862

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.

Upvotes: 1

Related Questions