Skip to content

PostgreSQL 窗口函数:数据分析的超级望远镜 🔭

💡 想象一下,当你用望远镜观察星空时,既能看清每颗星星的细节,又能分析星座的整体分布。这就是窗口函数的魔力!它让你在查看每一行数据细节的同时,还能进行复杂的跨行分析,就像拥有数据分析的"超级望远镜"!

一、为什么需要窗口函数?🤔

传统 SQL 在数据分析时有两大痛点:

窗口函数完美解决了这些问题:

  • 保留所有细节:不像 GROUP BY 会折叠数据
  • 跨行计算:轻松实现排名、累计值等复杂计算
  • 高性能:避免多层嵌套子查询

IMPORTANT

窗口函数的核心价值:在不折叠行的前提下,实现对数据集的智能分析

二、实际业务场景案例 🌟

场景 1:部门薪资分析(人力资源)

业务背景
公司需要分析各部门薪资分布,找出每个部门的薪资排名前3的员工进行表彰,同时计算员工薪资占部门总薪资的百分比

sql
::: code-group
```sql [解决方案]
SELECT 
    employee_name,
    department,
    salary,
    -- 部门内薪资排名
    DENSE_RANK() OVER (
        PARTITION BY department 
        ORDER BY salary DESC
    ) AS salary_rank,
    
    -- 薪资占部门比例
    ROUND(salary * 100.0 / SUM(salary) OVER (
        PARTITION BY department
    ), 2) AS salary_percent
FROM employees
WHERE salary_rank <= 3;  -- 取各部门前三名
sql
-- 需要多次查询 + 手动拼接结果
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) rn 
    FROM employees
) t WHERE rn <= 3;

-- 额外查询计算部门总薪资
SELECT dept, SUM(salary) FROM employees GROUP BY dept;

:::


**示例数据**:
```sql
-- 处理前数据
employee_name | department | salary
--------------|------------|--------
Alice         | IT         | 80000
Bob           | IT         | 75000
Charlie       | IT         | 85000
David         | HR         | 65000
Eve           | HR         | 70000

处理结果

employee_name | department | salary | salary_rank | salary_percent
--------------|------------|--------|-------------|---------------
Charlie       | IT         | 85000  |      1      |     35.42
Alice         | IT         | 80000  |      2      |     33.33
Bob           | IT         | 75000  |      3      |     31.25
Eve           | HR         | 70000  |      1      |     51.85
David         | HR         | 65000  |      2      |     48.15

价值:单次查询完成排名+占比计算,快速锁定高绩效员工!


场景 2:销售增长分析(电子商务)

业务背景
电商平台需要分析月度销售额同比增长率,识别增长异常月份,要求显示当月销售额、去年同期销售额和增长率。

sql
SELECT
    month,
    sales,
    -- 获取去年同月销售额
    LAG(sales, 12) OVER (ORDER BY month) AS last_year_sales,
    
    -- 计算同比增长率(处理除零错误)
    ROUND(
        (sales - COALESCE(LAG(sales, 12) OVER (ORDER BY month), 0)) * 100.0 /
        NULLIF(COALESCE(LAG(sales, 12) OVER (ORDER BY month), 0), 1  -- 注意除零保护
    , 2) AS yoy_growth
FROM monthly_sales;

示例数据

sql
-- 处理前数据
month   | sales
--------|-------
2023-01 | 10000
2023-02 | 12000
...
2024-01 | 15000
2024-02 | 13000

处理结果

month   | sales | last_year_sales | yoy_growth
--------|-------|-----------------|-----------
2024-01 | 15000 |     10000       |   50.00%
2024-02 | 13000 |     12000       |    8.33%

⚠️ 注意:实际查询需增加完整除零保护:

sql
CASE WHEN NULLIF(LAG(sales,12) OVER(),0) IS NULL 
     THEN 0 
     ELSE (sales - LAG(sales,12) OVER()) * 100.0 / NULLIF(LAG(sales,12) OVER(),0)
END

价值:一键计算关键业务指标,快速定位异常波动!


场景 3:用户留存分析(移动应用)

业务背景
APP 需要识别连续登录7天以上的忠诚用户,用于推送专属福利。

sql
-- 步骤1:标记连续登录组
WITH login_groups AS (
    SELECT
        user_id,
        login_date,
        login_date - INTERVAL '1 day' * ROW_NUMBER() OVER (
            PARTITION BY user_id ORDER BY login_date
        ) AS login_group  -- 核心技巧:日期序列减行号
    FROM user_logins
)

-- 步骤2:统计连续登录天数
SELECT
    user_id,
    MIN(login_date) AS start_date,
    MAX(login_date) AS end_date,
    COUNT(*) AS login_days
FROM login_groups
GROUP BY user_id, login_group
HAVING COUNT(*) >= 7;  -- 筛选连续登录7天以上

示例数据

sql
-- 处理前数据
user_id | login_date
--------|------------
1001    | 2024-01-01
1001    | 2024-01-02
...
1001    | 2024-01-07
1002    | 2024-01-01
1002    | 2024-01-03  -- 此处断开

处理结果

user_id | start_date  | end_date    | login_days
--------|-------------|-------------|-----------
1001    | 2024-01-01  | 2024-01-07  |     7

价值:精准识别高价值用户,提升用户留存率和活跃度!

三、核心概念解析 🧠

1. 分区(PARTITION BY) - 创建分析小组

就像把公司员工按部门分组,但不折叠任何成员!

sql
SELECT
    employee_name,
    department,
    salary,
    AVG(salary) OVER (PARTITION BY department) AS dept_avg  
FROM employees;

效果
每个员工都能看到自己部门的平均薪资 👇

Alice  | IT | 80000 | 80000
Bob    | IT | 75000 | 80000  -- 同部门平均值相同
Charlie| IT | 85000 | 80000

TIP

省略 PARTITION BY 时,整个表作为单一分区分析

2. 排序(ORDER BY) - 控制计算顺序

像给学生按成绩排队,再逐个发奖状 🎓

sql
SELECT
    employee_name,
    salary,
    SUM(salary) OVER (ORDER BY salary) AS running_total  
FROM employees;

效果:薪资累计和像爬楼梯一样上升 👇

Grace  | 55000 |  55000
Frank  | 60000 | 115000  (=55000+60000)
Henry  | 62000 | 177000  (=115000+62000)

3. 窗口帧 - 精确控制计算范围

模式适用场景示例
ROWS精确控制行数ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
RANGE按数值范围(如日期、金额)RANGE BETWEEN 30 PRECEDING AND CURRENT ROW
GROUPS处理相同值的组GROUPS BETWEEN 1 PRECEDING AND 1 FOLLOWING
sql
-- 7日移动平均销售额
SELECT
    date,
    sales,
    AVG(sales) OVER (
        ORDER BY date
        RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW
    ) AS ma_7days
FROM daily_sales;

四、常用函数工具箱 🧰

1. 排名三剑客

sql
SELECT
    product_name,
    price,
    ROW_NUMBER() OVER (ORDER BY price DESC) AS row_num,  -- 唯一序号
    RANK() OVER (ORDER BY price DESC) AS rank,           -- 排名(允许并列)
    DENSE_RANK() OVER (ORDER BY price DESC) AS dense_rank -- 紧凑排名
FROM products;

2. 分析函数(时间旅行者)

sql
-- 查看价格变化
SELECT
    date,
    price,
    LAG(price, 1) OVER (ORDER BY date) AS yesterday_price,  -- 昨日价格
    LEAD(price, 1) OVER (ORDER BY date) AS tomorrow_price   -- 明日价格
FROM stock_prices;

3. 聚合函数(保留细节版)

sql
-- 实时计算累计占比
SELECT
    time,
    revenue,
    SUM(revenue) OVER (ORDER BY time) AS running_total,
    revenue * 100.0 / SUM(revenue) OVER () AS total_percent  
FROM hourly_sales;

五、最佳实践与避坑指南 ⚠️

✅ 最佳实践

sql
-- 1. 使用命名窗口复用配置(DRY原则)
SELECT
    SUM(salary) OVER w AS dept_total,
    AVG(salary) OVER w AS dept_avg
FROM employees
WINDOW w AS (PARTITION BY department);  

-- 2. 为窗口列创建索引
CREATE INDEX idx_dept_salary ON employees (department, salary); -- [!code tip]

-- 3. 使用ROWS提升大表性能
-- 比RANGE更快,避免全分区扫描
AVG(price) OVER (ORDER BY date ROWS 6 PRECEDING)

❌ 常见错误

sql
-- 错误1:在WHERE中使用窗口函数
SELECT name, RANK() OVER (ORDER BY score) 
FROM students
WHERE RANK() OVER (ORDER BY score) <= 10; -- 语法错误!

-- 正确做法:使用子查询
SELECT * FROM (
    SELECT name, RANK() OVER (ORDER BY score) AS rnk
    FROM students
) WHERE rnk <= 10;

-- 错误2:忽略NULL和除零问题
SELECT sales / LAG(sales) OVER ()  -- 可能除零或NULL!
FROM monthly_sales;

-- 正确做法:添加保护
SELECT 
    sales / NULLIF(COALESCE(LAG(sales) OVER (), 1), 0) 
FROM monthly_sales;

性能警告

复杂窗口函数在大数据表上可能变慢!
优化方案

  1. 缩小窗口范围(如 ROWS 30 PRECEDING 替代整个分区)
  2. 在子查询中预过滤数据
  3. 使用物化视图预计算

六、总结 🚀

掌握窗口函数,你将获得数据分析的"超能力":

  1. 分区透视 👓 → PARTITION BY 分组不折叠
  2. 时空操控 ⏳ → ORDER BY + LAG/LEAD 穿越数据时间线
  3. 动态范围 🎯 → 窗口帧精确控制计算范围
  4. 多维分析 📊 → 排名/聚合/分析函数组合使用

💫 最终效果:单次查询完成复杂分析,性能提升 3-5 倍,代码量减少 50%!

TIP

实践建议:从简单的 OVER (PARTITION BY ... ORDER BY ...) 开始,逐步增加复杂度,很快你就能像数据分析大师一样思考!