MySQL多表JOIN:轻松计算用户佣金与每日出勤统计
2025-05-06 08:05:16
MySQL多表JOIN:巧妙计算用户佣金与每日出勤统计
搞定了数据库查询,尤其是涉及到多表联合(JOIN)又要聚合计算(COUNT、SUM)的时候,常常让人头疼。这里有个典型场景:咱们需要根据用户在特定分店的出勤情况,结合该分店当日的财务数据,来计算用户的佣金,并且还要统计当天该分店的总出勤人数。
原始需求的目标输出是这样的:
Date | totalAtt | Amount |
---|---|---|
2022-05-1 | 1 | 60 |
2022-05-2 | 2 | 50 |
2022-05-3 | 3 | 80 |
2022-05-4 | 1 | 200 |
Amount
的计算方式是 (ca + ce) - (car + cer)
,并且特别提到了,2022-05-04
这天的计算是基于 branchid = 5
的数据。这就点明了核心:所有计算都得跟用户实际出勤的分店挂钩 。
手上有的数据是两张表:
money
表 (财务数据)
ID | Date | branchId | ca | ce | car | cer |
---|---|---|---|---|---|---|
1 | 2022-05-1 | 7 | 50 | 20 | 5 | 5 |
2 | 2022-05-1 | 5 | 100 | 20 | 10 | 5 |
3 | 2022-05-2 | 7 | 50 | 20 | 5 | 15 |
4 | 2022-05-2 | 5 | 70 | 20 | 10 | 5 |
5 | 2022-05-3 | 7 | 80 | 30 | 25 | 5 |
6 | 2022-05-3 | 5 | 90 | 20 | 35 | 5 |
7 | 2022-05-4 | 7 | 80 | 30 | 25 | 5 |
8 | 2022-05-4 | 5 | 100 | 200 | 50 | 50 |
att
表 (出勤记录)
ID | date | userid | branchId | att |
---|---|---|---|---|
1 | 2022-05-1 | 20 | 7 | 1 |
2 | 2022-05-2 | 20 | 7 | 1 |
3 | 2022-05-2 | 21 | 7 | 1 |
4 | 2022-05-3 | 20 | 7 | 1 |
5 | 2022-05-3 | 21 | 7 | 1 |
6 | 2022-05-3 | 22 | 7 | 1 |
7 | 2022-05-4 | 20 | 5 | 1 |
咱这篇文章就来捋捋,怎么把这两张表的数据揉在一起,得到想要的结果。
一、 问题在哪儿?症结分析
直接上手 JOIN
,不加思考地 SUM
和 COUNT
很容易出岔子。
- 数据关联的基准 :核心是要确定“哪个用户在哪天去了哪个分店”。这个信息在
att
表里。然后,基于这个“日期”和“分店ID”,我们才能去money
表找对应的财务数据,以及在att
表里统计该分店当日总出勤人数。 Amount
计算的特定性 :Amount
是跟特定branchId
相关的。如果一个用户(比如userid = 20
)某天在branchId = 7
,那Amount
就得用money
表中branchId = 7
且日期匹配的记录。如果另一天他去了branchId = 5
,那就得用branchId = 5
的数据。totalAtt
统计的范围 :totalAtt
是指与目标用户同一天、同一分店的总出勤人数。不是所有分店的总和,也不是该用户所有出勤天数的总和。
如果用简单的 JOIN
和 GROUP BY date
,可能会把不同分店的数据混在一起算,或者 COUNT
出错。比如,如果 money
表里同一天有多个分店的记录,不加区分地 SUM
就会把所有分店的 ca
, ce
等加起来,这显然不对。
二、 解决方案:步步为营,逐个击破
思路是先定位出特定用户(假设是 userid = 20
)每天出勤的分店,然后以此为基础,关联财务数据和统计总出勤。用子查询或者公用表表达式(CTE)能让逻辑更清晰。
方案:子查询联合作战
这个法子通过几个子查询分别准备好需要的数据片段,最后拼起来。
1. 原理与作用
- 子查询1 (定位用户出勤信息) :先从
att
表筛选出特定用户(如userid = 20
)的每日出勤记录,明确他每天去了哪个branchId
。这是我们后续所有计算的“锚点”。 - 子查询2 (计算每日分店总出勤) :从
att
表统计每个branchId
在每一天的总出勤人数 (totalAtt
)。 - 主查询 (整合数据) :
- 将子查询1的结果与
money
表JOIN
,条件是date
和branchId
都匹配。这样就能拿到该用户出勤分店当日的财务数据,并计算Amount
。 - 再将上述结果与子查询2的结果
JOIN
,条件也是date
和branchId
匹配。这样就能拿到该用户出勤分店当日的totalAtt
。
- 将子查询1的结果与
2. 代码示例 (以 userid = 20
为例)
SELECT
ua.date AS `Date`,
ba.totalAtt AS `totalAtt`,
(m.ca + m.ce) - (m.car + m.cer) AS `Amount`
FROM
(
-- 子查询1: 找出 userid = 20 的用户每天去的 branchId
SELECT DISTINCT date, branchId
FROM att
WHERE userid = 20
) AS ua -- user_attendance
JOIN
money AS m ON ua.date = m.date AND ua.branchId = m.branchId
JOIN
(
-- 子查询2: 计算每天每个 branchId 的总出勤人数
SELECT date, branchId, COUNT(userid) AS totalAtt
FROM att
GROUP BY date, branchId
) AS ba -- branch_attendance
ON ua.date = ba.date AND ua.branchId = ba.branchId
ORDER BY
ua.date;
3. 解释
ua
(user_attendance): 这个子查询SELECT DISTINCT date, branchId FROM att WHERE userid = 20
非常关键。它为我们锚定了userid = 20
的用户在哪些天分别去了哪些分店。DISTINCT
在这里是为了防止万一att
表里某个用户某天在同一个分店有多条记录(虽然按表结构看不太可能,但加上无妨)。m
(money): 直接使用money
表。通过JOIN money AS m ON ua.date = m.date AND ua.branchId = m.branchId
,我们确保了money
表的数据是根据userid = 20
用户实际去的那个分店和日期来选取的。ba
(branch_attendance): 这个子查询SELECT date, branchId, COUNT(userid) AS totalAtt FROM att GROUP BY date, branchId
计算了数据库里每一天、每一个分店的总出勤人数。然后通过JOIN ... ON ua.date = ba.date AND ua.branchId = ba.branchId
,我们精确匹配到userid = 20
用户所在分店、当日的总出勤。ORDER BY ua.date
: 让结果按日期排序,跟期望输出一致。
这条 SQL 执行后,对于 userid = 20
的数据,应该就能得到题目中的目标结果了。
4. 安全建议
- 输入验证 :如果
userid
是从外部传入的(比如网页参数),务必进行严格的类型检查和清理,防止 SQL 注入。参数化查询是最佳实践。 - 权限最小化 :执行查询的数据库用户应该只拥有必需的最小权限(比如对这两张表的
SELECT
权限)。
5. 进阶使用技巧:使用公用表表达式 (CTE)
对于复杂查询,CTE 能让 SQL 语句的可读性大大增强。上面的查询用 CTE 改写如下:
WITH UserAttendance AS (
-- CTE 1: 找出 userid = 20 的用户每天去的 branchId
SELECT DISTINCT date, branchId
FROM att
WHERE userid = 20
),
BranchDailyAttendance AS (
-- CTE 2: 计算每天每个 branchId 的总出勤人数
SELECT date, branchId, COUNT(userid) AS totalAtt
FROM att
GROUP BY date, branchId
)
SELECT
ua.date AS `Date`,
bda.totalAtt AS `totalAtt`,
(m.ca + m.ce) - (m.car + m.cer) AS `Amount`
FROM
UserAttendance ua
JOIN
money m ON ua.date = m.date AND ua.branchId = m.branchId
JOIN
BranchDailyAttendance bda ON ua.date = bda.date AND ua.branchId = bda.branchId
ORDER BY
ua.date;
是不是清爽多了?每个 CTE 定义一个逻辑块,主查询的结构也更明了。功能上和子查询版本是一样的,但对于维护和理解来说,CTE 往往更胜一筹。
6. 再谈性能
如果 att
表和 money
表数据量巨大,性能可能会成为考量点。
- 索引 :确保
att
表的userid
,date
,branchId
列以及money
表的date
,branchId
列上有合适的索引。- 对于
att
表,(userid, date, branchId)
的复合索引可能很有用,或者至少(userid)
和(date, branchId)
分别有索引。 - 对于
money
表,(date, branchId)
的复合索引是必须的。
- 对于
DISTINCT
的考量 :在UserAttendance
CTE/子查询中,如果业务逻辑能保证att
表里一个userid
在同一date
只会对应一个branchId
,那DISTINCT
可以省略,有时能稍微提升点性能。但如果不能保证,DISTINCT
就是必要的。- 数据量 :如果子查询的结果集很大,连接操作的成本会相应增加。但在这个场景下,
UserAttendance
的结果集受限于单个用户的出勤天数,通常不会特别大。BranchDailyAttendance
的结果集大小取决于 (总天数 * 总分店数),这个可能是个大头,但也是必要计算。
这种分步构建查询的思路,把复杂问题拆解成小块,不仅解决了当前问题,也为处理其他类似的多表关联聚合统计需求提供了不错的参考。选择子查询还是CTE,主要看个人偏好和团队规范,目标都是写出正确、高效、易懂的SQL。