SQL查询以获取两个日期之间每天的总和

时间:2018-05-17 18:05:20

标签: sql firebird firebird2.5

我想查看两个日期之间特定名称的奖励总和

这是MyTable

|   NAME    |  REWARD   |    DATE      |
+-----------+-----------+--------------+
|   Chris   |    yes    |  05.05.2018  |
|   Chris   |    yes    |  05.05.2018  |
|   Chris   |    no     |  07.05.2018  |
|   John    |    yes    |  10.05.2018  |

假设我要找的名字是“Chris”,日期是04.05.2018 - 08.05.2018。查询还应计算每天的REWARD =“yes”字段,并在没有获得奖励的日子中添加金额值“0”。

然后这应该是结果:

|   NAME    |  AMOUNT   |    DATE      |
+-----------+-----------+--------------+
|   Chris   |    0      |  04.05.2018  |
|   Chris   |    2      |  05.05.2018  |
|   Chris   |    0      |  06.05.2018  |
|   Chris   |    0      |  07.05.2018  |
|   Chris   |    0      |  08.05.2018  |

我正在使用Firebird 2.5

我尝试了这个查询,但是这样做的时候没有生成“0”金额的缺失日期

SELECT name, SUM(CASE WHEN reward='yes' THEN 1 ELSE 0 END) AS AMOUNT, DATE
  from MyTable 
  WHERE DATE between '04.05.2018' and '08.05.2018'  
    AND NAME='Chris' 
  GROUP BY NAME, DATE

2 个答案:

答案 0 :(得分:2)

主要困难在于您希望拥有表中没有数据的日期行。所以你必须找到一种方法来生成零值的这些行。

我认为最简单,最容易理解的解决方案是可选择的存储过程,即

CREATE PROCEDURE damounts(d1 date, d2 date, name varchar(20)) 
RETURNS (d date, amount integer)
AS 
BEGIN
  d = d1;
  while(d <= d2)do begin
     amount = (select sum(case when Reward = 'yes' then 1 else 0 end) from test where d = :d and name = :name);
     if (amount is null) then amount = 0;
     suspend;
     d = d + 1;
  end
END

使用它只需从中选择:

select * from damounts('2018-05-04', '2018-05-10', 'Chris')

如果你想没有SP,那么Firebird 2.5支持递归CTE,它可用于生成给定范围的所有日期。使用另一个CTE计算有数据的日期的总和,然后按日期加入它们:

WITH RECURSIVE dates AS (
  select cast('2018-05-04' as date) d from rdb$database
  UNION ALL
  select d+1 from dates where d < '2018-05-10'
)
,
sums (d, dsum) AS (
  select
    d,
    sum(case when Reward = 'yes' then 1 else 0 end) AS amount
  from test
  where name = 'Chris' and d >= '2018-05-04' and d <= '2018-05-10'
  group by d
)
select
  'Chris' as name,
  d.d as "date",
  coalesce(s.dsum, 0) as amount
from dates d
left join sums s on(s.d = d.d);

请注意,在示例中,我使用了列名d而不是date,因为除非使用带引号的标识符(我从不这样做),否则Firebird中的列不能有date列)。而不是MyTable我使用了表名test

答案 1 :(得分:2)

我能想到的解决方案是:

可执行所有工作的可选存储过程

已在answer by ain

中显示

使用递归公用表表达式生成日期

此解决方案类似于ain提供的解决方案,但仅使用一个CTE,并使用count代替sum

with recursive dates as (
  select date'2018-05-04' as rewarddate 
  from rdb$database
  union all
  select rewarddate + 1 
  from dates 
  where rewarddate < date'2018-05-08'
)
select 
  'Chris' as name, 
  d.rewarddate, 
  count(case when g.reward = 'yes' then 1 end) as amount
from dates d 
left join MyTable g 
  on d.rewarddate = g."DATE" and g.name = 'Chris'
group by d.rewarddate

日期范围的可选存储过程

set term #;
recreate procedure daterange(startdate date, enddate date) 
    returns (dateval date)
as
begin
  dateval = startdate;
  while (dateval <= enddate) do
  begin
    -- output row
    suspend;
    dateval = dateval + 1;
  end
end#
set term ;#

此可选存储过程会生成从startdateenddate(包括)的日期范围。

然后我们可以用与CTE解决方案类似的方式使用它:

select 
  'Chris' as name, 
  r.dateval, 
  count(case when g.reward = 'yes' then 1 end) as amount
from daterange(date'2018-05-04', date'2018-05-08') r
left join MyTable g 
  on r.dateval = g."DATE" and g.name = 'Chris'
group by r.dateval

重新考虑您的数据库设计

我在当前设计中看到的许多(潜在)问题

  1. 需要在选择列表中明确指定名称为'Chris' as name,这限制了灵活性(例如,您无法直接使用此解决方案获取Chris和John作为单个查询结果的列表)
  2. MyTable中重复出现相同名称表明您需要维护一个单独的人员表(这也可以简化解决方案1)。
  3. 没有'奖励'的日期很重要,这似乎表明你可能需要维护一个日期表;这也可以弥补差距(例如,如果应该排除周末或假期)。这样做有其缺点(例如,必须填充和维护日期,可能有自己的维护开销)
  4. Chris在同一天获得多项奖励的事实可能表明奖励本身也应该是一张表(但只有在这是重要信息的情况下),或者MyTable需要额外的信息为什么或什么是奖励。
  5. 你注册的事实是克里斯没有在一个日期获得奖励,但没有在其他日期获奖,这表明也许你应该只注册一些奖励,而不是奖励。这样就无需拥有reward列。或者,如果克里斯在5月7日没有获得奖励的事实很重要,那么这可能意味着你需要额外的专栏来解释原因。
  6. 例如,替代设计可能类似于:

    使用表格person

    CREATE TABLE person (
       id integer generated by default as identity constraint pk_person primary key,
       name varchar(50) not null -- may need a unique constraint as well
    );
    

    填充为:

    id  name
    1   Chris
    2   John
    

    relevantdate(由于缺乏背景,我无法提出更好的名称)

    create table relevantdate (
       dateval date constraint pk_relevantdate primary key
    );
    

    在2018-05-04和2018-05-12之间填充日期(提示:使用上面创建的insert into .. select ..程序使用daterange。)

    然后,您可以将MyTable(此处重命名为reward)的设计更改为:

    create table reward (
      id integer generated by default as identity constraint pk_reward primary key,
      personid integer not null constraint fk_reward_person references person(id),
      rewarddate date not null constraint fk_reward_relevantdate references relevantdate(dateval)
      -- maybe add some more columns with information on why/what
    )
    

    填充为(因为它不相关而留下id):

    personid  rewarddate
    1         2018-05-05
    1         2018-05-05
    2         2018-05-10
    

    为了获得更大的灵活性,值得考虑不定义外键fk_reward_relevantdate。这将允许在不在relevantdate表中的日期插入奖励。在这种情况下,relevantdate表仅用作报告目的的支持对象。

    作为选择,您现在可以使用以下内容:

    select
      p.name,
      rd.dateval,
      count(r.rewarddate)
    from person p
    cross join relevantdate rd
    left join reward r
      on p.id = r.personid and rd.dateval = r.rewarddate
    where rd.dateval between date'2018-05-04' and date'2018-05-08'
    and p.name = 'Chris'
    group by rd.dateval, p.name
    

    退出p.name = 'Chris'条件,现在您可以获得Chris和John的信息。

    注意:我使用了generated by default as identity,这是Firebird 3的一项功能。这个例子并不是必需的。 Firebird 2.5及更早版本中的等价物需要序列+触发器来生成id,但在这些示例中,您可以简单地省略整个generated by default as identity,对于reward表,您可以可以考虑完全取消id列。