学习 SQL 时,连接(Join)和窗口函数(Window Function)是进阶阶段必须掌握的内容。连接用于将多张表按逻辑关系组合;窗口函数则能对分组后的数据进行排名、累计求和等分析。在校招面试中,这两类题型出现频率非常高。本文将梳理常见的连接方式,介绍窗口函数的基本概念与常用函数,并给出典型面试题思路,帮助你夯实基础、应对面试。

1 SQL 高阶 Join

连接是根据两张表之间的关系组合出新的结果集。在关系型数据库中,主要有以下几种逻辑连接方式:

### 连接类型与应用
- **内连接(INNER JOIN)**:只返回两表中匹配的行,常用于获得交集。
- **左外连接(LEFT JOIN)**:返回左表所有记录及右表匹配的行;右表无匹配则为 NULL,适用于保留左表完整数据。
- **右外连接(RIGHT JOIN)**:与左外连接相反,保留右表所有记录。
- **全外连接(FULL JOIN)**:返回两表全部记录,未匹配的列用 NULL 填充,用于查看交集和差集。
- **交叉连接(CROSS JOIN)**:返回笛卡尔积,慎用。
- **半连接(SEMI JOIN)**:只返回左表中有匹配记录的行,可用 EXISTS 实现。
- **反连接(ANTI JOIN)**:返回左表中无匹配记录的行,可用 NOT EXISTS 实现。

1.1 编写 Join 查询的要点

  1. 明确关系字段:Join 语句的 ON 条件应使用能够唯一标识关系的列,常见于外键和主键字段。

选择合适的 Join 类型:根据业务需求决定是否需要保留某张表的全部数据(外连接)还是仅需交集(内连接)。

  1. 注意数据量和索引:在大表 Join 时,如果关联列未建立索引,容易导致查询性能下降;合理创建索引或使用分区可以提升效率。
  2. 避免意外的笛卡尔积:使用 CROSS JOIN 要慎重,在没有 ON条件时与 INNER JOIN 类似的语法(旧式 FROM A, B WHERE ...)容易出错。

1.2 Join 示例

-- 内连接:查询下单的客户名称和订单金额
SELECT c.customer_id, c.name, o.amount
FROM customers AS c
INNER JOIN orders AS o
    ON c.customer_id = o.customer_id;

-- 左外连接:列出所有客户及其订单金额,没有订单的客户金额为 NULL
SELECT c.customer_id, c.name, o.amount
FROM customers AS c
LEFT JOIN orders AS o
    ON c.customer_id = o.customer_id;

-- 半连接:查找有订单的客户
SELECT customer_id, name
FROM customers AS c
WHERE EXISTS (
    SELECT 1 FROM orders AS o
    WHERE o.customer_id = c.customer_id
);

-- 反连接:查找没有下过订单的客户
SELECT customer_id, name
FROM customers AS c
WHERE NOT EXISTS (
    SELECT 1 FROM orders AS o
    WHERE o.customer_id = c.customer_id
);

半连接和反连接广泛应用于面试题中,例如“找出没有购买行为的用户”。使用 EXISTS或 NOT EXISTS比 IN/NOT IN 在大数据量情况下更高效,因为数据库可以提前终止扫描。

2 窗口函数的概念与语法

窗口函数用于在分组的基础上,对每行记录计算一个与当前行相关的聚合或分析值。它们不仅在业务分析中常用,也是校招 SQL 题的高频考点。根据 Heavy.AI 文档,窗口函数的特点如下:

  • 窗口函数对与当前行相关的一组行进行计算。
  • 每个窗口函数必须带有 OVER子句,该子句可指定 PARTITION BY(分区)和 ORDER BY(排序)来定义窗口范围。
  • PARTITION BY 将结果集分成若干分区,在每个分区内独立计算;ORDER BY确定窗口内的顺序。
  • 在排序列相同的情况下,同一分区中值相等的行被视为“同级(peers)”,排名函数会赋予它们相同的排名。

2.1 常用窗口函数

  • ROW_NUMBER()
    为分区中的每一行按顺序编号;常用于筛选排名第 N 的记录,如“取每个部门薪资最高的员工”。

  • RANK()
    为分区中的行排序并给出排名,但遇到并列时会跳过排名值(产生空缺)。

  • DENSE_RANK()
    类似 RANK,但排名不会有空缺;并列数据共享同一排名,而下一名紧跟其后。

窗口函数的语法示例:

-- 计算每个部门员工薪水的排名和累计薪水
SELECT dept_id,
       emp_id,
       salary,
       ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn,
       RANK()       OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk,
       SUM(salary) OVER (PARTITION BY dept_id ORDER BY salary DESC
                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM employees;
  • PARTITION BY dept_id表示按部门分组,排名和累计和在每个部门内重新开始。
  • ORDER BY salary DESC指定了排序规则。
  • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW定义了窗口帧,表示从分区第一行到当前行,用于计算累积和。

2.2 窗口函数面试题示例

示例 1:连续登录天数统计(LeetCode 签到题)

给定用户每天的登录记录表 user_login(userid, login_date),统计每个用户连续登录的天数,取最长的一段。

思路:

  1. 使用 DATEDIFF或日期差计算每次登录与上一天的间隔。
  2. 利用 LAG(login_date) 获取上一天的登录日期。
  3. 构建“断点标识”列:当 login_date – LAG(login_date) != 1时,标记为新的连续段。
  4. 使用 SUM() 作为窗口函数对断点标识进行累加,得到连续段编号。
  5. 最后在每个用户分组中统计最大连续天数。

关键代码片段:

WITH t AS (
  SELECT userid,
         login_date,
         CASE WHEN login_date = DATE_ADD(LAG(login_date) OVER (PARTITION BY userid ORDER BY login_date), INTERVAL 1 DAY)
              THEN 0 ELSE 1 END AS is_new_seq
  FROM user_login
),
seq AS (
  SELECT userid,
         login_date,
         SUM(is_new_seq) OVER (PARTITION BY userid ORDER BY login_date) AS seq_id
  FROM t
)
SELECT userid,
       MAX(COUNT(*)) AS max_consecutive_days
FROM seq
GROUP BY userid, seq_id;

示例 2:每个部门薪资前 3 的员工

利用 ROW_NUMBER() 或 RANK()

SELECT * FROM (
  SELECT dept_id, emp_id, salary,
         ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
  FROM employees
) AS t
WHERE rn <= 3;

此题常用于考察窗口函数配合 PARTITION BY ORDER BY 的使用,区别于使用子查询或 IN的传统做法,效率更高且语义明确。

3 总结与建议

  • 连接与窗口函数是校招 SQL 面试重难点。要理解各类连接的区别和适用场景,尤其是半连接和反连接的逻辑,能有效解决“查找有/无关联数据”的问题。
  • 窗口函数必须配合 OVER子句使用,学会拆解PARTITION BY 和 ORDER BY的作用,掌握常用排名函数和累计函数的特点。
  • 多练习题目与实战。连续登录天数、Top N 排名、累计和等题型是面试高频考点;建议在写 SQL 时先思考如何通过窗口函数简化代码,再注意优化执行效率。
  • 注意 SQL 兼容差异。虽然标准 SQL 定义了大部分窗口函数,但在不同数据库中的语法细节可能略有不同,面试时应说明你熟悉的数据库方言(如 MySQL、PostgreSQL、SQL Server 等)。

通过系统掌握这些高级连接和窗口函数的用法,再结合大量实战练习,相信你在数仓或数据开发的校招面试中能够游刃有余。