Repeating rows from right join - sql

My application saves logs that need to be taken at least one time during each of the 3 different time periods during the day. So ideally 3 logs per day, each with a unique time period ID. I need to write an exception report (MSSQL 2008) that will show when a time period is missed for any given day. I have a LogTimePeriods table that contains 3 rows for each of the time periods. The Logs table contains the LogTimePeriodID so I do not need to do any logic to see what Time period the log belongs in (that is done via the application).
I know I need something along the lines of a right/left join to try to match all the LogTimePeriodID for every Log row for a given date. I cant seem to make any progress. Any help is appreciated! Thanks for reading.
SQL Fiddle
EDIT: Desired output below
Date | LogPeriodID
6/3 | 3
6/5 | 2
6/5 | 3

Your SQL Fiddle is set to use MYSQL, not SQL Server 2008, so I can't test my answer against your data: however, based on my understanding of your requirements and assuming you are querying a SQL 2008 database, the following example should work for you (the references to my table variables would obviously be replaced with your actual tables).
DECLARE #StartDate DATE = '06/04/2014'
DECLARE #EndDate DATE = GETDATE();
DECLARE #LogTimePeriod TABLE (LogTimePeriodID INT IDENTITY(1,1), TimePeriod VARCHAR(20))
INSERT INTO #LogTImePeriod (TimePeriod) SELECT '00:00 - 07:59'
INSERT INTO #LogTImePeriod (TimePeriod) SELECT '08:00 - 15:59'
INSERT INTO #LogTImePeriod (TimePeriod) SELECT '16:00 - 23:59'
DECLARE #logs TABLE (LogDataID INT IDENTITY(1,1), LogDate DATE, SomeInformation VARCHAR(10), LogTimePeriodID INT)
INSERT INTO #logs (SomeInformation, LogDate, LogTimePeriodID) SELECT 'abc', '6/4/2014', 1
INSERT INTO #logs (SomeInformation, LogDate, LogTimePeriodID) SELECT 'def', '6/4/2014', 2
INSERT INTO #logs (SomeInformation, LogDate, LogTimePeriodID) SELECT 'ghi', '6/4/2014', 3
INSERT INTO #logs (SomeInformation, LogDate, LogTimePeriodID) SELECT 'abc', '6/5/2014', 1
INSERT INTO #logs (SomeInformation, LogDate, LogTimePeriodID) SELECT 'def', '6/5/2014', 2;
WITH dates AS (
SELECT CAST(#StartDate AS DATETIME) 'date'
UNION ALL
SELECT DATEADD(dd, 1, t.date)
FROM dates t
WHERE DATEADD(dd, 1, t.date) <= #EndDate)
SELECT ltp.LogTimePeriodID, ltp.TimePeriod, dates.date
FROM
#LogTimePeriod ltp
INNER JOIN
dates ON 1=1
LEFT JOIN
#logs ld ON
ltp.LogTimePeriodID = ld.LogTimePeriodID AND
dates.date = ld.LogDate
WHERE ld.LogDataID IS NULL
OPTION (MAXRECURSION 1000) -- 0 is unlimited, 1000 limits to 1000 rows

SQLServer 2008 has the EXCEPT keyword that subtract the second recordset from the first.
In this case it's possible to generate all the possible logs and remove from those the ones in the logs table, that will left the logs not present in the table.
SELECT DISTINCT StartDateTime, StartTime, EndTime
FROM Logs
CROSS JOIN LogTimePeriods
EXCEPT
SELECT StartDateTime, StartTime, EndTime
FROM LogTimePeriods ltp
LEFT JOIN logs l ON l.LogTimePeriodID = ltp.LogTimePeriodID
ORDER BY StartDateTime, StartTime
SQLFiddle demo with you data converted to SQLServer 2008

Related

SQL - How to ignore seconds and round down minutes in DateTime data type

At work we did a project that required a team to count students 8 times a day over 5 days at specific time periods. They are, as follows :-
09:00, 10:00, 11:00, 13:15, 14:15, 14:50, 15:50, 16:20.
Now, the data collected was put directly into a database via a web app. The problem is that database recorded each record using the standard YYYY-MM-DD HH:MM:SS.MIL, but if I were to order the records by date and then by student count it would cause the following problem;
e.g.:-
if the students counted in a room was 5 at 09:00:12, but another room had a count of 0 at 09:02:20 and I did the following:
select student_count, audit_date
from table_name
order by audit_date, student_count;
The query will return:
5 09:00:12
0 09:02:20
but I want:
0 09:00:00
5 09:00:00
because we're looking for the number of students in each room for the period 09:00, but unfortunately to collect the data it required us to do so within that hour and obviously the database will pick up on that accuracy. Furthermore, this issue becomes more problematic when it gets to the periods 14:15 and 14:50, where we will need to be able to distinguish between the two periods.
Is there a way to ignore the seconds part of the DateTime, and the round the minutes down to the nearest ten minute?
I'm using SQL Server Management Studio 2012. If none of this made sense, I'm sorry!
You may want some sort of Period table to store your segments. Then you can use that to join to your counts table.
CREATE TABLE [Periods]
( -- maybe [id] INT,
[start_time] TIME,
[end_time] TIME
);
INSERT INTO [Periods]
VALUES ('09:00','10:00'),
('10:00','11:00'),
('11:00','13:15'),
('13:15','14:15'),
('14:15','14:50'),
('14:50','15:50'),
('15:50','16:20'),
('16:20','17:00')
SELECT
student_count, [start_time]
FROM table_name A
INNER JOIN [Periods] B
ON CAST(A.[audit_date] AS TIME) >= B.[start_time]
AND CAST(A.[audit_date] AS TIME) < B.[end_time]
You can use the DATEADDand DATEPARTfunctions to accomplish this together with a CASEexpression. If you want more precise cutoffs between the .14and .50periods you can easily adjust the case statement and also if you want to minutes to be .10or.15
-- some test data
declare #t table (the_time time)
insert #t values ('09:00:12')
insert #t values ('14:16:12')
insert #t values ('09:02:12')
insert #t values ('14:22:12')
insert #t values ('15:49:12')
insert #t values ('15:50:08')
select
the_time,
case
when datepart(minute,the_time) < 15 then
dateadd(second, -datepart(second,the_time),dateadd(minute, -datepart(minute,the_time),the_time))
when datepart(minute,the_time) >= 15 and datepart(minute,the_time) < 50 then
dateadd(second, -datepart(second,the_time),dateadd(minute, -datepart(minute,the_time)+10,the_time))
else
dateadd(second, -datepart(second,the_time),dateadd(minute, -datepart(minute,the_time)+50,the_time))
end as WithoutSeconds
from #t
Results:
the_time WithoutSeconds
---------------- ----------------
09:00:12.0000000 09:00:00.0000000
14:16:12.0000000 14:10:00.0000000
09:02:12.0000000 09:00:00.0000000
14:22:12.0000000 14:10:00.0000000
15:49:12.0000000 15:10:00.0000000
15:50:08.0000000 15:50:00.0000000
Try this:
SELECT
CAST(
DATEADD(SECOND, - (CONVERT(INT, RIGHT(CONVERT(CHAR(2),
DATEPART(MINUTE, GETDATE())),1))*60) - (DATEPART(SECOND,GETDATE())), GETDATE())
AS SMALLDATETIME);
You can try ORDER BY this formula
DATEADD(minute, floor((DATEDIFF(minute, '20000101', audit_date) + 5)/10)*10, '20000101')
e.g.
WITH tbl AS(
SELECT * FROM ( VALUES (5,'2014-03-28 09:00:09.793'),(0,'2014-03-28 09:02:20.123')) a (student_count, audit_date)
)
SELECT *,DATEADD(minute, floor((DATEDIFF(minute, '20000101', audit_date) + 5)/10)*10, '20000101') as ORDER_DT
FROM tbl
ORDER BY ORDER_DT,student_count
SQL Fiddle

How do I compare dates in one SQL table to a range defined in another table?

I have one table holding events and dates:
NAME | DOB
-------------------
Adam | 6/26/1999
Barry | 7/18/2005
Daniel| 1/18/1984
I have another table defining date ranges as either start or end times, each with a descriptive code:
CODE | DATE
---------------------
YearStart| 6/28/2013
YearEnd | 8/14/2013
I am trying to write SQL that will find all Birthdates that fall between the start and end of the times described in the second table. The YearStart will always be in June, and the YearEnd will always be in August. My thought was to try:
SELECT
u.Name
CAST(MONTH(u.DOB) AS varchar) + '/' + CAST(DAY(u.DOB) AS varchar) as 'Birthdate',
u.DOB as 'Birthday'
FROM
Users u
WHERE
MONTH(DOB) = '7' OR
(MONTH(DOB) = '6' AND DAY(DOB) >= DAY(SELECT d.Date FROM Dates d WHERE d.Code='YearStart')) OR
(MONTH(DOB) = '8' AND DAY(DOB) <= DAY(SELECT d.Date FROM Dates d WHERE d.Code='YearEnd')))
ORDER BY
MONTH(DOB) ASC, DAY(DOB) ASC
But this doesn't pass, I'm guessing because there is no guarantee that the internal SELECT statement will return only one row, so cannot be parsed as a datetime. How do I actually accomplish this query?
This seems strange and I still feel like we're missing a relevant piece of the requirements, but look at the following. It seems from your description that the years are irrelevant and you want birthdays that fall between the given months/days.
SELECT
t1.Name, t1.DOB
FROM
t1
JOIN t2 AS startDate ON (startDate.Code = 'YearStart')
JOIN t2 AS endDate ON (endDate.Code = 'YearEnd')
WHERE
STUFF(CONVERT(varchar, t1.DOB, 112), 1, 4, '') BETWEEN
STUFF(CONVERT(varchar, startDate.[Date], 112), 1, 4, '')
AND
STUFF(CONVERT(varchar, endDate.[Date], 112), 1, 4, '')
Try using a PIVOT to get the years on the same row, like this. This will return only 'Bob'
DECLARE #Names TABLE(
NAME VARCHAR(20),
DOB VARCHAR(10));
DECLARE #Dates TABLE(
CODE VARCHAR(20),
THEDATE VARCHAR(10));
INSERT #Names (NAME,DOB) VALUES ('Adam', '6/26/1999');
INSERT #Names (NAME,DOB) VALUES ('Daniel', '1/18/1984');
INSERT #Names (NAME,DOB) VALUES ('Bob', '7/1/2013');
INSERT #Dates (CODE,THEDATE) VALUES ('YearStart', '6/28/2013');
INSERT #Dates (CODE,THEDATE) VALUES ('YearEnd', '8/14/2013');
SELECT * FROM #Names;
SELECT * FROM #Dates;
SELECT n.*
FROM #Names AS n
INNER JOIN (
SELECT
1 AS YearTypeId
, [YearStart]
, [YearEnd]
FROM ( SELECT [CODE]
, THEDATE
FROM #Dates
) p PIVOT ( MIN(THEDATE)
FOR [CODE]
IN ([YearStart],[YearEnd])
) AS pvt) AS y
ON
n.DOB >= y.YearStart
AND n.DOB <= y.YearEnd
From the last paragraph in your question, I am assuming that the Dates table have one YearStart and one YearEnd row for each year, correct? If so, your SQL query should include the year you are interrested in.
Also, even if "date" is not strictly speaking a reserved word for SQL Server (see Reserved Keywords for Transact SQL), you should avoid using such column names since, for example, ODBC does not allow them.
But to do something with only the information that you have already provided, you could do something like this to get the birthday celebrants for the last year defined in Dates (providing there really is both a YearStart and YearEnd entry for that year):
SELECT DISTINCT <the rest as in your example>
FROM Users u
WHERE u.DOB >= (SELECT max(d1.Date) FROM Dates d1 WHERE d1.Code = 'YearStart')
AND u.DOB <= (SELECT max(d2.Date) FROM Dates d2 WHERE d2.Code = 'YearEnd')
ORDER BY u.DOB;
The main difference between the query above (which I have not tested - this is just to show the principle) and your post is that I trust the datetime type (or whichever variant of it that you have used in the database) to work as intended. What I mean by that is that the database engine is well aware (well, it should be) of which of two full dates is the earliest and the latest - you do not have to extract and compare their components separately.
/Bosse

Is there a way to GROUP BY a time interval in this table?

I have a table like this one:
DateTime A
10:00:01 2
10:00:07 4
10:00:10 2
10:00:17 1
10:00:18 3
Is this possible to create a query that returns me the average value of A each 10 seconds? In this case the result would be:
3 (4+2)/2
2 (2+1+3)/3
Thanks in advance!
EDIT: If you really think that this can not be done just say NO WAY! :) It's an acceptable answer, I really don't know if this can be done.
EDIT2: I'm using SQL Server 2008. I would like to have different groupings but fixed. For example, ranges each 10 sec, 1 minute, 5 minutes, 30 minutes, 1 hour and 1 day (just an example but something like that)
In SQL Server, you can use DATEPART and then group by hour, minute and second integer-division 10.
CREATE TABLE #times
(
thetime time,
A int
)
INSERT #times
VALUES ('10:00:01', 2)
INSERT #times
VALUES ('10:00:07', 4)
INSERT #times
VALUES ('10:00:10', 2)
INSERT #times
VALUES ('10:00:17', 1)
INSERT #times
VALUES ('10:00:18', 3)
SELECT avg(A) -- <-- here you might deal with precision issues if you need non-integer results. eg: (avg(a * 1.0)
FROM #times
GROUP BY datepart(hour, thetime), DATEPART(minute, thetime), DATEPART(SECOND, thetime) / 10
DROP TABLE #times
It depends on DBMS you are using.
In Oracle you can do the following:
SELECT AVG(A)
FROM MYTABLE
GROUP BY to_char(DateTime, 'HH24:MI')
CREATE TABLE IF NOT EXISTS `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dtime` datetime NOT NULL,
`val` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
INSERT INTO `test` (`id`, `dtime`, `val`) VALUES
(1, '2011-09-27 18:36:19', 8),
(2, '2011-09-27 18:36:21', 4),
(3, '2011-09-27 18:36:27', 5),
(4, '2011-09-27 18:36:35', 3),
(5, '2011-09-27 18:36:37', 2);
SELECT *, AVG(val) FROM test GROUP BY FLOOR(UNIX_TIMESTAMP(dtime) / 10)
Someone may come along and give you an answer with full code, but the way I would approach this is to break it down to several smaller problems/solutions:
(1) Create a temp table with intervals. See the accepted answer on this question:
Get a list of dates between two dates
This answer was for MySQL, but should get you started. Googling "Create intervals SQL" should also yield additional ways to accomplish this. You will want to use the MAX(DateTime) and MIN(DateTime) from your main table as inputs into whatever method you use (and 10 seconds for the span, obviously).
(2) Join the temp table with your main table, with a join condition of (pseudocode):
FROM mainTable m INNER JOIN #tempTable t ON m BETWEEN t.StartDate AND t.EndDate
(3) Now it should be as simple as correctly SELECTing and GROUPing:
SELECT
AVG(m.A)
FROM
mainTable m
INNER JOIN #tempTable t ON m BETWEEN t.StartDate AND t.EndDate
GROUP BY
t.StartDate
Edit: if you want to see intervals with that have no records (zero average), you would have to rearrage the query, use a LEFT JOIN, and COALESCE on m.A (see below). If you don't care about seeing such interals, OCary's solution is better/cleaner.
SELECT
AVG(COALESCE(m.A, 0))
FROM
#tempTable t
LEFT JOIN mainTable m ON m BETWEEN t.StartDate AND t.EndDate
GROUP BY
t.StartDate
I approached this by using a Common Table Expression to get all the periods between any given dates of my data. In principal you could change the interval to any SQL interval.
DECLARE #interval_minutes INT = 5, #start_date DATETIME = '20130201', #end_date DATETIME = GETDATE()
;WITH cte_period AS
(
SELECT CAST(#start_date AS DATETIME) AS [date]
UNION ALL
SELECT DATEADD(MINUTE, #interval_minutes, cte_period.[date]) AS [date]
FROM cte_period
WHERE DATEADD(MINUTE, #interval_minutes, cte_period.[date]) < #end_date
)
, cte_intervals AS
(SELECT [first].[date] AS [Start], [second].[date] AS [End]
FROM cte_period [first]
LEFT OUTER JOIN cte_period [second] ON DATEADD(MINUTE, 5, [first].[date]) = [second].[date]
)
SELECT i.[Start], AVG(data)
FROM cte_intervals i
LEFT OUTER JOIN your_data mu ON mu.your_date_time >= i.Start and mu.your_date_time < i.[End]
GROUP BY i.[Start]
OPTION (MAXRECURSION 0)
From http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=142634 you can use the following query as well:
select dateadd(minute, datediff(minute, 0, timestamp ) / 10 * 10, 0), avg ( value )
from yourtable
group by dateadd(minute, datediff(minute, 0, timestamp ) / 10 * 10, 0)
which someone then expands upon to suggest:
Select
a.MyDate,
Start_of_10_Min =
dateadd(mi,(datepart(mi,a.MyDate)/10)*10,dateadd(hh,datediff(hh,0,a.Mydate),0))
from
( -- Test Data
select MyDate = getdate()
) a
although I'm not too how they plan on getting the average in in the second suggestion.
Personally I prefer OCary's answer as I know what is going on there and that I'll be able to understand it in 6 months time without looking it up again but I include this one for completeness.

group data by any range of 30 days (not by range of dates) in SQL Server

I got a table with a list of transactions.
for the example, lets say it has 4 fields:
ID, UserID, DateAddedd, Amount
I would like to run a query that checks if there was a time, that in 30 days, a user made transactions in the sum of 100 or more
I saw lots of samples of grouping by month or a day but the problem is that if for example
a user made a 50$ transaction on the 20/4 and on the 5/5 he made another 50$ transaction, the query should show it. (its 100$ or more in a period of 30 days)
I think that this should work (I'm assuming that transactions have a date component, and that a user can have multiple transactions on a single day):
;with DailyTransactions as (
select UserID,DATEADD(day,DATEDIFF(day,0,DateAdded),0) as DateOnly,SUM(Amount) as Amount
from Transactions group by UserID,DATEADD(day,DATEDIFF(day,0,DateAdded),0)
), Numbers as (
select ROW_NUMBER() OVER (ORDER BY object_id) as n from sys.objects
), DayRange as (
select n from Numbers where n between 1 and 29
)
select
dt.UserID,dt.DateOnly as StartDate,MAX(ot.DateOnly) as EndDate, dt.Amount + COALESCE(SUM(ot.Amount),0) as TotalSpend
from
DailyTransactions dt
cross join
DayRange dr
left join
DailyTransactions ot
on
dt.UserID = ot.UserID and
DATEADD(day,dr.n,dt.DateOnly) = ot.DateOnly
group by dt.UserID,dt.DateOnly,dt.Amount
having dt.Amount + COALESCE(SUM(ot.Amount),0) >= 100.00
Okay, I'm using 3 common table expressions. The first (DailyTransactions) is reducing the transactions table to a single transaction per user per day (this isn't necessary if the DateAdded is a date only, and each user has a single transaction per day). The second and third (Numbers and DayRange) are a bit of a cheat - I wanted to have the numbers 1-29 available to me (for use in a DATEADD). There are a variety of ways of creating either a permanent or (as in this case) temporary Numbers table. I just picked one, and then in DayRange, I filter it down to the numbers I need.
Now that we have those available to us, we write the main query. We're querying for rows from the DailyTransactions table, but we want to find later rows in the same table that are within 30 days. That's what the left join to DailyTransactions is doing. It's finding those later rows, of which there may be 0, 1 or more. If it's more than one, we want to add all of those values together, so that's why we need to do a further bit of grouping at this stage. Finally, we can write our having clause, to filter down only to those results where the Amount from a particular day (dt.Amount) + the sum of amounts from later days (SUM(ot.Amount)) meets the criteria you set out.
I based this on a table defined thus:
create table Transactions (
UserID int not null,
DateAdded datetime not null,
Amount decimal (38,2)
)
If I understand you correctly, you need a calendar table and then check the sum between date and date+30. So if you want to check a period of 1 year you need to check something like 365 periods.
Here is one way of doing that. The recursive CTE creates the calendar and the cross apply calculates the sum for each CalDate between CalDate and CalDate+30.
declare #T table(ID int, UserID int, DateAdded datetime, Amount money)
insert into #T values(1, 1, getdate(), 50)
insert into #T values(2, 1, getdate()-29, 60)
insert into #T values(4, 2, getdate(), 40)
insert into #T values(5, 2, getdate()-29, 50)
insert into #T values(7, 3, getdate(), 70)
insert into #T values(8, 3, getdate()-30, 80)
insert into #T values(9, 4, getdate()+50, 50)
insert into #T values(10,4, getdate()+51, 50)
declare #FromDate datetime
declare #ToDate datetime
select
#FromDate = min(dateadd(d, datediff(d, 0, DateAdded), 0)),
#ToDate = max(dateadd(d, datediff(d, 0, DateAdded), 0))
from #T
;with cal as
(
select #FromDate as CalDate
union all
select CalDate + 1
from cal
where CalDate < #ToDate
)
select S.UserID
from cal as C
cross apply
(select
T.UserID,
sum(Amount) as Amount
from #T as T
where T.DateAdded between CalDate and CalDate + 30
group by T.UserID) as S
where S.Amount >= 100
group by S.UserID
option (maxrecursion 0)

Select data from SQL DB per day

I have a table with order information in an E-commerce store. Schema looks like this:
[Orders]
Id|SubTotal|TaxAmount|ShippingAmount|DateCreated
This table does only contain data for every Order. So if a day goes by without any orders, no sales data is there for that day.
I would like to select subtotal-per-day for the last 30 days, including those days with no sales.
The resultset would look like this:
Date | SalesSum
2009-08-01 | 15235
2009-08-02 | 0
2009-08-03 | 340
2009-08-04 | 0
...
Doing this, only gives me data for those days with orders:
select DateCreated as Date, sum(ordersubtotal) as SalesSum
from Orders
group by DateCreated
You could create a table called Dates, and select from that table and join the Orders table. But I really want to avoid that, because it doesn't work good enough when dealing with different time zones and things...
Please don't laugh. SQL is not my kind of thing... :)
Create a function that can generate a date table as follows:
(stolen from http://www.codeproject.com/KB/database/GenerateDateTable.aspx)
Create Function dbo.fnDateTable
(
#StartDate datetime,
#EndDate datetime,
#DayPart char(5) -- support 'day','month','year','hour', default 'day'
)
Returns #Result Table
(
[Date] datetime
)
As
Begin
Declare #CurrentDate datetime
Set #CurrentDate=#StartDate
While #CurrentDate<=#EndDate
Begin
Insert Into #Result Values (#CurrentDate)
Select #CurrentDate=
Case
When #DayPart='year' Then DateAdd(yy,1,#CurrentDate)
When #DayPart='month' Then DateAdd(mm,1,#CurrentDate)
When #DayPart='hour' Then DateAdd(hh,1,#CurrentDate)
Else
DateAdd(dd,1,#CurrentDate)
End
End
Return
End
Then, join against that table
SELECT dates.Date as Date, sum(SubTotal+TaxAmount+ShippingAmount)
FROM [fnDateTable] (dateadd("m",-1,CONVERT(VARCHAR(10),GETDATE(),111)),CONVERT(VARCHAR(10),GETDATE(),111),'day') dates
LEFT JOIN Orders
ON dates.Date = DateCreated
GROUP BY dates.Date
declare #oldest_date datetime
declare #daily_sum numeric(18,2)
declare #temp table(
sales_date datetime,
sales_sum numeric(18,2)
)
select #oldest_date = dateadd(day,-30,getdate())
while #oldest_date <= getdate()
begin
set #daily_sum = (select sum(SubTotal) from SalesTable where DateCreated = #oldest_date)
insert into #temp(sales_date, sales_sum) values(#oldest_date, #daily_sum)
set #oldest_date = dateadd(day,1,#oldest_date)
end
select * from #temp
OK - I missed that 'last 30 days' part. The bit above, while not as clean, IMHO, as the date table, should work. Another variant would be to use the while loop to fill a temp table just with the last 30 days and do a left outer join with the result of my original query.
including those days with no sales.
That's the difficult part. I don't think the first answer will help you with that. I did something similar to this with a separate date table.
You can find the directions on how to do so here:
Date Table
I have a Log table table with LogID an index which i never delete any records. it has index from 1 to ~10000000. Using this table I can write
select
s.ddate, SUM(isnull(o.SubTotal,0))
from
(
select
cast(datediff(d,LogID,getdate()) as datetime) AS ddate
from
Log
where
LogID <31
) s right join orders o on o.orderdate = s.ddate
group by s.ddate
I actually did this today. We also got a e-commerce application. I don't want to fill our database with "useless" dates. I just do the group by and create all the days for the last N days in Java, and peer them with the date/sales results from the database.
Where is this ultimately going to end up? I ask only because it may be easier to fill in the empty days with whatever program is going to deal with the data instead of trying to get it done in SQL.
SQL is a wonderful language, and it is capable of a great many things, but sometimes you're just better off working the finer points of the data in the program instead.
(Revised a bit--I hit enter too soon)
I started poking at this, and as it hits some pretty tricky SQL concepts it quickly grew into the following monster. If feasible, you might be better off adapting THEn's solution; or, like many others advise, using application code to fill in the gaps could be preferrable.
-- A temp table holding the 30 dates that you want to check
DECLARE #Foo Table (Date smalldatetime not null)
-- Populate the table using a common "tally table" methodology (I got this from SQL Server magazine long ago)
;WITH
L0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows
L1 AS (SELECT 1 AS C FROM L0 AS A, L0 AS B),--4 rows
L2 AS (SELECT 1 AS C FROM L1 AS A, L1 AS B),--16 rows
L3 AS (SELECT 1 AS C FROM L2 AS A, L2 AS B),--256 rows
Tally AS (SELECT ROW_NUMBER() OVER(ORDER BY C) AS Number FROM L3)
INSERT #Foo (Date)
select dateadd(dd, datediff(dd, 0, dateadd(dd, -number + 1, getdate())), 0)
from Tally
where Number < 31
Step 1 is to build a temp table containint the 30 dates that you are concerned with. That abstract wierdness is about the fastest way known to build a table of consecutive integers; add a few more subqueries, and you can populate millions or more in mere seconds. I take the first 30, and use dateadd and the current date/time to convert them into dates. If you already have a "fixed" table that has 1-30, you can use that and skip the CTE entirely (by replacing table "Tally" with your table).
The outer two date function calls remove the time portion of the generated string.
(Note that I assume that your order date also has no time portion -- otherwise you've got another common problem to resolve.)
For testing purposes I built table #Orders, and this gets you the rest:
SELECT f.Date, sum(ordersubtotal) as SalesSum
from #Foo f
left outer join #Orders o
on o.DateCreated = f.Date
group by f.Date
I created the Function DateTable as JamesMLV pointed out to me.
And then the SQL looks like this:
SELECT dates.date, ISNULL(SUM(ordersubtotal), 0) as Sales FROM [dbo].[DateTable] ('2009-08-01','2009-08-31','day') dates
LEFT JOIN Orders ON CONVERT(VARCHAR(10),Orders.datecreated, 111) = dates.date
group by dates.date
SELECT DateCreated,
SUM(SubTotal) AS SalesSum
FROM Orders
GROUP BY DateCreated