「80% 的 GMV 是谁贡献的」:占比与百分位
老板问了个致命问题:「我们 80% 的 GMV 是多少头部用户贡献的?」--组内占比与百分位专场。
学 · 35 min
01sum / avg / count / max 作为窗口函数使用
场景明细行旁边要同时出现「组内总和、组内均值」--普通聚合做不到同框。
任何聚合函数加 over 都是窗口版:sum() over (partition by ...) 在每行旁边贴上组内总和。这比「先 group by 算汇总,再 join 回明细」少一步、快一截、还更可读。
select id, user_id, total_amount, sum(total_amount) over (partition by user_id) as 该用户总额, avg(total_amount) over (partition by user_id) as 该用户均值 from orders;易错窗口版聚合和普通聚合不是二选一:要明细带汇总用窗口,要纯汇总报表用 group by。
02组内占比的通用写法:x / sum(x) OVER (PARTITION BY g)
场景「每个商品销售额占其类目的比例」--分母是组内总和。
占比 = 分子 /
sum(分子) over (partition by 组),一行搞定,不用子查询。要同时算「占类目%」和「占全站%」就嵌两个不同分区的 sum:partition by 类目 和 空括号(全表)。分母分区是什么,占比就是什么口径。select p.name as 商品, s.销售额, round(100.0 * s.销售额 / sum(s.销售额) over (partition by s.类目), 1) as 占类目pct, round(100.0 * s.销售额 / sum(s.销售额) over (), 1) as 占全站pct from ( select p.id, p.name, c.name as 类目, sum(i.qty * i.unit_price) as 销售额 from order_items i join products p on p.id = i.product_id join categories c on c.id = p.category_id group by p.id, p.name, c.name ) s;易错占比列求和应 = 100% 是自检手段;round 后可能是 99.9 / 100.1,属于舍入误差,交付时说一句即可。
03明细行同时带组均值、与均值的差
场景哪些订单「明显高出该用户的平均水平」--离群排查。
x - avg(x) over (partition by 组):每行直接标出离组均值多远。加上第 1 条的组均值列,一张表看懂「这个用户的正常水位在哪、这单偏了多少」。select id, user_id, total_amount, avg(total_amount) over (partition by user_id) as 用户均值, total_amount - avg(total_amount) over (partition by user_id) as 偏离均值 from orders;易错avg 跳过 NULL 行(D4 的分母口径问题在窗口版同样存在)--列里有 NULL 时先想清楚分母。
04percentile_cont 算分组中位数
场景「每个类目的价格中位数」--中位数没有窗口函数版,语法也和别的聚合长得不一样。
percentile_cont(0.5) within group (order by 列)算任意分位数(0.5 = 中位数),配合 group by 出分组结果。它是分组聚合不是窗口函数,PG 也不支持给它加 over(ordered-set 聚合没有窗口版,直接报错)--想要「明细行带组内中位数」,把分组结果写成 CTE 再 join 回明细。-- 每个一级类目的价格中位数(分组聚合版) select c.name as 类目, percentile_cont(0.5) within group (order by p.price) as 价格中位数 from products p join categories c on c.id = p.category_id group by c.name; -- 明细带组内中位数:分组结果当 CTE 再 join 回去(没有窗口版可走捷径) with m as ( select category_id, percentile_cont(0.5) within group (order by price) as 中位数 from products group by 1 ) select p.name, p.price, m.中位数 from products p join m on m.category_id = p.category_id;易错PG 没有 median() 函数;within group 这个子句别漏--漏了语法就错。给 ordered-set 聚合加 over 也是语法错。
练 · 70 min
- 每个商品销售额占其所属一级类目的比例
参考答案
分母 = sum(销售额) over (partition by 类目):partition by 是谁,占比就是什么口径。自检手段:按类目把占类目pct求和应等于 100(round 后 99.9/100.1 属舍入误差)。
select 商品, 类目, 销售额, round(100.0 * 销售额 / sum(销售额) over (partition by 类目), 1) as 占类目pct from ( select p.name as 商品, c1.name as 类目, sum(i.qty * i.unit_price) as 销售额 from order_items i join products p on p.id = i.product_id join categories c2 on c2.id = p.category_id -- 二级 join categories c1 on c1.id = c2.parent_id -- 自连接到一级 group by p.id, p.name, c1.name ) s order by 类目, 销售额 desc; - 每个一级类目销售额占全站的比例(同一条 SQL 里两个占比都要有)
参考答案
两个分母两个口径:partition by 类目 vs 空括号(全表)。这就是「先 group by 算汇总再 join 回明细」的窗口一步到位版。
select 商品, 类目, 销售额, round(100.0 * 销售额 / sum(销售额) over (partition by 类目), 1) as 占类目pct, round(100.0 * 销售额 / sum(销售额) over (), 1) as 占全站pct from ( select p.name as 商品, c1.name as 类目, sum(i.qty * i.unit_price) as 销售额 from order_items i join products p on p.id = i.product_id join categories c2 on c2.id = p.category_id join categories c1 on c1.id = c2.parent_id group by p.id, p.name, c1.name ) s order by 类目, 销售额 desc; - 每个用户消费额的百分位排名
参考答案
percent_rank = (rank - 1) / (总行数 - 1),区间 [0, 1],0 表示排第一、并列给相同值。想知道「我和比我强的共占多少」用 cume_dist。
select user_id, 消费额, round(percent_rank() over (order by 消费额 desc)::numeric, 3) as 前百分之几 from (select user_id, sum(total_amount) as 消费额 from orders group by 1) t order by 消费额 desc limit 20; - 每个一级类目的价格中位数
参考答案
PG 没有 median();percentile_cont(0.5) within group (order by 列) 是唯一正解,within group 子句漏了直接语法错。想给明细行带组内中位数,PG16 不支持它的窗口写法,得把分组结果当 CTE 再 join 回明细。
select c1.name as 一级类目, percentile_cont(0.5) within group (order by p.price) as 价格中位数 from products p join categories c2 on c2.id = p.category_id join categories c1 on c1.id = c2.parent_id group by c1.name order by 一级类目; - 每笔订单金额与该用户平均客单价的差额
参考答案
明细行同框组均值 + 偏离值,一眼看出哪单明显高于该用户的正常水位(离群排查)。注意 avg 窗口版同样跳过 NULL 行--D4 的分母口径问题跟着窗口一起继承。
select id, user_id, total_amount, round(avg(total_amount) over (partition by user_id), 2) as 用户平均客单价, round(total_amount - avg(total_amount) over (partition by user_id), 2) as 偏离均值 from orders order by user_id, 偏离均值 desc;