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

SQL query to get a sum for each day between two dates

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

这是 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

主要困难是您想要为 table 中没有数据的日期创建行。所以你必须找到一种方法来生成这些具有零值的行。

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

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:

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 的列,除非使用带引号的标识符(我从不这样做) ).我用 table 名字 test.

而不是你的 MyTable

我能想到的解决办法是:

选择table存储过程来完成所有工作

已经显示在

使用递归通用 table 表达式生成日期

这个解决方案与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

为日期范围

选择table存储过程
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 ;#

此 selectable 存储过程生成从 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. 需要在 select 列表中将名称显式指定为 'Chris' as name 限制了灵活性(例如,您不能直接使用此解决方案来获取 Chris 和 John 的列表作为单个查询结果)
  2. MyTable 中重复出现相同的名字表明您需要维护一个单独的 table 人(这也将简化求解 1)
  3. 没有 'rewards' 的日期很重要,这似乎表明您可能需要维护 table 个日期;这也将考虑到差距(例如,如果周末或假期应该被排除在外)。这样做有其缺点(例如,必须填充和维护日期,可能有自己的维护开销)
  4. Chris 在同一天获得多项奖励这一事实可能表明奖励本身也应该是 table(但前提是这是重要信息),或者 MyTable 需要更多信息为什么或什么被奖励。
  5. 您注册 Chris 在一次约会中没有得到奖励,但在其他日期却没有,这一事实表明,也许您应该只注册某些东西得到了奖励,而不是在没有奖励的时候。这消除了对 reward 列的需要。或者,如果 Chris 在 5 月 7 日没有得到奖励这一事实很重要,这可能意味着您需要额外的列来说明原因。

例如,替代设计可能类似于:

带一个tableperson

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 之间的日期(提示:使用上面创建的 daterange 过程的 insert into .. select ..)。

然后您可以将 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 table 之外的日期插入奖励。在那种情况下,relevantdate table 仅用作报告目的的支持对象。

作为 select,您现在可以使用类似的东西:

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 [=99 的情况下=],您可以考虑完全关闭 id 列。