8、高级查询技巧:JOIN连接、子查询与聚合函数

说实话,数据库编程里最考验功力的地方,就是怎么把数据从多个表里“捞”出来。单表查询谁都会写,但一旦涉及到两张甚至更多表,很多人就开始犯晕。我刚开始做嵌入式数据库开发时,也在这上面栽过跟头——有一次因为搞混了 LEFT JOIN 和 INNER JOIN,查出来的数据少了整整一半,排查了整整一个下午。

今天咱们就把这些高级查询技巧掰开揉碎了讲。你想想看,掌握了这些,以后写 SQL 就能像搭积木一样灵活。

8.1 聚合函数:给数据“算总账”

聚合函数说白了就是用来做统计的。我习惯把它们分成两类:一类是数数的(COUNT),一类是算数的(SUM、AVG、MAX、MIN)。

先看个例子。假设我们有个订单表:

CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    customer TEXT,
    amount REAL,
    status TEXT
);

INSERT INTO orders VALUES 
(1, '张三', 150.0, '已完成'),
(2, '李四', 200.0, '已完成'),
(3, '张三', 80.0, '待支付'),
(4, '王五', 300.0, '已完成'),
(5, '李四', 120.0, '已取消');

现在我想知道:

  • 总共有多少笔订单?
  • 已完成订单的总金额是多少?
  • 平均每笔订单金额是多少?

用聚合函数写出来就是:

SELECT 
    COUNT(*) AS 总订单数,
    SUM(CASE WHEN status = '已完成' THEN amount ELSE 0 END) AS 已完成总额,
    AVG(amount) AS 平均金额,
    MAX(amount) AS 最大金额,
    MIN(amount) AS 最小金额
FROM orders;

输出结果:

总订单数已完成总额平均金额最大金额最小金额
5650.0170.0300.080.0

避坑指南:我曾经犯过一个低级错误——用 COUNT(amount) 去统计行数。结果发现有一行 amount 是 NULL,COUNT 直接把它跳过了。记住,COUNT(*) 统计所有行,COUNT(列名) 只统计非 NULL 的行。

8.2 GROUP BY:分组统计的艺术

光有聚合函数还不够。很多时候我们需要按某个维度分组统计。比如按客户统计每个人的订单总额:

SELECT 
    customer,
    COUNT(*) AS 订单数,
    SUM(amount) AS 总金额
FROM orders
GROUP BY customer;

输出:

customer订单数总金额
张三2230.0
李四2320.0
王五1300.0

嗯,这里要注意:SELECT 子句中出现的非聚合列,必须出现在 GROUP BY 中。这是 SQL 的硬性规定,违反就会报错。

8.3 HAVING:对分组结果“再过滤”

WHERE 是在分组前过滤,HAVING 是在分组后过滤。比如我只想看订单数超过1的客户:

SELECT 
    customer,
    COUNT(*) AS 订单数
FROM orders
GROUP BY customer
HAVING COUNT(*) > 1;

结果只返回张三和李四,王五被过滤掉了。

重要提醒:HAVING 和 WHERE 的执行顺序不同。WHERE 先执行,过滤掉不需要的行;然后 GROUP BY 分组;最后 HAVING 过滤分组结果。这个顺序搞错了,查询结果可能完全不对。

8.4 JOIN连接:把表“粘”在一起

实际项目中数据很少只存在一张表里。比如订单表只存客户ID,客户详细信息在另一张表。这时候就需要 JOIN。

先建两张表:

CREATE TABLE customers (
    id INTEGER PRIMARY KEY,
    name TEXT,
    city TEXT
);

CREATE TABLE orders2 (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER,
    amount REAL,
    order_date TEXT
);

INSERT INTO customers VALUES 
(1, '张三', '北京'),
(2, '李四', '上海'),
(3, '王五', '广州');

INSERT INTO orders2 VALUES 
(101, 1, 150.0, '2024-01-15'),
(102, 2, 200.0, '2024-01-16'),
(103, 1, 80.0, '2024-01-17'),
(104, 4, 300.0, '2024-01-18');  -- 注意:客户ID=4 在customers表中不存在

8.4.1 INNER JOIN:只取交集

INNER JOIN 只返回两个表中匹配的行。说白了就是“两边都有才算数”:

SELECT 
    c.name,
    c.city,
    o.amount,
    o.order_date
FROM customers c
INNER JOIN orders2 o ON c.id = o.customer_id;

结果只有3行:张三2笔,李四1笔。王五没有订单所以不出现,客户ID=4的订单因为customers表里没有对应记录也不出现。

8.4.2 LEFT JOIN:左表全保留

LEFT JOIN 会保留左表的所有行,右表没有匹配的用 NULL 填充。这个我用的最多,因为很多时候需要知道“哪些数据没有关联上”:

SELECT 
    c.name,
    c.city,
    o.amount,
    o.order_date
FROM customers c
LEFT JOIN orders2 o ON c.id = o.customer_id;

结果有4行:张三2笔,李四1笔,王五1笔(amount和order_date为NULL)。

我的经验:调试 JOIN 查询时,我习惯先分别查两张表的数据量,再对比 JOIN 后的结果。如果行数对不上,十有八九是 JOIN 条件写错了。有一次我排查了半天,发现是 ON 条件里用了 c.id = o.id,而不是 c.id = o.customer_id——这种低级错误,写代码时真的很容易忽略。

8.5 子查询:查询里套查询

子查询就是 SQL 里再嵌套一个 SQL。我把它分成两种:

  • 标量子查询:返回单个值,可以用在 SELECT、WHERE 里
  • 表子查询:返回多行多列,通常用在 FROM 或 IN 里

举个例子,找出订单金额高于平均值的订单:

SELECT id, customer_id, amount
FROM orders2
WHERE amount > (SELECT AVG(amount) FROM orders2);

这里 (SELECT AVG(amount) FROM orders2) 就是一个标量子查询,先算出平均值,再拿每行去比较。

再看一个表子查询的例子:找出每个客户的最新一笔订单:

SELECT c.name, o.amount, o.order_date
FROM customers c
INNER JOIN (
    SELECT customer_id, amount, order_date,
           ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
    FROM orders2
) o ON c.id = o.customer_id AND o.rn = 1;

性能提示:子查询不是万能的。我遇到过嵌套了四五层的子查询,跑起来慢得像蜗牛。后来改成 JOIN 或者临时表,速度提升了十几倍。记住:能用 JOIN 解决的,尽量别用子查询。

8.6 综合实战:一个完整的查询

把今天学的串起来,写一个稍微复杂点的查询:统计每个城市已完成订单的总金额,只显示总金额超过100的城市,按金额降序排列。

SELECT 
    c.city,
    COUNT(o.id) AS 订单数,
    SUM(o.amount) AS 总金额
FROM customers c
LEFT JOIN orders2 o ON c.id = o.customer_id
WHERE o.status = '已完成' OR o.status IS NULL
GROUP BY c.city
HAVING SUM(o.amount) > 100
ORDER BY 总金额 DESC;

这个查询里,LEFT JOIN、WHERE、GROUP BY、HAVING、ORDER BY 全用上了。你想想看,如果不用这些高级技巧,光靠单表查询,得写多少代码才能实现同样的功能?

8.7 知识体系总览

下面这张图把今天讲的内容串了起来,方便你复习时快速回忆:

高级查询技巧知识体系 聚合函数 COUNT / SUM / AVG / MAX / MIN JOIN连接 INNER JOIN / LEFT JOIN 子查询 标量子查询 / 表子查询 GROUP BY 分组 按维度分组统计 HAVING 过滤 对分组结果再过滤 ORDER BY 排序 ASC / DESC 排序 SQL执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY 常见陷阱 • COUNT(列名) 会忽略 NULL,COUNT(*) 不会 • SELECT 非聚合列必须出现在 GROUP BY 中 • JOIN 条件写错会导致数据丢失或重复

嗯,今天的内容就到这里。这些高级查询技巧,说白了就是让你能用更少的代码,做更复杂的数据分析。刚开始可能会觉得有点绕,但多写几次就熟练了。记住:先理解数据之间的关系,再写 SQL,这样能少走很多弯路。


公众号:蓝海资料掘金营,微信deep3321