返回

MySQL多表JOIN:轻松计算用户佣金与每日出勤统计

mysql

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,不加思考地 SUMCOUNT 很容易出岔子。

  1. 数据关联的基准 :核心是要确定“哪个用户在哪天去了哪个分店”。这个信息在 att 表里。然后,基于这个“日期”和“分店ID”,我们才能去 money 表找对应的财务数据,以及在 att 表里统计该分店当日总出勤人数。
  2. Amount 计算的特定性Amount 是跟特定 branchId 相关的。如果一个用户(比如 userid = 20)某天在 branchId = 7,那 Amount 就得用 money 表中 branchId = 7 且日期匹配的记录。如果另一天他去了 branchId = 5,那就得用 branchId = 5 的数据。
  3. totalAtt 统计的范围totalAtt 是指与目标用户同一天、同一分店的总出勤人数。不是所有分店的总和,也不是该用户所有出勤天数的总和。

如果用简单的 JOINGROUP BY date,可能会把不同分店的数据混在一起算,或者 COUNT 出错。比如,如果 money 表里同一天有多个分店的记录,不加区分地 SUM 就会把所有分店的 ca, ce 等加起来,这显然不对。

二、 解决方案:步步为营,逐个击破

思路是先定位出特定用户(假设是 userid = 20)每天出勤的分店,然后以此为基础,关联财务数据和统计总出勤。用子查询或者公用表表达式(CTE)能让逻辑更清晰。

方案:子查询联合作战

这个法子通过几个子查询分别准备好需要的数据片段,最后拼起来。

1. 原理与作用

  • 子查询1 (定位用户出勤信息) :先从 att 表筛选出特定用户(如 userid = 20)的每日出勤记录,明确他每天去了哪个 branchId。这是我们后续所有计算的“锚点”。
  • 子查询2 (计算每日分店总出勤) :从 att 表统计每个 branchId 在每一天的总出勤人数 (totalAtt)。
  • 主查询 (整合数据)
    • 将子查询1的结果与 moneyJOIN,条件是 datebranchId 都匹配。这样就能拿到该用户出勤分店当日的财务数据,并计算 Amount
    • 再将上述结果与子查询2的结果 JOIN,条件也是 datebranchId 匹配。这样就能拿到该用户出勤分店当日的 totalAtt

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。