随着数据规模不断膨胀,市场对于精通SQL的数据从业者的需求已不仅限于基础层面。掌握中级乃至高级的SQL概念,对于提升数据分析效率与通过技术面试都至关重要。在云栈社区的技术讨论中,高效的SQL查询也是经久不衰的话题。本文将深入探讨10个关键的高级SQL查询技巧,助你从容应对复杂的数据处理场景。
1. 公共表表达式(CTEs)
当你需要频繁使用子查询时,公共表表达式(CTEs)便能大显身手——它本质上是创建一个临时的命名结果集。
使用CTEs是模块化与分解代码的绝佳方式,就像你将一篇文章拆分成多个段落。试想以下在WHERE子句中嵌套了子查询的语句:
SELECT
name,
salary
FROM
People
WHERE
NAME IN ( SELECT DISTINCT NAME FROM population WHERE country = "Canada" AND city = "Toronto" )
AND salary >= (
SELECT
AVG( salary )
FROM
salaries
WHERE
gender = "Female")
这看起来已经有些复杂了。如果查询中需要更多的子查询,代码会变得多么难以维护?此时,CTEs的优势就体现出来了。
with toronto_ppl as (
SELECT DISTINCT name
FROM population
WHERE country = "Canada"
AND city = "Toronto"
)
, avg_female_salary as (
SELECT AVG(salary) as avgSalary
FROM salaries
WHERE gender = "Female"
)
SELECT name
, salary
FROM People
WHERE name in (SELECT DISTINCT FROM toronto_ppl)
AND salary >= (SELECT avgSalary FROM avg_female_salary)
现在逻辑清晰多了:WHERE子句用于筛选出在多伦多的人。CTEs不仅允许你将代码分解为更小的、可管理的块,还能为每个CTE分配一个变量名(如toronto_ppl和avg_female_salary),极大地增强了可读性。
此外,CTEs还支持实现更高级的技术,例如创建递归查询。
2. 递归CTEs
递归CTE是一种能够自我引用的CTE,类似于编程中的递归函数。它在处理具有层次结构的数据时特别有用,例如组织架构图、文件系统或网页链接图。
一个递归CTE通常包含三个部分:
- 锚定成员:返回CTE基本结果的初始查询。
- 递归成员:引用CTE自身的递归查询。该部分通过UNION ALL与锚定成员的结果集合并。
- 终止条件:确保递归能够停止。
以下是一个用于获取每位员工及其经理ID的递归CTE示例:
with org_structure as (
SELECT id
, manager_id
FROM staff_members
WHERE manager_id IS NULL
UNION ALL
SELECT sm.id
, sm.manager_id
FROM staff_members sm
INNER JOIN org_structure os
ON os.id = sm.manager_id
)
3. 临时函数
临时函数是提升代码质量的重要工具,原因如下:
- 它允许你将代码逻辑封装成更小的、可复用的单元。
- 它有助于编写更清晰、更易读的代码。
- 它可以防止代码重复,类似于在Python中定义和使用函数。
考虑以下示例,它使用CASE语句根据工作年限划分职级:
SELECT name
, CASE WHEN tenure < 1 THEN "analyst"
WHEN tenure BETWEEN 1 and 3 THEN "associate"
WHEN tenure BETWEEN 3 and 5 THEN "senior"
WHEN tenure > 5 THEN "vp"
ELSE "n/a"
END AS seniority
FROM employees
相反,你可以创建一个临时函数来封装这个职级判断逻辑:
CREATE TEMPORARY FUNCTION get_seniority(tenure INT64) AS (
CASE WHEN tenure < 1 THEN "analyst"
WHEN tenure BETWEEN 1 and 3 THEN "associate"
WHEN tenure BETWEEN 3 and 5 THEN "senior"
WHEN tenure > 5 THEN "vp"
ELSE "n/a"
END
);
SELECT name
, get_seniority(tenure) as seniority
FROM employees
通过使用临时函数,主查询变得简洁且易于理解,并且get_seniority函数可以在其他查询中被重复使用。
4. 使用CASE WHEN进行数据透视
CASE WHEN语句因其强大的条件逻辑能力,在SQL问题中极为常见。它允许你根据其他变量的值来分配特定的值或类别。
一个不太为人熟知但极其有用的功能是,CASE WHEN可以用来“透视”数据。例如,如果你有一个“月份”列,并且希望为每个月份创建一个单独的列,你可以使用一系列CASE WHEN语句来实现。
示例问题:编写一个SQL查询来重新格式化表格,使每个月都有一个对应的收入列。
Initial table:
+------+---------+-------+
| id | revenue | month |
+------+---------+-------+
| 1 | 8000 | Jan |
| 2 | 9000 | Jan |
| 3 | 10000 | Feb |
| 1 | 7000 | Feb |
| 1 | 6000 | Mar |
+------+---------+-------+
Result table:
+------+-------------+-------------+-------------+-----+-----------+
| id | Jan_Revenue | Feb_Revenue | Mar_Revenue | ... | Dec_Revenue |
+------+-------------+-------------+-------------+-----+-----------+
| 1 | 8000 | 7000 | 6000 | ... | null |
| 2 | 9000 | null | null | ... | null |
| 3 | null | 10000 | null | ... | null |
+------+-------------+-------------+-------------+-----+-----------+
5. EXCEPT vs NOT IN
EXCEPT和NOT IN操作符的作用几乎是相同的,它们都用于比较两个查询/表之间的行。然而,两者之间存在一些细微但重要的差别。
首先,EXCEPT会自动过滤掉重复的行,并返回唯一的结果集,而NOT IN则不会。
其次,EXCEPT要求两个查询/表具有相同数量的列,而NOT IN通常用于比较单个列。
6. 自联结
自联结是指一个表与自身进行连接。你或许认为这没什么用处,但实际上它在实际场景中非常普遍。在许多情况下,数据往往存储在一个大表中,而非多个小表中。这时,自联结就成为解决特定问题的关键。
让我们看一个例子。
示例问题:给定下面的员工表,编写一个SQL查询,找出薪水高于其经理的员工。对于下表来说,Joe是唯一一个薪水高于经理的员工。
+----+-------+--------+-----------+
| Id | Name | Salary | ManagerId |
+----+-------+--------+-----------+
| 1 | Joe | 70000 | 3 |
| 2 | Henry | 80000 | 4 |
| 3 | Sam | 60000 | NULL |
| 4 | Max | 90000 | NULL |
+----+-------+--------+-----------+
Answer:
SELECT
a.Name as Employee
FROM
Employee as a
JOIN Employee as b on a.ManagerID = b.Id
WHERE a.Salary > b.Salary
7. Rank vs Dense Rank vs Row Number
对行或数值进行排名是一个非常常见的需求。以下是一些公司常用的排名场景:
- 按消费额、利润等对客户进行排名。
- 对销量最高的产品进行排名。
- 对销售额最高的国家进行排名。
- 按观看时长、独立观众数等对视频进行排名。
在SQL中,有几种方法可以为行分配“排名”,我们将通过示例来探索它们。考虑以下查询及其结果:
SELECT Name
, GPA
, ROW_NUMBER() OVER (ORDER BY GPA desc)
, RANK() OVER (ORDER BY GPA desc)
, DENSE_RANK() OVER (ORDER BY GPA desc)
FROM student_grades

图1:ROW_NUMBER、RANK和DENSE_RANK函数排名结果对比
ROW_NUMBER() 为每一行分配一个从1开始的唯一序号。当出现并列情况时(例如Bob和Carrie),如果没有定义进一步的排序规则,ROW_NUMBER() 会任意分配数字。
RANK() 同样从1开始排名,但当出现并列时,RANK() 会分配相同的名次,并且后续名次会出现跳跃(即留下空位)。
DENSE_RANK() 与 RANK() 类似,但在出现并列名次后不会留下空位。请注意,使用 DENSE_RANK(),Daniel排名第3,而不是第4。
8. 计算差值
另一个常见需求是比较不同时间段的值。例如,本月销售额与上月销售额的差值(Delta)是多少?或者本月销售额与去年同期本月销售额的差值是多少?
当需要比较不同时间段的值以计算差异时,LEAD() 和 LAG() 窗口函数就派上用场了。
以下是一些示例:
-- 将每月的销售额与上月进行比较
SELECT month
, sales
, sales - LAG(sales, 1) OVER (ORDER BY month)
FROM monthly_sales
-- 将每月的销售额与去年同月进行比较
SELECT month
, sales
, sales - LAG(sales, 12) OVER (ORDER BY month)
FROM monthly_sales
9. 计算累计总数
如果你已经了解了 ROW_NUMBER() 和 LAG()/LEAD(),那么这个技巧可能不会让你感到意外。但如果你还不了解,那么这可能是最有用的窗口函数之一,尤其是在你需要可视化增长趋势时!
结合 SUM() 函数与窗口函数,我们可以轻松计算累计总数。参考以下示例:
SELECT Month
, Revenue
, SUM(Revenue) OVER (ORDER BY Month) AS Cumulative
FROM monthly_revenue

图2:使用窗口函数计算月度累计收入
10. 日期时间处理
在数据科学面试中,你几乎肯定会遇到涉及日期时间数据的SQL问题。例如,你可能需要按时间维度对数据分组,或者将日期格式从“DD-MM-YYYY”转换为“YYYY-MM-DD”。
你需要熟悉的一些常用日期时间函数包括:
EXTRACT
DATE_TRUNC
DATE_ADD, DATE_SUB
DATEDIFF
示例问题:给定天气表,编写一个SQL查询,找出所有温度高于前一天(昨天)的日期的ID。
+---------+------------------+------------------+
| Id(INT) | RecordDate(DATE) | Temperature(INT) |
+---------+------------------+------------------+
| 1 | 2015-01-01 | 10 |
| 2 | 2015-01-02 | 25 |
| 3 | 2015-01-03 | 20 |
| 4 | 2015-01-04 | 30 |
+---------+------------------+------------------+
Answer:
SELECT
a.Id
FROM
Weather a,
Weather b
WHERE
a.Temperature > b.Temperature
AND DATEDIFF(a.RecordDate, b.RecordDate) = 1
熟练掌握这些SQL高级查询技巧,不仅能让你在处理复杂数据时游刃有余,也能在技术面试中脱颖而出。