排序分页与子查询

ORDER BY 排序

基础语法

-- 单列排序(默认 ASC 升序)
SELECT * FROM products ORDER BY price;

-- 降序
SELECT * FROM products ORDER BY price DESC;

-- 多列排序:先按分类升序,同分类内按价格降序
SELECT name, category_id, price
FROM products
ORDER BY category_id ASC, price DESC;

NULL 值排序

PostgreSQL 中 NULL 默认排在最后(ASC 时)或最前(DESC 时).可以显式控制:

-- NULL 放最前
SELECT * FROM users ORDER BY city ASC NULLS FIRST;

-- NULL 放最后(DESC 时默认 NULL 在前,用 NULLS LAST 改变)
SELECT * FROM users ORDER BY balance DESC NULLS LAST;

按表达式排序

-- 按用户名长度排序
SELECT username, LENGTH(username) AS name_len
FROM users
ORDER BY LENGTH(username) DESC;

-- 按列的序号排序(SELECT 中第 2 列)
SELECT username, balance FROM users ORDER BY 2 DESC;

-- 按 CASE 表达式自定义排序优先级
SELECT id, status, total FROM orders
ORDER BY
CASE status
WHEN 'pending' THEN 1
WHEN 'paid' THEN 2
WHEN 'shipped' THEN 3
WHEN 'done' THEN 4
WHEN 'cancelled' THEN 5
END;

ORDER BY 与 GROUP BY 的配合

聚合查询中,ORDER BY 可以使用聚合结果排序:SELECT city, COUNT(*) AS cnt FROM users GROUP BY city ORDER BY cnt DESC; 这里的 cnt 是别名,PostgreSQL 允许在 ORDER BY 中引用 SELECT 中的别名.

LIMIT / OFFSET 分页

-- 取前 3 条(最贵的 3 个商品)
SELECT name, price FROM products ORDER BY price DESC LIMIT 3;

-- 分页:第 2 页,每页 5 条(跳过前 5 条)
SELECT * FROM products ORDER BY id LIMIT 5 OFFSET 5;

-- 只取第一条(等价于 LIMIT 1)
SELECT * FROM orders ORDER BY created_at DESC FETCH FIRST 1 ROW ONLY;

OFFSET 分页的性能陷阱

OFFSET 10000 意味着数据库要扫描并丢弃前 10000 行. 数据量大时性能急剧下降. 生产环境推荐游标分页(Keyset Pagination):

-- 游标分页:基于上一页最后一条的 id 继续查
-- 第 1 页
SELECT * FROM orders ORDER BY id LIMIT 10;

-- 第 2 页(假设上一页最后一条 id = 10)
SELECT * FROM orders WHERE id > 10 ORDER BY id LIMIT 10;

-- 优势:无论第几页,性能恒定(利用 id 索引直接定位)

子查询概述

子查询就是"查询里面套查询".根据返回结果的形状分三类:

类型 返回 典型用法
标量子查询 单个值(1行1列) 放在 SELECT / WHERE / HAVING 中当作一个值用
行子查询 一行多列 与行比较 WHERE (a, b) = (SELECT ...)
表子查询 多行多列 放在 FROM 中当作临时表,IN / EXISTS 判断

标量子查询

-- 在 SELECT 中:每个商品价格与平均价格的差值
SELECT name, price,
price - (SELECT AVG(price) FROM products) AS diff_from_avg
FROM products
ORDER BY diff_from_avg DESC;

-- 在 WHERE 中:查询价格高于平均价的商品
SELECT name, price FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- 在 HAVING 中:哪些分类的平均价格高于全局平均
SELECT c.name, ROUND(AVG(p.price), 2) AS avg_price
FROM categories c
JOIN products p ON p.category_id = c.id
GROUP BY c.name
HAVING AVG(p.price) > (SELECT AVG(price) FROM products);

标量子查询的要求

标量子查询必须恰好返回 1 行 1 列. 如果返回 0 行,值为 NULL;如果返回多行,PostgreSQL 报错. 聚合函数(AVG/MAX/MIN)天然返回单值,所以最常用.

行子查询与表子查询

表子查询放在 FROM(派生表)

-- 先聚合,再筛选(等价于 HAVING,但有时更清晰)
SELECT * FROM (
SELECT user_id, COUNT(*) AS cnt, SUM(total) AS total_spent
FROM orders
WHERE status = 'done'
GROUP BY user_id
) AS user_stats
WHERE total_spent > 5000
ORDER BY total_spent DESC;

IN 子查询

-- 查询购买过"电子产品"的用户
SELECT username FROM users
WHERE id IN (
SELECT DISTINCT o.user_id
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.product_id = oi.product_id
WHERE p.category_id = 1
);

-- NOT IN:查询从未下过单的用户
SELECT username FROM users
WHERE id NOT IN (SELECT DISTINCT user_id FROM orders);

NOT IN 与 NULL 的陷阱

如果子查询结果中包含 NULL,NOT IN 会返回空结果. 因为 x NOT IN (1, 2, NULL) 等价于 x != 1 AND x != 2 AND x != NULL,最后一个条件永远是 UNKNOWN. 安全做法:用 NOT EXISTS 替代.

EXISTS 与 IN

EXISTS 检查"子查询是否有结果",不关心具体返回什么值:

-- 查询至少下过一单的用户(EXISTS 写法)
SELECT u.username
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);

-- 查询从未写过评价的用户(NOT EXISTS)
SELECT u.username
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM reviews r WHERE r.user_id = u.id
);
-- frank, henry(这两个用户没有评价记录)

EXISTS vs IN:如何选择?

场景 推荐 原因
子查询结果集小 IN 简洁,优化器能内联
子查询结果集大 EXISTS 找到第一条匹配就停止扫描
子查询可能含 NULL EXISTS 避免 NOT IN + NULL 的陷阱
需要关联外层条件 EXISTS 天然支持相关子查询
-- 相关子查询:查询"订单金额超过该用户平均订单金额"的订单
SELECT o.id, o.user_id, o.total
FROM orders o
WHERE o.total > (
SELECT AVG(o2.total)
FROM orders o2
WHERE o2.user_id = o.user_id
)
ORDER BY o.user_id, o.total DESC;

CTE - 公用表表达式

CTE(Common Table Expression)用 WITH 关键字定义,可以理解为"给子查询起个名字".好处:

  • 可读性:复杂查询拆成多个命名步骤
  • 可复用:同一个 CTE 可在主查询中多次引用
  • 递归:支持递归查询(树形结构遍历等)
-- 基础 CTE:先算每用户的订单统计,再筛选
WITH user_orders AS (
SELECT user_id,
COUNT(*) AS order_count,
SUM(total) AS total_spent
FROM orders
WHERE status != 'cancelled'
GROUP BY user_id
)
SELECT u.username, u.city, uo.order_count, uo.total_spent
FROM users u
JOIN user_orders uo ON uo.user_id = u.id
WHERE uo.total_spent > 5000
ORDER BY uo.total_spent DESC;
-- 多个 CTE 组合:用户消费排名 + 评价活跃度
WITH spending AS (
SELECT user_id, SUM(total) AS total_spent
FROM orders WHERE status = 'done'
GROUP BY user_id
),
reviewing AS (
SELECT user_id, COUNT(*) AS review_count, ROUND(AVG(rating), 2) AS avg_given
FROM reviews
GROUP BY user_id
)
SELECT u.username,
COALESCE(s.total_spent, 0) AS total_spent,
COALESCE(r.review_count, 0) AS review_count,
r.avg_given
FROM users u
LEFT JOIN spending s ON s.user_id = u.id
LEFT JOIN reviewing r ON r.user_id = u.id
ORDER BY total_spent DESC;

CTE vs 子查询 - 什么时候用哪个?

功能上两者等价,选择依据是可读性:

  • 子查询只用一次且很短 → 直接内联
  • 需要复用,或逻辑复杂需要"步骤拆分" → 用 CTE

PostgreSQL 12+ 默认会将 CTE 内联优化(等价于子查询),性能无差异. 如果需要强制物化 CTE,加 MATERIALIZED.

-- CTE 中引用前面的 CTE(链式)
WITH monthly_sales AS (
SELECT TO_CHAR(created_at, 'YYYY-MM') AS month,
SUM(total) AS revenue
FROM orders WHERE status = 'done'
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
),
avg_monthly AS (
SELECT ROUND(AVG(revenue), 2) AS avg_revenue FROM monthly_sales
)
SELECT ms.month, ms.revenue,
am.avg_revenue,
CASE WHEN ms.revenue > am.avg_revenue THEN 'above' ELSE 'below' END AS vs_avg
FROM monthly_sales ms, avg_monthly am
ORDER BY ms.month;

练习

  1. Top-N 查询 -- 查询价格最高的 5 个商品,显示名称和价格
  2. OFFSET 分页 -- 查询第 2 贵到第 4 贵的商品(跳过最贵的一个,取 3 个)
  3. 标量子查询 -- 查询余额高于所有用户平均余额的用户
  4. NOT EXISTS -- 查询从未被购买过的商品
  5. CTE + Top-N -- 先计算每个商品的总销量(order_items 中 quantity 之和),再查询销量排名前 3 的商品
  6. 多表 DISTINCT 聚合 -- 查询"至少购买过 2 种不同分类商品"的用户名
  7. 相关子查询 -- 找出每个用户的"最大单笔订单"的订单 ID 和金额
  8. 月度报表 -- 用 CTE 构建每月的订单数,总金额,以及相比上月的金额增长率(第一个月为 NULL)
  9. EXISTS 组合 -- 查询"购买过 MacBook Pro 14 但没有给它写过评价"的用户
参考答案
-- 1. 最贵的 5 个商品
SELECT name, price FROM products ORDER BY price DESC LIMIT 5;

-- 2. 第 2~4 贵
SELECT name, price FROM products ORDER BY price DESC LIMIT 3 OFFSET 1;

-- 3. 余额高于平均
SELECT username, balance FROM users
WHERE balance > (SELECT AVG(balance) FROM users)
ORDER BY balance DESC;
-- eve(15000), alice(5000), grace(2200)

-- 4. 从未被购买的商品
SELECT p.name FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);
-- 羽绒服, 算法导论

-- 5. CTE + 销量 Top 3
WITH product_sales AS (
SELECT product_id, SUM(quantity) AS total_sold
FROM order_items
GROUP BY product_id
)
SELECT p.name, ps.total_sold
FROM product_sales ps
JOIN products p ON p.id = ps.product_id
ORDER BY ps.total_sold DESC
LIMIT 3;
-- AirPods Pro: 4, MacBook Pro 14: 3, iPhone 15: 3

-- 6. 至少购买过 2 种不同分类
SELECT u.username
FROM users u
JOIN orders o ON o.user_id = u.id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
GROUP BY u.id, u.username
HAVING COUNT(DISTINCT p.category_id) >= 2;
-- eve, dave, bob, grace

-- 7. 每用户最大单笔订单(相关子查询)
SELECT o.id, o.user_id, u.username, o.total
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.total = (
SELECT MAX(o2.total) FROM orders o2 WHERE o2.user_id = o.user_id
)
ORDER BY o.total DESC;

-- 8. 月度报表 + 增长率(自连接方式,窗口函数版本见下一篇)
WITH monthly AS (
SELECT TO_CHAR(created_at, 'YYYY-MM') AS month,
COUNT(*) AS order_count,
SUM(total) AS revenue
FROM orders
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
)
SELECT m1.month, m1.order_count, m1.revenue,
ROUND((m1.revenue - m2.revenue) / m2.revenue * 100, 1) AS growth_pct
FROM monthly m1
LEFT JOIN monthly m2
ON m2.month = TO_CHAR(
(TO_DATE(m1.month, 'YYYY-MM') - INTERVAL '1 month'),
'YYYY-MM'
)
ORDER BY m1.month;

-- 9. 买了 MacBook 但没写评价
SELECT u.username
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.user_id = u.id AND oi.product_id = 1
)
AND NOT EXISTS (
SELECT 1 FROM reviews r
WHERE r.user_id = u.id AND r.product_id = 1
);
-- carol

第 2 题 OFFSET 1 跳过第 1 条,LIMIT 3 取接下来 3 条. 第 4 题 NOT EXISTS 比 NOT IN 更安全(不受 NULL 影响). 第 7 题对每行 order 做相关子查询找到同 user_id 的最大 total 进行比对. 第 8 题用自连接把"当月"和"上月"拼在一起算增长率. 第 9 题用两个 EXISTS 组合"满足条件 A 但不满足条件 B"的逻辑.