以小时为单位显示日期范围内的总销售额

时间:2016-06-25 06:08:33

标签: sql sql-server sql-server-2008

我必须为日期范围生成销售报告,用户将在该日期范围内选择日 - 开始时和结束时。

以下查询将返回日期时间和销售额

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

由此我需要按照以下每小时的销售情况,在6月6日至16日至6月18日期间进行小时销售

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

如何从上表中获得相同的结果。请说明一下。我完全坚持这个。

2 个答案:

答案 0 :(得分:1)

准备报告结构

销售数据没有给出每个时段的每小时的记录。因此,为了获得所有“交易日”(或任何你称之为)的所需结构,小时和每个日期的日期,你必须从其他地方提供你的脚本的时间间隔和工作日列表(因为销售只存储一些数据) )。

因此,您需要时间间隔列表和日期列表(工作日)。 之后,如果没有合适的数据,您将能够生成LEFT JOIN并从Sales 获取数据或NULL。

您的方法基于简单的GROUP BY - 这就是为什么除dbo.Sales之外的其他行外,您无法获得任何其他行。

如果我理解你的请求是正确的 - 你正在构建类似“每周报告”的东西,我的解决方案基于这个假设(你总是需要7天)。

汇总数据

现在,要生成所需的输出,您需要从行开始的日期成为列。这可以通过PIVOT(或SUM(CASE WHEN ...)技巧)来完成。 没有转动time+date left join sales的输出看起来像:

[From-To]    [Date]       [Amt]
10:00-11:00   12.06 (Sun)  11
10:00-11:00   13.06 (Mon)  5

PIVOT之后,您会将日期(工作日)作为列,但在同一时间段内有很多行 - 来自不同日期:

[From-To]    [Sun] [Mon]
10:00-11:00    11     0
10:00-11:00     0     5

所以你必须聚合它

[From-To]    [Sun] [Mon]
10:00-11:00    11     5

可以通过简单的GROUP BY

完成

来源

这里使用递归CTE来为@Timing@WeekDays表填充顺序数据(注意+1dateadd)。

使用诸如时间段或日历日期列表等数据的最简单且经常建议的方法之一是使用您需要的所有内容填充持久Calendar表。然后 - 只需从中选择即可获得给定的所有日期。

另一方面注意:您的交易日不是在0:00开始,所以您可能需要了解4:00中哪一个属于这个天文日,哪个属于下一个天文日。在列表中,您需要按正确的顺序获取这些行。请参阅下面代码中的hrs列,其中包含4到27小时的小时数。

如果我对“每周报告”的假设是错误的,并且您需要一个包含动态列数的报告,那么获取它的唯一方法是使用动态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

enter image description here

部署

要从示例@Sales表转移到真实源,您需要在第一个cte的最终选择中替换它(脚本的一部分):

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

但@WeekDays和@Timing将留下来!您需要它来生成所需的报告结构。

注意,s.StartDate包含时间,因此between之类的简单日期过滤器在某些情况下不起作用(对于上限日期)。建议的方法是在开始日期过滤>=,并在给定期间上限零时间后过滤< 下一个日期。这样你就可以找到所有'23:59:59'。

请注意,我已更改了工作日中枢轴的顺序。实际上,在PIVOT之后,列顺序没有区别 - 它应该由客户端应用程序管理。或者通过上面示例中的最终选择语句。所有其他代码都知道您的一周是从星期日还是星期一开始。

UPD:动态天数

固定日期过滤器为下一个天文日00:00-04:00期间。我添加了一些行来演示它。

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

解决方案几乎相同,除非您需要在构建最终选择之前收集列列表,并且不能使用@表,因为它们的范围仅限于当前批次。

FOR XML东西保证生成列列表的正确顺序。

enter image description here

对于动态天数,您还需要列名称中的月份和年份(例如,如果想要建立从12月的最后几天到明年1月的报告)。

UPD2

  • 修复cteWeekDays以避免始终从一周的第一天开始,并从@DateBegin给出
  • 介绍@TradeDateEnd以保持@DateEnd不变

upd3

  • 来自Sales in output
  • 的附加列

还原

答案 1 :(得分:0)

Unpivot可能就是你想要的。例如

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