在 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_mileage
和 bhds_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
系统变量。就我个人而言,我会避免使用YEAR
、WEEK
和CONCAT
函数,而是使用更简单的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 问题的表的主要内容,如果未能解决你的问题,请参考以下文章