高频 SQL 50 题 之 连接篇 1378 1068 1581 197 1661 577 1280 570 1934

  1. 使用唯一标识码替换员工ID

  2. 产品销售分析 I

  3. 进店却未进行过交易的顾客

  4. 上升的温度

  5. 每台机器的进程平均运行

  6. 员工奖金

  7. 学生们参加各科测试的次数

  8. 至少有5名直接下属的经理

  9. 确认率

1378. 使用唯一标识码替换员工ID

写法

复制代码
select unique_id , name from Employees left join EmployeeUNI on Employees.id =EmployeeUNI.id;

1068. 产品销售分析 I

我的写法

复制代码
select product_name , year , price from Sales natural join Product ;

参考写法

复制代码
SELECT Product.product_name, Sales.year, Sales.price 
FROM  Product INNER Join  Sales 
On  Product.product_id = Sales.product_id;

1581. 进店却未进行过交易的顾客

步骤一:使用 LEFT JOIN 关联两表(Visits 左表,Transactions 右表)
步骤二:关联后得到中间表

|----------|-------------|----------------|----------|--------|
| visit_id | customer_id | transaction_id | visit_id | amount |
| 1 | 23 | 12 | 1 | 910 |
| 2 | 9 | 13 | 2 | 970 |
| 4 | 30 | null | 4 | null |
| 5 | 54 | 2 | 5 | 310 |
| 5 | 54 | 3 | 5 | 300 |
| 5 | 54 | 9 | 5 | 200 |
| 6 | 96 | null | 6 | null |
| 7 | 54 | null | 7 | null |
| 8 | 54 | null | 8 | null |

步骤三:最终需要 customer_idcount_no_trans 两个字段
步骤四:用过滤条件 WHERE transaction_id IS NULL(或 amount IS NULL)筛选

过滤后得到的表:

|----------|-------------|----------------|----------|--------|
| visit_id | customer_id | transaction_id | visit_id | amount |
| 4 | 30 | null | 4 | null |
| 6 | 96 | null | 6 | null |
| 7 | 54 | null | 7 | null |
| 8 | 54 | null | 8 | null |

步骤五:按 customer_id 分组聚合,统计无交易的访问次数

最终结果表:

|-------------|----------------|
| customer_id | count_no_trans |
| 54 | 2 |
| 30 | 1 |
| 96 | 1 |

复制代码
SELECT
    v.customer_id,
    COUNT(v.visit_id) AS count_no_trans
FROM
    Visits v
LEFT JOIN
    Transactions t
ON
    v.visit_id = t.visit_id
WHERE
    t.transaction_id IS NULL
GROUP BY
    v.customer_id;

197. 上升的温度

复制代码
SELECT w1.id
FROM Weather w1
JOIN Weather w2
  ON DATEDIFF(w1.recordDate, w2.recordDate) = 1
  AND w1.temperature > w2.temperature;

FROM Weather w1 JOIN Weather w2Weather 表做自连接,把表拆成两个副本:

  • w1:代表「今天」的温度记录

  • w2:代表「昨天」的温度记录

ON DATEDIFF(w1.recordDate, w2.recordDate) = 1DATEDIFF 函数计算两个日期的天数差,筛选出 ** 相差 1 天(即昨天和今天)** 的记录对

  • 不同数据库的日期函数差异:

    • MySQL:DATEDIFF(w1.recordDate, w2.recordDate) = 1

    • PostgreSQL:w1.recordDate - w2.recordDate = INTERVAL '1 day'

    • SQL Server:DATEDIFF(day, w2.recordDate, w1.recordDate) = 1

AND w1.temperature > w2.temperature 筛选出「今天温度 > 昨天温度」的记录,最终取出今天的 id 作为结果

1661. 每台机器的进程平均运行时间

复制代码
SELECT
    a1.machine_id,
    ROUND(AVG(a2.timestamp - a1.timestamp), 3) AS processing_time
FROM
    Activity a1
JOIN
    Activity a2
ON
    a1.machine_id = a2.machine_id
    AND a1.process_id = a2.process_id
    AND a1.activity_type = 'start'
    AND a2.activity_type = 'end'
GROUP BY
    a1.machine_id;

FROM Activity a1 JOIN Activity a2Activity 表做自连接 ,将同一进程的 startend 记录关联到同一行:

  • a1:代表 start 类型的记录

  • a2:代表 end 类型的记录

ON 关联条件

  • a1.machine_id = a2.machine_id:同一机器

  • a1.process_id = a2.process_id:同一进程

  • a1.activity_type = 'start' AND a2.activity_type = 'end':匹配开始和结束记录

**a2.timestamp - a1.timestamp**计算单个进程的运行耗时

**AVG(...)**对同一机器的所有进程耗时求平均值

**ROUND(..., 3)**四舍五入保留 3 位小数,符合题目要求

**GROUP BY a1.machine_id**按机器 ID 分组,分别计算每台机器的平均耗时

577. 员工奖金

复制代码
SELECT
    e.name,
    b.bonus
FROM
    Employee e
LEFT JOIN
    Bonus b
ON
    e.empId = b.empId
WHERE
    b.bonus < 1000 OR b.bonus IS NULL;

FROM Employee e LEFT JOIN Bonus b ON e.empId = b.empIdEmployee 为左表做左连接,保留所有员工记录:

  • 有奖金的员工:bonus 列显示对应金额

  • 无奖金的员工:bonus 列自动填充 null

**WHERE b.bonus < 1000 OR b.bonus IS NULL**同时满足两个筛选条件:

  • b.bonus < 1000:奖金小于 1000

  • b.bonus IS NULL:无任何奖金(左连接产生的空值)

**SELECT e.name, b.bonus**最终返回员工姓名和对应奖金,符合题目要求

1280. 学生们参加各科测试的次数

复制代码
SELECT
    s.student_id,
    s.student_name,
    sub.subject_name,
    COUNT(e.subject_name) AS attended_exams
FROM
    Students s
CROSS JOIN
    Subjects sub
LEFT JOIN
    Examinations e
ON
    s.student_id = e.student_id
    AND sub.subject_name = e.subject_name
GROUP BY
    s.student_id, s.student_name, sub.subject_name
ORDER BY
    s.student_id, sub.subject_name;

CROSS JOIN Students s, Subjects sub(笛卡尔积)

  • 作用:生成所有学生 × 所有科目 的完整组合(即每个学生对应每一门科目,共 学生数 × 科目数 行)

  • 这是实现「0 次考试也显示」的核心步骤,确保不会遗漏任何学生 - 科目组合

LEFT JOIN Examinations e ON ...(左连接考试记录)

  • 关联条件:student_idsubject_name 同时匹配

  • 作用:将完整组合与实际考试记录关联,无考试记录的组合会被填充为 null

COUNT(e.subject_name) AS attended_exams(统计次数)

  • 注意:必须用 COUNT(e.subject_name) 而非 COUNT(*)

    • COUNT(*) 会统计所有行(包括 null),导致 0 次考试被计为 1

    • COUNT(列名) 会自动忽略 null 值,精准统计实际考试次数

GROUP BY s.student_id, s.student_name, sub.subject_name(分组)

  • 按学生 ID、学生姓名、科目名分组,确保每个学生 - 科目组合单独统计

  • 符合 SQL 语法规范(非聚合列必须出现在 GROUP BY 中)

ORDER BY s.student_id, sub.subject_name(排序)

  • 按要求对结果升序排序

570. 至少有5名直接下属的经理

写法 1:子查询分组(推荐,兼容性最好)

复制代码
SELECT name
FROM Employee
WHERE id IN (
    SELECT managerId
    FROM Employee
    WHERE managerId IS NOT NULL
    GROUP BY managerId
    HAVING COUNT(*) >= 5
);

写法 2:自连接 + 分组

复制代码
SELECT e1.name
FROM Employee e1
JOIN Employee e2
  ON e1.id = e2.managerId
GROUP BY e1.id, e1.name
HAVING COUNT(e2.id) >= 5;

1934. 确认率

复制代码
-- 等价手动计算写法
SELECT
    s.user_id,
    ROUND(
        IFNULL(SUM(IF(c.action = 'confirmed', 1, 0)) / COUNT(c.action), 0),
        2
    ) AS confirmation_rate
FROM Signups s
LEFT JOIN Confirmations c ON s.user_id = c.user_id
GROUP BY s.user_id;
复制代码

SUM(IF(c.action = 'confirmed', 1, 0))

  • 如果 action = confirmed → 记 1

  • 否则(timeout / NULL)→ 记 0

  • SUM 把所有 1 加起来 → 成功确认次数

总结

这篇内容整理了多道经典 SQL 联表练习题,涵盖左连接、自连接、内连接、笛卡尔积、分组统计、条件过滤等核心用法,从员工 ID 关联、销售数据分析,到无交易顾客统计、温度对比、进程耗时计算、奖金筛选、考试次数统计、经理下属人数及用户确认率计算,通过清晰步骤和对应 SQL 写法,系统练习了多表联合查询与聚合函数的实际应用,能快速巩固 JOIN 与 GROUP BY 的解题思路。

相关推荐
用户5757303346242 小时前
从 SQL 到对象:Prisma 如何成为全栈开发的“降维打击”利器
数据库
三更两点2 小时前
智能代理工具包:MCP vs. Agent Skills vs. AGENTS.md
数据库·人工智能
丸辣,我代码炸了2 小时前
PostgreSQL 大数据查询与索引优化核心总结
大数据·数据库·postgresql
等....3 小时前
Redis使用
数据库·redis·mybatis
betazhou3 小时前
记一次Oracle REDO在线日志损坏故障修复
数据库·oracle·redo·ora-00600
一只小bit3 小时前
Redis 初步入门教程:简单介绍和安装配置
数据库·redis·缓存
ChatInfo3 小时前
Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模
数据库·人工智能·mysql
SPC的存折3 小时前
6、MySQL设置TLS加密访问
linux·运维·服务器·数据库·mysql
老苏畅谈运维3 小时前
DBA分析 ORA 报错的利器,errorstack让 Oracle 错误现原形
数据库·oracle·dba