在 MYSQL 中加入具有 SUM 问题的表

Posted

技术标签:

【中文标题】在 MYSQL 中加入具有 SUM 问题的表【英文标题】:Join tables with SUM issue in MYSQL 【发布时间】:2016-10-25 00:20:14 【问题描述】:

我一直无法在连接表上获取 SUM,总是有一个问题,我可以通过运行两个查询得到我需要的结果,我想知道这两个查询是否可以组合成一个连接查询,这里是我的查询以及我加入查询的尝试

查询 1

SELECT last_name, first_name, DATE_FORMAT( (mil_date),  '%m/%d/%y' ) AS dates, 
SUM( drive_time ) MINUTES FROM bhds_mileage LEFT JOIN bhds_teachers i 
ON i.ds_id = bhds_mileage.ds_id 
WHERE mil_date BETWEEN  '2016-04-11' AND  '2016-04-30'
AND bhds_mileage.ds_id =5
GROUP BY CONCAT( YEAR( mil_date ) ,  '/', WEEK( mil_date ) ) ,    
bhds_mileage.ds_id
ORDER BY last_name ASC , dates ASC 

以分钟为单位的输出是 271, 281, 279

查询 2

SELECT last_name, first_name, DATE_FORMAT((tm_date), '%m/%d/%y') AS dates,   
SUM(tm_hours) total FROM bhds_timecard LEFT JOIN bhds_teachers i 
ON i.ds_id = bhds_timecard.ds_id 
WHERE tm_date BETWEEN '2016-04-11' AND '2016-04-30' AND bhds_timecard.ds_id = 5
GROUP BY CONCAT(YEAR(tm_date), '/', WEEK(tm_date)), bhds_timecard.ds_id 
ORDER BY last_name ASC, dates ASC

这里的输出是 33.00, 36.00, 26.75

现在我尝试加入查询

SELECT last_name, first_name, DATE_FORMAT((tm_date), '%m/%d/%y') AS dates,  
SUM(tm_hours) total,  SUM( drive_time ) MINUTES FROM bhds_timecard 
LEFT JOIN bhds_teachers i ON i.ds_id = bhds_timecard.ds_id 
LEFT JOIN bhds_mileage ON DATE_FORMAT((bhds_timecard.tm_date), '%m/%d/%y') = 
DATE_FORMAT((bhds_mileage.mil_date), '%m/%d/%y') AND bhds_timecard.ds_id = bhds_mileage.ds_id
WHERE tm_date BETWEEN '2016-04-11' AND '2016-04-30' AND bhds_timecard.ds_id = 5
GROUP BY CONCAT(YEAR(tm_date), '/', WEEK(tm_date)), bhds_timecard.ds_id 

括号是预期的

这输出 1044 (271), 1086 (281), 1215 (279)

【问题讨论】:

试试:select ... from (<query1>) q1 inner join (<query2>) q2 on ...order by .... 这能回答你的问题吗? Two SQL LEFT JOINS produce incorrect result 【参考方案1】:

有几个问题...bhds_mileagebhds_timecard 之间的部分笛卡尔积(叉积),因为一个表中的每个详细信息行(在一个组内)都将与来自一个表的详细信息行“交叉连接”另一张桌子。这发生在 GROUP BY 操作折叠行并计算 SUM 之前。这就解释了为什么您会看到“膨胀”的值。

解决方法是在内联视图中计算至少一个 SUM() 聚合...像您的第一个查询一样完成 SUM() / GROUP BY()。为清楚起见,您可以对两个原始查询执行相同的操作,然后连接内联视图的结果。

mysql 本身不支持 FULL 外连接。其中一张桌子需要是驾驶桌。例如,我们可以使用_timecard 作为驾驶表,但这意味着我们必须从_timecard 返回给定周的一行,以便从_mileage 返回相应的行。也就是说,如果_timecard 中没有一行,我们就无法从_mileage 中获取一行。

我们注意到bhds_teacher 的连接是一个外连接。如果我们在_mileage_timecard 中都有ds_id 之间的外键约束,引用_teacher,那么这不一定需要是外连接,我们可以使用内连接,并使用@987654332 @ 作为两个外连接的驱动表。

另一个问题是 SELECT 列表中的非聚合...例如DATE_FORMAT((tm_date), '%m/%d/%y')

GROUP BY 是按年和周计算的,因此 DATE_FORMAT 的值是不确定的……它可能来自组内的 any tm_date。无法保证您会得到一周的第一天、一周内最早的日期等等。

另外,WEEK 函数的第二个参数被省略,因此默认为default_week_format 系统变量。就我个人而言,我会避免使用YEARWEEKCONCAT 函数,而是使用更简单的DATE_FORMAT,使用明确包含星期模式参数的日期格式字符串。

如果你想在“周”加入,那么加入谓词应该在“周”值上,而不是一周内的一个不确定的日期。

(可能有一些我们不知道的数据的特定限制...如果 _mileage 中有给定一周的行,在星期一,那么我们保证在同一个星期一有一个 _timecard . 在更一般的情况下,我们不会有这样的保证。)

即使我们有这样的保证,我们也不能保证 SELECT 列表中的非聚合不会返回星期二 _timecard 和星期四 _mileage 中的日期...(除非有某种保证数据将仅包含 _timecard 和 _mileage 上带有“星期一”日期的行)。否则,非聚合表达式就不是连接谓词的可靠表达式。

假设ds_id_teacher 上是唯一的,并且由来自_mileage_timecard 的外键ds_id 引用,则如下所示:

SELECT i.last_name
     , i.first_name
     , tm.dates
     , tm.total_hours
     , mm.total_minutes
  FROM bhds_teacher i 
  LEFT
  JOIN ( SELECT t.ds_id
              , DATE_FORMAT( t.tm_date,'%Y/%U')          AS week_
              , DATE_FORMAT( MIN(t.tm_date) ,'%m/%d/%y') AS dates
              , SUM(t.tm_hours)                          AS total_hours
           FROM bhds_timecard t
          WHERE t.tm_date BETWEEN '2016-04-11' AND '2016-04-30'   -- <
            AND t.ds_id = 5                                       -- <
          GROUP
             BY t.ds_id
              , DATE_FORMAT( t.tm_date,'%Y/%U')                   -- week
       ) tm
    ON tm.ds_id = i.ds_id
  LEFT
  JOIN ( SELECT m.ds_id
              , DATE_FORMAT( m.mil_date,'%Y/%U')           AS week_
              , DATE_FORMAT( MIN(m.mil_date), '%m/%d/%y' ) AS dates
              , SUM( m.drive_time )                        AS total_minutes 
           FROM bhds_mileage m
          WHERE m.mil_date BETWEEN '2016-04-11' AND '2016-04-30'  -- <
            AND m.ds_id = 5                                       -- <
          GROUP
             BY m.ds_id
              , DATE_FORMAT( m.mil_date,'%Y/%U')                  -- week
       ) mm
    ON mm.ds_id = i.ds_id
   AND mm.week_ = tm.week_
 WHERE i.ds_id = 5                                                -- <
 ORDER
    BY i.last_name ASC, tm.dates ASC

【讨论】:

【参考方案2】:

当您在主查询中使用多个连接时,您最终会得到所有表的叉积,因此总和会乘以另一个表中匹配的行数。您需要将总和移动到子查询中。

SELECT last_name, first_name, DATE_FORMAT(LEAST(mil_date, tm_date),  '%m/%d/%y' ) AS dates, 
        total, minutes
FROM bhds_teachers AS i
LEFT JOIN (
    SELECT ds_id, YEARWEEK(mil_date) AS week, MIN(mil_date) AS mil_date, SUM(drive_time) AS minutes
    FROM bhds_mileage
    WHERE mil_date BETWEEN '2016-04-11' AND  '2016-04-30'
    AND bhds_mileage.ds_id = 5
    GROUP BY ds_id, week) AS m 
ON m.ds_id = i.ds_id
LEFT JOIN (
    SELECT ds_id, YEARWEEK(tm_date) AS week, MIN(tm_date) AS tm_date, SUM(tm_hours) AS total
    WHERE tm_date BETWEEN '2016-04-11' AND '2016-04-30' AND bhds_timecard.ds_id = 5
    GROUP BY ds_id, week) AS t 
ON t.ds_id = i.ds_id AND t.week = m.week

【讨论】:

以上是关于在 MYSQL 中加入具有 SUM 问题的表的主要内容,如果未能解决你的问题,请参考以下文章

如何在 MySQL/MariaDB 中加入两个巨大的表?

我想在大查询中加入两个具有公共列的表?

从 MySQL 中的表中加入单行

在 MySQL 中加入表的转置

在实体框架中加入 3 个一对多表

在 MySQL 中加入“博客”表和“评论”表