【问题标题】:Select each month even though the month not exist in mysql table选择每个月,即使月份在 mysql 表中不存在
【发布时间】:2016-06-09 10:21:00
【问题描述】:

假设我在 mysql 中有这两个表。

表1:

date         staff_no
2016-06-10   1
2016-06-09   1
2016-05-09   1
2016-04-09   1

表2:

staff_no    name
1           David

然后,我有这个查询来获取每个月的员工分析:

SELECT DATE_FORMAT(table1.date,'%b %Y') as month,COUNT(table1.date) as total_records,table2.name as name
FROM table1 as table1 
LEFT JOIN table2 as table2 on table2.staff_no = table1.staff_no
WHERE table1.staff_no = "1" and date(table1.date) between = "2016-04-01" and "2016-06-30" 
GROUP BY table2.name,DATE_FORMAT(table1.date,'%Y-%m')
ORDER BY DATE_FORMAT(table1.date,'%Y-%m-%d')

此查询将输出:

month      total_records  name
Apr 2016               1  David
May 2016               1  David
Jun 2016               2  David

但是,如果我从查询中替换“2016-04-01”和“2016-07-31”之间的日期,它不会显示 7 月的记录,因为它在 table1 中不存在,这不是我想要的.我仍然想得到这样的结果:

month      total_records  name
Apr 2016               1  David
May 2016               1  David
Jun 2016               2  David
Jul 2016               0  David   

有这方面的专家吗?请帮我解决这个问题。谢谢!

【问题讨论】:

  • 如果你的数据库中有一个日期表会有所帮助 - 你能做到吗?
  • 数据库中有日期表是什么意思?我的示例中的表格有日期列。
  • 有但不是所有日期。日期表通常用作数据库中的辅助表,以在没有发生实际事件但您想显示没有发生事件的情况下提供帮助,并且会包括很多年的每个日期日(通常包括月、周。季度和财政日期)

标签: mysql


【解决方案1】:

考虑以下架构,第三个表是提到的年/月帮助表。辅助表非常常见,可以自然地在整个代码中重复使用。我会留给你用大量的日期数据加载它。但是请注意,我们这些想要做更少工作的人将每个月的结束日期放在一起,同时允许数据库引擎为我们计算闰年。

您可以在该帮助表中只有一列。但这需要在您的某些函数中使用函数调用结束日期,这意味着更慢。我们喜欢快。

架构

create table workerRecords
(   id int auto_increment primary key,
    the_date date not null,
    staff_no int not null
);
-- truncate workerRecords;
insert workerRecords(the_date,staff_no) values
('2016-06-10',1),
('2016-06-09',1),
('2016-05-09',1),
('2016-04-09',1),
('2016-03-02',2),
('2016-07-02',2);

create table workers
(   staff_no int primary key,
    full_name varchar(100) not null
);
-- truncate workers;
insert workers(staff_no,full_name) values
(1,'David Higgins'),(2,"Sally O'Riordan");

下面的帮助表

create table ymHelper
(   -- Year Month helper table. Used for left joins to pick up all dates.
    -- PK is programmer's choice.
    dtBegin date primary key,   -- by definition not null
    dtEnd date null
);
-- truncate ymHelper;
insert ymHelper (dtBegin,dtEnd) values
('2015-01-01',null),('2015-02-01',null),('2015-03-01',null),('2015-04-01',null),('2015-05-01',null),('2015-06-01',null),('2015-07-01',null),('2015-08-01',null),('2015-09-01',null),('2015-10-01',null),('2015-11-01',null),('2015-12-01',null),
('2016-01-01',null),('2016-02-01',null),('2016-03-01',null),('2016-04-01',null),('2016-05-01',null),('2016-06-01',null),('2016-07-01',null),('2016-08-01',null),('2016-09-01',null),('2016-10-01',null),('2016-11-01',null),('2016-12-01',null),
('2017-01-01',null),('2017-02-01',null),('2017-03-01',null),('2017-04-01',null),('2017-05-01',null),('2017-06-01',null),('2017-07-01',null),('2017-08-01',null),('2017-09-01',null),('2017-10-01',null),('2017-11-01',null),('2017-12-01',null),
('2018-01-01',null),('2018-02-01',null),('2018-03-01',null),('2018-04-01',null),('2018-05-01',null),('2018-06-01',null),('2018-07-01',null),('2018-08-01',null),('2018-09-01',null),('2018-10-01',null),('2018-11-01',null),('2018-12-01',null),
('2019-01-01',null),('2019-02-01',null),('2019-03-01',null),('2019-04-01',null),('2019-05-01',null),('2019-06-01',null),('2019-07-01',null),('2019-08-01',null),('2019-09-01',null),('2019-10-01',null),('2019-11-01',null),('2019-12-01',null);
-- will leave as an exercise for you to add more years. Good idea to start, 10 in either direction, at least.
update ymHelper set dtEnd=LAST_DAY(dtBegin);    -- data patch. Confirmed leap years.
alter table ymHelper modify dtEnd date not null;    -- there, ugly patch above worked fine. Can forget it ever happened (until you add rows)
-- show create table ymHelper; -- this confirms that dtEnd is not null

所以这是一个辅助表。设置一次,几年后忘记它

注意:别忘了运行上面的update stmt

查询快速测试

SELECT DATE_FORMAT(ymH.dtBegin,'%b %Y') as month,
ifnull(COUNT(wr.the_date),0) as total_records,@soloName as full_name 
FROM ymHelper ymH 
left join workerRecords wr 
on wr.the_date between ymH.dtBegin and ymH.dtEnd 
and wr.staff_no = 1 and wr.the_date between '2016-04-01' and '2016-07-31' 
LEFT JOIN workers w on w.staff_no = wr.staff_no 
cross join (select @soloName:=full_name from workers where staff_no=1) xDerived 
WHERE ymH.dtBegin between '2016-04-01' and '2016-07-31' 
GROUP BY ymH.dtBegin 
order by ymH.dtBegin; 

+----------+---------------+---------------+
| month    | total_records | full_name     |
+----------+---------------+---------------+
| Apr 2016 |             1 | David Higgins |
| May 2016 |             1 | David Higgins |
| Jun 2016 |             2 | David Higgins |
| Jul 2016 |             0 | David Higgins |
+----------+---------------+---------------+

它工作正常。第一个 mysql 表是 Helper 表。用于引入工作人员记录的左连接(允许为空)。让我们在这里暂停一下。这就是您问题的重点:缺少数据。最后是交叉连接中的工作表。

cross join 是初始化一个变量(@soloName),它是工人的名字。虽然您请求的缺失日期的空状态可以通过返回 0 的 ifnull() 函数很好地提取,但我们没有为工人的名字提供的奢侈。这迫使cross join

交叉连接是笛卡尔积。但是由于它是单行,所以我们不会遇到笛卡尔导致结果集中的许多行的常见问题。无论如何,它有效。

但是这里有一个问题:可以看出,维护和插入 6 个位置的值太难了。所以考虑下面的存储过程。

存储过程

drop procedure if exists getOneWorkersRecCount;
DELIMITER $$
create procedure getOneWorkersRecCount
(pStaffNo int, pBeginDt date, pEndDt  date)
BEGIN
    SELECT DATE_FORMAT(ymH.dtBegin,'%b %Y') as month,ifnull(COUNT(wr.the_date),0) as total_records,@soloName as full_name
    FROM ymHelper ymH 
    left join workerRecords wr 
    on wr.the_date between ymH.dtBegin and ymH.dtEnd 
    and wr.staff_no = pStaffNo and wr.the_date between pBeginDt and pEndDt
    LEFT JOIN workers w on w.staff_no = wr.staff_no 
    cross join (select @soloName:=full_name from workers where staff_no=pStaffNo) xDerived
    WHERE ymH.dtBegin between pBeginDt and pEndDt 
    GROUP BY ymH.dtBegin
    order by ymH.dtBegin;
END$$
DELIMITER ;

多次测试存储过程

call getOneWorkersRecCount(1,'2016-04-01','2016-06-09');
call getOneWorkersRecCount(1,'2016-04-01','2016-06-10');
call getOneWorkersRecCount(1,'2016-04-01','2016-07-01');
call getOneWorkersRecCount(2,'2016-02-01','2016-11-01');

啊,使用起来更容易(在 PHP、c#、Java 中,随你怎么说)。选择是你的,存储过程与否。

奖励存储过程

drop procedure if exists getAllWorkersRecCount;
DELIMITER $$
create procedure getAllWorkersRecCount
(pBeginDt date, pEndDt  date)
BEGIN
    SELECT DATE_FORMAT(ymH.dtBegin,'%b %Y') as month,ifnull(COUNT(wr.the_date),0) as total_records,w.staff_no,w.full_name
    FROM ymHelper ymH 
    cross join workers w 
    left join workerRecords wr 
    on wr.the_date between ymH.dtBegin and ymH.dtEnd 
    and wr.staff_no = w.staff_no and wr.the_date between pBeginDt and pEndDt
    -- LEFT JOIN workers w on w.staff_no = wr.staff_no 
    -- cross join (select @soloName:=full_name from workers ) xDerived
    WHERE ymH.dtBegin between pBeginDt and pEndDt 
    GROUP BY ymH.dtBegin,w.staff_no,w.full_name
    order by ymH.dtBegin,w.staff_no;
END$$
DELIMITER ;

快速测试一下

call getAllWorkersRecCount('2016-03-01','2016-08-01');
+----------+---------------+----------+-----------------+
| month    | total_records | staff_no | full_name       |
+----------+---------------+----------+-----------------+
| Mar 2016 |             0 |        1 | David Higgins   |
| Mar 2016 |             1 |        2 | Sally O'Riordan |
| Apr 2016 |             1 |        1 | David Higgins   |
| Apr 2016 |             0 |        2 | Sally O'Riordan |
| May 2016 |             1 |        1 | David Higgins   |
| May 2016 |             0 |        2 | Sally O'Riordan |
| Jun 2016 |             2 |        1 | David Higgins   |
| Jun 2016 |             0 |        2 | Sally O'Riordan |
| Jul 2016 |             0 |        1 | David Higgins   |
| Jul 2016 |             1 |        2 | Sally O'Riordan |
| Aug 2016 |             0 |        1 | David Higgins   |
| Aug 2016 |             0 |        2 | Sally O'Riordan |
+----------+---------------+----------+-----------------+

外卖

Helper Tables 已经使用了几十年。不要害怕或尴尬地使用它们。事实上,有时尝试在没有他们的情况下完成一些专业工作几乎是不可能的。

【讨论】:

  • 除了这个详细的答案,我不能再接受了。认为没有日期助手表也可以工作,我感到很愚蠢。谢谢朋友。
【解决方案2】:

您可以通过使用系统中的任何其他表来构建一个内联变量集来表示您想要的所有日期,即使数据不必具有日期,该表至少具有您尝试表示的月数。只是有可以限制的记录。

尝试以下使用 MySql 变量的语句。 FROM 子句声明了一个内联到 SQL 语句“@Date1”的变量。我从 2016 年 3 月 1 日开始。现在,选择字段列表采用该变量并不断向其添加 1 个月。由于它与“AnyTableWithAtLeast12Records”(字面意思是系统中至少有 X 条记录的任何表)结合使用,它将创建一个显示日期的结果。这是强制使用日历类型列表的一种方式。

但请注意,此选择中的 SECOND 列不会通过 := 分配更改 @Date1。因此,它采用现在的日期,并在 END Date 中再增加一个月。如果您需要更小或更大的日期范围,只需更改记录限制以创建日历跨度...

select
     @Date1 := date_add( @Date1, interval 1 month ) StartDate,
      date_add( @Date1, interval 1 month ) EndDate
    from
      AnyTableWithAtLeast12Records,
      ( select @Date1 := '2016-03-01' ) sqlvars
   limit 12;

结果类似于...

StartDate   EndDate
2016-04-01  2016-05-01
2016-05-01  2016-06-01
2016-06-01  2016-07-01
2016-07-01  2016-08-01
2016-08-01  2016-09-01
2016-09-01  2016-10-01
2016-10-01  2016-11-01
2016-11-01  2016-12-01
2016-12-01  2017-01-01
2017-01-01  2017-02-01
2017-02-01  2017-03-01
2017-03-01  2017-04-01

现在您已经在一个简单的查询中完成了您的动态“日历”。现在,将其用作您需要计数和格式化的所有记录的基础。因此,将上面的整个查询作为一个 JOIN 来查找这些日期范围内的记录......不需要其他查询或存储过程。现在,一个简单的 LEFT JOIN 将保留所有日期,但仅在每个开始/结束之间的范围内显示有员工的日期。例如:大于或等于 04/01/2016,但小于 05/01/2016,其中包括 04/30/2016 @ 11:59:59pm。

SELECT 
      DATE_FORMAT(MyCalendar.StartDate,'%b %Y') as month,
      COALESCE(COUNT(T1.Staff_no),0) as total_records,
      COALESCE(T2.name,"") as name
   FROM 
      ( select @Date1 := date_add( @Date1, interval 1 month ) StartDate,
               date_add( @Date1, interval 1 month ) EndDate
           from
              AnyTableWithAtLeast12Records,
              ( select @Date1 := '2016-03-01' ) sqlvars
           limit 12 ) MyCalendar
        LEFT JOIN table1 T1
           ON T1.Date >= MyCalendar.StartDate
           AND T1.Date < MyCalendar.EndDate
           AND T1.Staff_No = 1
           LEFT JOIN table2 T2
              ON T1.staff_no = T2.StaffNo
   GROUP BY
      T2.name,
      DATE_FORMAT(MyCalendar.StartDate,'%Y-%m')
   ORDER BY 
      DATE_FORMAT(MyCalendar.StartDate,'%Y-%m-%d')

【讨论】:

    【解决方案3】:

    我会说你需要在这里有 RIGHT JOIN 才能包括第二张桌子的员工

    【讨论】:

    • 我希望我有如何做到这一点的图片。
    猜你喜欢
    • 2012-08-22
    • 1970-01-01
    • 2014-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-19
    • 1970-01-01
    相关资源
    最近更新 更多