Reputation: 7766
I have to generate a sales report for a date range in which user will select day -start hour and day- end hour.
The below query will return date-time and amount of sale
select s.StartDate ,
CONVERT(DECIMAL(10,2),sum(OrigionalSubTotal)/100.0) Amt from Sale s
where
s.StartDate
BETWEEN '2016-06-12 04:00:01'
and '2016-06-18 04:00:00'
and s.IsSuspend = 0 and s.IsTrainMode = 0 and wasrefunded=0
and IsCancelled = 0
group by S.StartDate
order by s.StartDate
O/p
StartDate Amt
2016-06-12 10:01:15.780 10.00
2016-06-12 10:15:57.360 20.00
2016-06-12 12:48:41.250 50.00
2016-06-13 11:02:50.850 5.00
2016-06-13 12:04:45.090 15.00
2016-06-14 14:18:38.960 10.00
From this I need to take hourly sales as below for the date range 12-June-16 to 18-June-16
From -To Sun-12 Mon-13 Tue-14 Wed-15 Thu-16 Fri-17 Sat-18
04:00-05:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00--nosale
05:00-06:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 "
06:00-07:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 "
07:00-08:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 "
08:00-09:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 "
09:00-10:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 "
10:00-11:00 30.00 0.00 0.00 0.00 0.00 0.00 0.00
11:00-12:00 0.00 5.00 0.00 0.00 0.00 0.00 0.00
12:00-13:00 50.00 15.00 0.00 0.00 0.00 0.00 0.00
13:00-14:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
14:00-15:00 0.00 0.00 10.00 0.00 0.00 0.00 0.00
..
..
..
23:00-00:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
..
..
03:00-04:00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
How can I achieve the same from above table. Please show some light on this. I am totally stuck on this.
Upvotes: 1
Views: 626
Reputation: 9318
Sales data does not have records for each hour in every day of period given. So to obtain required structure with all "trade day" (or whatever you call it) hours and each date of period given you'll have to provide list of time intervals and weekdays to your script from somewhere else (since Sales stores only some data).
So you'll need list of time intervals and list of dates (weekdays).
After that you'll be able to make LEFT JOIN
and get data from Sales or NULL if no appropriate data exists.
Your approach is based on simple GROUP BY
- that's why you can not get any other rows except those you have in dbo.Sales
.
And if I understood your request right - you are building something like "weekly report" and my solution is based on this assumption (that you'll always need 7 days only).
Now, to produce required output you need dates from rows to become columns. This can be accomplished by PIVOT
(or SUM(CASE WHEN ...)
trick).
Without pivoting output of time+date left join sales
would look like:
[From-To] [Date] [Amt]
10:00-11:00 12.06 (Sun) 11
10:00-11:00 13.06 (Mon) 5
After PIVOT
you will have dates (weekdays) as columns but many rows for same time period - from different days:
[From-To] [Sun] [Mon]
10:00-11:00 11 0
10:00-11:00 0 5
So you'll have to aggregate it
[From-To] [Sun] [Mon]
10:00-11:00 11 5
which can be done by simple GROUP BY
Recursive CTE is used here to populate @Timing
and @WeekDays
tables with sequential data (note +1
and dateadd
there).
One of the easiest and often suggested ways to work with such data as lists of periods of time or calendar dates is to populate persistent Calendar
table with everything you need. Then - just select from it to get all days of period given.
Another side note: your trade day starts not at 0:00 so you'll probably need to understand which of 4:00 belongs to this astronomy day and which - to the next. At list you'll need to get those rows in right order. See hrs
column in code below which stores hours from 4 to 27.
If my assumption of "weekly report" is wrong and you need a report with dynamic number of columns then the only way to get it is to use dynamic sql.
declare
@DateBegin datetime = '20160612',
@DateEnd datetime = '20160615'
declare @Sales table
(
StartDate datetime,
OrigionalSubTotal decimal(10, 2)
)
insert into @Sales(StartDate, OrigionalSubTotal)
values
('2016-06-12 10:01:15.780', 10.00),
('2016-06-12 10:15:57.360', 20.00),
('2016-06-12 12:48:41.250', 50.00),
('2016-06-13 11:02:50.850', 5.00),
('2016-06-13 12:04:45.090', 15.00),
('2016-06-14 14:18:38.960', 10.00)
/* DEBUG: * /
select * from @Sales
/ * :DEBUG */
declare @Timing table
(
hrs smallint,
hours_start smallint,
hours_end smallint,
period varchar(11)
)
;with cteTiming as
(
select 4 as hrs
union all
select t.hrs+1
from cteTiming t
where t.hrs < 27
)
insert into @Timing (hrs, hours_start, hours_end, period)
select t.hrs, hrs % 24, (hrs + 1) % 24,
cast(hrs % 24 as varchar(10)) + ':00-' + cast((hrs+1) % 24 as varchar(10)) + ':00'
from cteTiming t
/* DEBUG: * /
select * from @Timing
/ * :DEBUG */
declare @WeekDays table
(
wd smallint,
wd_name varchar(3),
wd_date date
)
;with cteWeekDays as
(
select
1 wd,
dateadd(day, 1-datepart(weekday, @DateBegin), cast(@DateBegin as date)) wd_date
union all
select
wd.wd+1,
dateadd(day, 1, wd.wd_date) wd_date
from cteWeekDays wd
where wd.wd < 7
)
insert into @WeekDays (wd, wd_name, wd_date)
select wd.wd, left(datename(weekday, wd.wd_date), 3), wd.wd_date
from cteWeekDays wd
/* DEBUG: * /
select * from @WeekDays
/ * :DEBUG */
;with cteSales as
(
select
datepart(weekday, s.StartDate) as SalesWeekDay,
datediff(hour, cast(s.StartDate as date), s.StartDate) DayTime,
cast(s.StartDate as date) SalesDate,
s.OrigionalSubTotal
from @Sales s
where s.StartDate >= @DateBegin and s.StartDate < @DateEnd
),
cteSalesPerWeekDays as
(
select
p.hrs,
p.period,
p.[Mon], p.[Tue], p.[Wed], p.[Thu], p.[Fri], p.[Sat], p.[Sun]
from @Timing t
cross join @WeekDays w
left join cteSales s on s.DayTime >= t.hours_start and s.DayTime < t.hours_end
and s.SalesDate = w.wd_date
pivot
(
Sum(s.OrigionalSubTotal)
for w.wd_name in ([Mon], [Tue], [Wed], [Thu], [Fri], [Sat], [Sun])
) p
)
select
spd.hrs,
spd.period as [From-To],
Sum(IsNull(spd.[Sun], 0)) as [Sun],
Sum(IsNull(spd.[Mon], 0)) as [Mon],
Sum(IsNull(spd.[Tue], 0)) as [Tue],
Sum(IsNull(spd.[Wed], 0)) as [Wed],
Sum(IsNull(spd.[Thu], 0)) as [Thu],
Sum(IsNull(spd.[Fri], 0)) as [Fri],
Sum(IsNull(spd.[Sat], 0)) as [Sat]
from cteSalesPerWeekDays spd
group by spd.hrs, spd.period
order by spd.hrs
To move from sample @Sales
table to real source you'll need to replace it in first cte of final select with this (part of your script):
;with cteSales as
(
select
datepart(weekday, s.StartDate) as SalesWeekDay,
datediff(hour, cast(s.StartDate as date), s.StartDate) DayTime,
cast(s.StartDate as date) SalesDate,
CONVERT(DECIMAL(10,2),sum(OrigionalSubTotal)/100.0) as OrigionalSubTotal
from dbo.Sales s
where s.StartDate >= @DateBegin and s.StartDate < @DateEnd
and s.IsSuspend = 0 and s.IsTrainMode = 0
and s.IsCancelled = 0 and s.wasrefunded=0
)
...
But @WeekDays and @Timing will stay! You need it to produce report structure needed.
Note, s.StartDate
contains time so simple date filter like between
won't work in some cases (for upper date). Suggested way is to filter >=
for start date and <
next date after given period upper bound with zero time. This way you'll find all '23:59:59' for sure.
And note that I have changed the order of weekdays in pivot. Actually, after the PIVOT
there is no difference of column order - it should be managed by client-side app. Or by the final select statement as in example above. All other code is aware of whether your week starts with Sunday or Monday.
And fixed date filter for 00:00-04:00 period of next astronomy day. I added some rows to demonstrate it.
if object_id('tempdb..#Sales', 'U') is not NULL
exec('drop table #Sales')
GO
if object_id('tempdb..#Timing', 'U') is not NULL
exec('drop table #Timing')
GO
if object_id('tempdb..#WeekDays', 'U') is not NULL
exec('drop table #WeekDays')
GO
declare
@DateBegin datetime = '20160612',
@DateEnd datetime = '20160715',
@TradeDateEnd datetime
/* to enable "<" filter */
set @TradeDateEnd = dateadd(hour, 4, dateadd(day, 1, @DateEnd))
declare
@sql nvarchar(max),
@col_per_day_list nvarchar(max),
@pivot_val_per_day_list nvarchar(max),
@sum_cols_per_day_list nvarchar(max)
create table #Sales
(
StartDate datetime,
OrigionalSubTotal decimal(10, 2)
)
insert into #Sales(StartDate, OrigionalSubTotal)
values
('20160612 03:55:00.000', 77.00),
('20160612 10:01:15.780', 10.00),
('20160612 10:15:57.360', 20.00),
('20160612 12:48:41.250', 50.00),
('20160613 11:02:50.850', 5.00),
('20160613 12:04:45.090', 15.00),
('20160614 14:18:38.960', 10.00),
('20160715 17:22:00.000', 11.00),
('20160716 03:55:00.000', 99.00)
/* DEBUG: * /
select * from #Sales
/ * :DEBUG */
create table #Timing
(
hrs smallint,
hours_start smallint,
hours_end smallint,
period varchar(11)
)
;with cteTiming as
(
select 4 as hrs
union all
select t.hrs+1
from cteTiming t
where t.hrs < 27
)
insert into #Timing (hrs, hours_start, hours_end, period)
select t.hrs, hrs % 24, (hrs + 1) % 24,
cast(hrs % 24 as varchar(10)) + ':00-' + cast((hrs+1) % 24 as varchar(10)) + ':00'
from cteTiming t
/* DEBUG: * /
select * from #Timing
/ * :DEBUG */
create table #WeekDays
(
wd smallint,
wd_date datetime,
wd_name varchar(12)
)
;with cteWeekDays as
(
select
datepart(weekday, @DateBegin) wd,
cast(@DateBegin as date) wd_date
union all
select
wd.wd+1,
dateadd(day, 1, wd.wd_date) wd_date
from cteWeekDays wd
where wd_date < cast(@DateEnd as date)
)
insert into #WeekDays (wd, wd_date, wd_name)
select wd.wd, wd.wd_date,
left(datename(weekday, wd.wd_date), 3) + '-' + convert(varchar(8), wd.wd_date, 112)
from cteWeekDays wd
/* DEBUG: * /
select * from #WeekDays
/ * :DEBUG */
select
@sum_cols_per_day_list = stuff(wd.value('.', 'varchar(max)'), 1, 2, '')
from
(
select
',
Sum(IsNull(r.' + quotename(wd.wd_name) + ', 0)) as ' + quotename(wd.wd_name)
from #WeekDays wd
order by wd.wd_date
for xml path(''), type
) x(wd)
select
@col_per_day_list = stuff(wd.value('.', 'varchar(max)'), 1, 2, '')
from
(
select
',
p.' + quotename(wd.wd_name)
from #WeekDays wd
order by wd.wd_date
for xml path(''), type
) x(wd)
set @pivot_val_per_day_list = replace(@col_per_day_list, 'p.[', ' [')
/* DEBUG: * /
print @col_per_day_list
print @pivot_val_per_day_list
print @sum_cols_per_day_list
/ * :DEBUG */
set @sql = cast(N'
;with cteSales as
(
select
datepart(weekday, s.StartDate) as SalesWeekDay,
datediff(hour, cast(s.StartDate as date), s.StartDate) DayTime,
s.StartDate SalesDate,
s.OrigionalSubTotal
from #Sales s
where s.StartDate >= @DateBegin and s.StartDate < @DateEnd
),
cteSalesPerWeekDays as
(
select
p.hrs,
p.period,' as nvarchar(max)) + @col_per_day_list + N'
from #Timing t
cross join #WeekDays w
left join cteSales s on datediff(hour, w.wd_date, s.SalesDate) = t.hrs
pivot
(
Sum(s.OrigionalSubTotal)
for w.wd_name in (' + @pivot_val_per_day_list + N')
) p
)
select
r.hrs,
r.period as [From-To],' + @sum_cols_per_day_list + N'
from cteSalesPerWeekDays r
group by r.hrs, r.period
order by r.hrs
'
/* DEBUG: */
print left(@sql, 4000)
print substring(@sql, 4000, 4000)
print datalength(@sql) / 2
/* :DEBUG */
exec sp_executesql @sql, N'@DateBegin datetime, @DateEnd datetime',
@DateBegin = @DateBegin,
@DateEnd = @TradeDateEnd
GO
if object_id('tempdb..#Sales', 'U') is not NULL
exec('drop table #Sales')
GO
if object_id('tempdb..#Timing', 'U') is not NULL
exec('drop table #Timing')
GO
if object_id('tempdb..#WeekDays', 'U') is not NULL
exec('drop table #WeekDays')
GO
Solution is almost the same except you need to collect column list before building final select and @
tables can't be used because their scope is limited to current batch.
FOR XML
stuff guaranties right order of column list generated.
For dynamic number of days you will also need month and year in column names (for example if one wants to build report from last days of December till January of next year).
@DateBegin
given@TradeDateEnd
to keep @DateEnd
unchangedreverted
Upvotes: 1
Reputation: 17655
Unpivot might be what you are looking for. For Example
declare @s table(FromTo varchar(11),Sun12 decimal(10,2), Mon13 decimal(10,2), Tue14 decimal(10,2), Wed15 decimal(10,2) ,Thu16 decimal(10,2), Fri17 decimal(10,2), Sat18 decimal(10,2))
insert into @s
values
('04:00-05:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('05:00-06:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('06:00-07:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('07:00-08:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('08:00-09:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('09:00-10:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('10:00-11:00' , 30.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('11:00-12:00' , 0.00 , 5.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('12:00-13:00' , 50.00 , 15.00, 0.00, 0.00, 0.00, 0.00, 0.00),
('13:00-14:00' , 0.00 , 0.00 , 0.00, 0.00, 0.00, 0.00, 0.00),
('14:00-15:00' , 0.00 , 0.00 , 10.00, 0.00, 0.00, 0.00, 0.00)
;WITH CTE AS
(
select UPVT.fromto,UPVT.DD,UPVT.sales
from
( SELECT FROMTO,Sun12, Mon13, Tue14, Wed15 ,Thu16, Fri17, Sat18 FROM @S) P
unpivot (sales for DD in (Sun12, Mon13, Tue14, Wed15 ,Thu16, Fri17, Sat18)
) UPVT
)
SELECT DD,CTE.FROMTO, SUM(CTE.SALES) SALES
FROM CTE
GROUP BY DD,CTE.FROMTO HAVING SUM(CTE.SALES) > 0
order by cast(right(DD,2) as int),CTE.FROMTO
Upvotes: 0