组内计数广播是什么
COUNT(*) OVER (PARTITION BY 维度) 把分组计数写到该分区的每一行上,不折叠行。它最常见的用法是「明细加组总数」一起展示,或者用外层条件筛掉不满足数量的分组(例如找出订单数超过 10 的用户并保留其明细)。语义上等价于 GROUP BY 之后再把统计结果连接回明细表,但一条语句就写完,优化器通常只扫一遍数据。MySQL 8 与 PostgreSQL 都支持。
基本信息
| 适用平台 | MySQL 8.0+ / PostgreSQL 11+ |
|---|---|
| 语义 | 分区内计数并广播到该分区每一行,结果行数不变 |
| 与 GROUP BY 区别 | 窗口函数不折叠行,聚合函数折叠行 |
| 等价改写 | LEFT JOIN 子查询 GROUP BY 的结果,再取计数列 |
| 过滤限制 | 窗口函数不能写在 WHERE / HAVING,需包一层派生表或 CTE |
典型使用场景
- 明细列表里带上所属分组的总数,省掉一次额外查询
- 筛选分组总量:订单数超过阈值的用户、消费额达标的客户
- 算明细占组内比例的前置步骤(分母就来自这个广播值)
- 找重复数据:按业务键分区,计数大于 1 的行即为重复
- 数据质量校验:核对每组的明细行数与预期是否一致
基础用法与过滤
基础用法与过滤
-- 明细行 + 所属用户的订单总数
SELECT id, user_id, amount,
COUNT(*) OVER (PARTITION BY user_id) AS user_order_cnt
FROM orders
WHERE created_at >= '2026-09-01';
-- 过滤「组内数量」必须包一层:窗口函数不能直接写在 WHERE
SELECT * FROM (
SELECT id, user_id, amount,
COUNT(*) OVER (PARTITION BY user_id) AS cnt
FROM orders
) t
WHERE t.cnt > 10;找重复与占比
找重复与占比
# 同一个手机号出现多次的行:cnt 大于 1 即重复
SELECT * FROM (
SELECT id, phone,
COUNT(*) OVER (PARTITION BY phone) AS cnt
FROM users
) t WHERE t.cnt > 1;
-- 明细占组内份额:分子是当前行,分母是广播的组内合计
SELECT id, user_id, amount,
ROUND(100 * amount / NULLIF(SUM(amount) OVER (PARTITION BY user_id), 0), 2) AS pct
FROM orders;三种写法对比
| 需求 | 写法 | 说明 |
|---|---|---|
| 明细 + 组总数 | COUNT(*) OVER (PARTITION BY …) | 一条语句完成,不折叠行 |
| 只要组统计 | GROUP BY + COUNT(*) | 结果行数更少,内存占用更低 |
| 过滤组统计 | 派生表 / CTE 包住窗口结果 | 窗口函数不能出现在 WHERE |
| 组内去重计数 | COUNT(DISTINCT x) OVER (…) | PostgreSQL 支持,MySQL 8 不支持 |
窗口里用 COUNT(DISTINCT …) 的支持面很窄:PostgreSQL 支持,MySQL 8 会直接报错;需要在 MySQL 里做组内去重计数时,先用派生表去重,再在外面做窗口计数。
分区键含 NULL 时所有 NULL 行会被归入同一个分区(这一点与连接中「NULL 永不相等」的行为相反),用它检测重复会误报,需要先排除或统一空值。