WITH:带名字的派生表
上一课结束在一个嵌在 FROM 里的派生表。同一条查询写成公用表表达式是这样:
1
WITH category_stats AS (
SELECT cat.name AS category,
COUNT(*) AS products,
ROUND(AVG(p.price), 2) AS average
FROM products p
JOIN categories cat ON cat.id = p.category_id
GROUP BY cat.id
)
SELECT *
FROM category_stats
WHERE products >= 10
ORDER BY average DESC;
结果 — 共 26 行,这里是前 5 行
| category | products | average |
|---|---|---|
| Wearables | 11 | 1672.29 |
| Programming | 10 | 1585.67 |
| Tablets | 17 | 1529.45 |
| Audio | 11 | 1482.89 |
| Kitchen | 12 | 1443.21 |
浏览器会另开一个标签页打开 Planche,示例数据库已经载入,这条查询也已经放好。本页其他示例都会复用同一个标签页;在你按下 F9 之前,什么都不会执行。
结果完全一样,但现在是往前读的:先定义 category_stats,再用它。这个名字只在这一条语句里有效——什么都没被创建,也不需要收拾。
一步接一步
CTE 可以串联,后面的能用前面的,于是嵌套查询变成了一串有名字的步骤。
2
WITH paid_orders AS (
SELECT id, customer_id, total
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
),
per_customer AS (
SELECT customer_id,
COUNT(*) AS orders,
ROUND(SUM(total), 2) AS revenue
FROM paid_orders
GROUP BY customer_id
)
SELECT c.first_name || ' ' || c.last_name AS customer,
c.country,
pc.orders,
pc.revenue
FROM per_customer pc
JOIN customers c ON c.id = pc.customer_id
ORDER BY pc.revenue DESC
LIMIT 20;
结果 — 共 20 行,这里是前 5 行
| customer | country | orders | revenue |
|---|---|---|---|
| Sofia Patel | BR | 10 | 70324.42 |
| Zara Novak | JP | 5 | 70149.53 |
| Elena Rossi | KR | 10 | 58867.23 |
| Ines Choi | CA | 6 | 58455.76 |
| Noah Costa | ES | 4 | 57721.52 |
三个有名字的步骤:先缩小订单范围,再按客户汇总,最后接上客户姓名。写成嵌套子查询就得从里往外读;这样写则是按发生顺序排列的一串步骤。查询开始难跟了,先试着拆成 CTE,通常是最有效的一招。
窗口函数:聚合但不压缩
GROUP BY 把多行变成一行。窗口函数在一批行上做计算,同时保留每一行。
3
SELECT p.name,
p.price,
RANK() OVER (ORDER BY p.price DESC) AS price_rank,
DENSE_RANK() OVER (ORDER BY p.price DESC) AS dense_price_rank,
ROW_NUMBER() OVER (ORDER BY p.price DESC) AS row_no
FROM products p
ORDER BY p.price DESC
LIMIT 20;
结果 — 共 20 行,这里是前 5 行
| name | price | price_rank | dense_price_rank | row_no |
|---|---|---|---|---|
| Glide GPS Watch Pro | 2484.4 | 1 | 1 | 1 |
| Terra 3D Puzzle | 2481.77 | 2 | 2 | 2 |
| Aria Castle Kit | 2479.41 | 3 | 3 | 3 |
| Aria LED Strip Pro | 2463.41 | 4 | 4 | 4 |
| Nova Brief Answers | 2445.49 | 5 | 5 | 5 |
OVER (ORDER BY p.price DESC) 就是窗口:这次计算要看哪些行、按什么顺序看。价格打平时,三个函数给出三种答案:
ROW_NUMBER()— 1、2、3、4。永远互不相同,平局随便断。RANK()— 1、2、2、4。平局共用一个名次,下一个数字会跳过去。DENSE_RANK()— 1、2、2、3。平局共用名次,数字不跳。
用哪个取决于你的问题。"给我正好十行"要 ROW_NUMBER;"谁是第二名"要的是两种排名之一。
每组前 N 名
没有窗口函数的话,这需要一条相关子查询去数同分类里有多少商品更贵。PARTITION BY 会为每一组重新开始编号。
4
WITH ranked AS (
SELECT cat.name AS category,
p.name AS product,
p.price,
ROW_NUMBER() OVER (PARTITION BY cat.id ORDER BY p.price DESC) AS rank_in_category
FROM products p
JOIN categories cat ON cat.id = p.category_id
)
SELECT category, product, price
FROM ranked
WHERE rank_in_category <= 3
ORDER BY category, rank_in_category;
结果 — 共 81 行,这里是前 5 行
| category | product | price |
|---|---|---|
| Audio | Atlas Earbuds | 2334.28 |
| Audio | Kite Speaker | 2331.15 |
| Audio | Quill Speaker | 2177.84 |
| Bags | Pulse Crossbody Mini | 2377 |
| Bags | Zen Crossbody | 2295.35 |
累计值
给窗口加上 ORDER BY,每一行就能看到直到它自己为止的全部。
5
WITH monthly AS (
SELECT substr(ordered_at, 1, 7) AS month,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
AND ordered_at >= '2025-01-01'
GROUP BY month
)
SELECT month,
revenue,
ROUND(SUM(revenue) OVER (ORDER BY month), 2) AS running_total
FROM monthly
ORDER BY month;
结果 — 共 12 行,这里是前 5 行
| month | revenue | running_total |
|---|---|---|
| 2025-01 | 311707.73 | 311707.73 |
| 2025-02 | 350775.62 | 662483.35 |
| 2025-03 | 351184.96 | 1013668.31 |
| 2025-04 | 374465.94 | 1388134.25 |
| 2025-05 | 381541.09 | 1769675.34 |
CTE 产出每月一行,SUM(revenue) OVER (ORDER BY month) 沿着这张列表往下累加。同一招还能做累计计数、累计百分比和燃尽图——凡是某一行的答案依赖它前面那些行的,都是这个形状。
和上一行比较
6
WITH monthly AS (
SELECT substr(ordered_at, 1, 7) AS month,
ROUND(SUM(total), 2) AS revenue
FROM orders
WHERE status IN ('paid', 'shipped', 'delivered')
AND ordered_at >= '2025-01-01'
GROUP BY month
)
SELECT month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS previous_month,
ROUND(revenue - LAG(revenue) OVER (ORDER BY month), 2) AS change
FROM monthly
ORDER BY month;
结果 — 共 12 行,这里是前 5 行
| month | revenue | previous_month | change |
|---|---|---|---|
| 2025-01 | 311707.73 | NULL | NULL |
| 2025-02 | 350775.62 | 311707.73 | 39067.89 |
| 2025-03 | 351184.96 | 350775.62 | 409.34 |
| 2025-04 | 374465.94 | 351184.96 | 23280.98 |
| 2025-05 | 381541.09 | 374465.94 | 7075.15 |
LAG(revenue) 在窗口里往回够一行,LEAD 往前够一行。本来需要把表按偏移量和自己连接才能算的环比,现在一行就够了。第一行的 previous_month 是 NULL——它前面什么都没有——所以变化也是 NULL,这比一个会误导人的零要诚实。
支持情况
窗口函数是标准特性,你可能遇到的引擎基本都有:PostgreSQL、SQL Server 和 Oracle 多年前就有了,SQLite 从 3.25(2018)起,MySQL 从 8.0(2018)起,MariaDB 从 10.2 起。你还可能碰到的、没有窗口函数的,大概只剩 MySQL 5.7。
现在你能做什么
读数据方面,你基本能把结果整成任何需要的形状了。最后两课讲的是工作的另一半:安全地改数据,以及查清一条查询为什么慢。