找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4667

积分

0

好友

645

主题
发表于 3 天前 | 查看: 16| 回复: 0

SQL中的窗口函数(Window Function)是强大的分析工具,相比于传统的自连接、子查询等复杂写法,它能以更简洁的代码、更高的执行效率和更清晰的逻辑解决许多常见问题。以下,我们将通过五个典型的业务场景,对比传统写法与窗口函数写法,直观展示其优势。

本文示例基于 MySQL 8.0+ 和 PostgreSQL 通用语法。首先创建并插入测试数据:

-- 创建员工表
CREATE TABLE 员工表 (
  员工ID INT PRIMARY KEY,
  员工姓名 VARCHAR(20),
  部门 VARCHAR(20),
  工资 DECIMAL(10,2),
  入职日期 DATE,
  绩效评分 INT -- 1-5分
);

-- 插入测试数据
INSERT INTO 员工表 VALUES
(1, '张三', '研发部', 8000.00, '2020-01-10', 4),
(2, '李四', '研发部', 9500.00, '2019-03-15', 5),
(3, '王五', '研发部', 8000.00, '2021-05-20', 3),
(4, '赵六', '市场部', 7000.00, '2020-07-08', 4),
(5, '钱七', '市场部', 8500.00, '2018-11-30', 5),
(6, '孙八', '人事部', 6500.00, '2022-02-18', 4),
(7, '周九', '人事部', 7500.00, '2021-09-05', 5);

场景1:替代自连接计算相邻行数据(如环比)

需求:计算每个员工的工资与同部门内上一个入职同事的工资差值。

传统写法:自连接 + 日期筛选

-- 传统自连接写法,缺点:需要处理NULL、性能差、逻辑复杂
SELECT 
  e1.员工姓名,
  e1.部门,
  e1.工资,
  e2.工资 AS 上一入职同事工资,
  e1.工资 - COALESCE(e2.工资, 0) AS 工资差值
FROM 员工表 e1
LEFT JOIN 员工表 e2 
  ON e1.部门 = e2.部门  -- 同部门
  AND e2.入职日期 = (  -- 找到e1之前最近入职的员工
    SELECT MAX(入职日期) 
    FROM 员工表 e3 
    WHERE e3.部门 = e1.部门 AND e3.入职日期 < e1.入职日期
  )
ORDER BY e1.部门, e1.入职日期;

窗口函数写法:使用 LAG()

SELECT 
  员工姓名,
  部门,
  工资,
  -- LAG(列名, 偏移量, 默认值):获取分区内当前行之前第N行的数据
  -- PARTITION BY 部门:按部门分区
  -- ORDER BY 入职日期:按入职日期排序
  LAG(工资, 1, 0) OVER (PARTITION BY 部门 ORDER BY 入职日期) AS 上一入职同事工资,
  工资 - LAG(工资, 1, 0) OVER (PARTITION BY 部门 ORDER BY 入职日期) AS 工资差值
FROM 员工表
ORDER BY 部门, 入职日期;

执行结果

员工姓名 部门 工资 上一入职同事工资 工资差值
钱七 市场部 8500.00 0.00 8500.00
赵六 市场部 7000.00 8500.00 -1500.00
李四 研发部 9500.00 0.00 9500.00
张三 研发部 8000.00 9500.00 -1500.00
王五 研发部 8000.00 8000.00 0.00
周九 人事部 7500.00 0.00 7500.00
孙八 人事部 6500.00 7500.00 -1000.00

场景2:替代子查询计算累计值

需求:按入职日期排序,计算每个员工所在部门的累计工资总和。

传统写法:子查询 + 聚合

-- 传统子查询写法,缺点:多次扫描表、性能差、嵌套深
SELECT 
  e1.员工姓名,
  e1.部门,
  e1.工资,
  e1.入职日期,
  (
    SELECT SUM(e2.工资) 
    FROM 员工表 e2 
    WHERE e2.部门 = e1.部门 
      AND e2.入职日期 <= e1.入职日期
  ) AS 部门累计工资
FROM 员工表 e1
ORDER BY e1.部门, e1.入职日期;

窗口函数写法:SUM() 窗口聚合

SELECT 
  员工姓名,
  部门,
  工资,
  入职日期,
  -- SUM(列名) OVER (分区 + 排序):默认计算从分区开始到当前行的累计和
  -- ORDER BY 入职日期:默认窗口范围是“PARTITION内从第一行到当前行”
  SUM(工资) OVER (
    PARTITION BY 部门 
    ORDER BY 入职日期 
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 显式指定累计范围
  ) AS 部门累计工资
FROM 员工表
ORDER BY 部门, 入职日期;

执行结果

员工姓名 部门 工资 入职日期 部门累计工资
钱七 市场部 8500.00 2018-11-30 8500.00
赵六 市场部 7000.00 2020-07-08 15500.00
李四 研发部 9500.00 2019-03-15 9500.00
张三 研发部 8000.00 2020-01-10 17500.00
王五 研发部 8000.00 2021-05-20 25500.00
周九 人事部 7500.00 2021-09-05 7500.00
孙八 人事部 6500.00 2022-02-18 14000.00

场景3:替代笛卡尔积实现 Top N 查询

需求:找出每个部门绩效评分最高的前2名员工(绩效相同则按工资降序)。

传统写法:笛卡尔积 + COUNT()

-- 传统写法,缺点:笛卡尔积导致数据膨胀、性能极差、易出错
SELECT 
  e1.员工姓名,
  e1.部门,
  e1.绩效评分,
  e1.工资
FROM 员工表 e1
WHERE (
  SELECT COUNT(DISTINCT e2.绩效评分) 
  FROM 员工表 e2 
  WHERE e2.部门 = e1.部门 
    AND (e2.绩效评分 > e1.绩效评分 OR (e2.绩效评分 = e1.绩效评分 AND e2.工资 > e1.工资))
) < 2  -- 前2名
ORDER BY e1.部门, e1.绩效评分 DESC, e1.工资 DESC;

窗口函数写法:ROW_NUMBER()/RANK()

WITH 部门绩效排名 AS (
  SELECT 
    员工姓名,
    部门,
    绩效评分,
    工资,
    -- ROW_NUMBER():连续排名(即使值相同,排名也不同)
    -- RANK():跳跃排名(值相同则排名相同,后续排名跳过)
    -- DENSE_RANK():密集排名(值相同则排名相同,后续排名不跳过)
    ROW_NUMBER() OVER (
      PARTITION BY 部门 
      ORDER BY 绩效评分 DESC, 工资 DESC
    ) AS 排名
  FROM 员工表
)
SELECT 员工姓名, 部门, 绩效评分, 工资, 排名
FROM 部门绩效排名
WHERE 排名 <= 2  -- 筛选前2名
ORDER BY 部门, 排名;

执行结果

员工姓名 部门 绩效评分 工资 排名
钱七 市场部 5 8500.00 1
赵六 市场部 4 7000.00 2
李四 研发部 5 9500.00 1
张三 研发部 4 8000.00 2
周九 人事部 5 7500.00 1
孙八 人事部 4 6500.00 2

场景4:替代多表连接计算占比

需求:计算每个员工工资占其所在部门总工资的比例。

传统写法:GROUP BY + 连接

-- 传统写法,缺点:需要先聚合再连接、代码冗余
SELECT 
  e1.员工姓名,
  e1.部门,
  e1.工资,
  e2.部门总工资,
  ROUND(e1.工资 / e2.部门总工资 * 100, 2) AS `工资占比(%)`
FROM 员工表 e1
JOIN (
  SELECT 部门, SUM(工资) AS 部门总工资 
  FROM 员工表 
  GROUP BY 部门
) e2 ON e1.部门 = e2.部门
ORDER BY e1.部门, e1.工资 DESC;

窗口函数写法:SUM() 窗口聚合

SELECT 
  员工姓名,
  部门,
  工资,
  -- SUM(工资) OVER (PARTITION BY 部门):计算每个部门的总工资
  SUM(工资) OVER (PARTITION BY 部门) AS 部门总工资,
  -- 直接计算占比,无需连接
  ROUND(工资 / SUM(工资) OVER (PARTITION BY 部门) * 100, 2) AS `工资占比(%)`
FROM 员工表
ORDER BY 部门, 工资 DESC;

执行结果

员工姓名 部门 工资 部门总工资 工资占比(%)
钱七 市场部 8500.00 15500.00 54.84
赵六 市场部 7000.00 15500.00 45.16
李四 研发部 9500.00 25500.00 37.25
张三 研发部 8000.00 25500.00 31.37
王五 研发部 8000.00 25500.00 31.37
周九 人事部 7500.00 14000.00 53.57
孙八 人事部 6500.00 14000.00 46.43

场景5:替代嵌套子查询实现数据分桶(PostgreSQL)

需求:将每个部门的员工工资分为3档(高:前33%、中:中间34%、低:后33%)。

传统写法:CASE + 子查询

-- 传统写法,缺点:需要多次计算分位数、逻辑复杂
SELECT 
  e1.员工姓名,
  e1.部门,
  e1.工资,
  CASE 
    WHEN e1.工资 >= (SELECT PERCENTILE_CONT(0.67) WITHIN GROUP (ORDER BY 工资) FROM 员工表 e2 WHERE e2.部门 = e1.部门) THEN '高'
    WHEN e1.工资 >= (SELECT PERCENTILE_CONT(0.33) WITHIN GROUP (ORDER BY 工资) FROM 员工表 e2 WHERE e2.部门 = e1.部门) THEN '中'
    ELSE '低'
  END AS 工资档位
FROM 员工表 e1
ORDER BY e1.部门, e1.工资 DESC;

窗口函数写法:NTILE() 函数

WITH 工资分桶 AS (
  SELECT 
    员工姓名,
    部门,
    工资,
    NTILE(3) OVER (PARTITION BY 部门 ORDER BY 工资 DESC) AS 桶编号
  FROM 员工表
)
SELECT 
  员工姓名,
  部门,
  工资,
  桶编号,
  CASE 桶编号
    WHEN 1 THEN '高'
    WHEN 2 THEN '中'
    ELSE '低'
  END AS 工资档位
FROM 工资分桶
ORDER BY 部门, 工资 DESC;

执行结果

员工姓名 部门 工资 桶编号 工资档位
钱七 市场部 8500.00 1
赵六 市场部 7000.00 2
李四 研发部 9500.00 1
张三 研发部 8000.00 2
王五 研发部 8000.00 3
周九 人事部 7500.00 1
孙八 人事部 6500.00 2

总结

通过以上五个场景的对比,可以清晰地看到窗口函数的优势:逻辑简洁、易于维护,并且在大多数 数据库 引擎中拥有更好的执行性能。掌握 LAG()/LEAD()SUM() OVERROW_NUMBER()RANK()NTILE() 等核心窗口函数,能显著提升复杂 SQL 查询的编写效率和可读性。如果你在学习和实践 SQL 优化的过程中有更多心得,欢迎来 云栈社区 与大家一起交流探讨。




上一篇:谷歌TurboQuant技术详解:3bit压缩KV缓存,为大模型长上下文提速降本
下一篇:技术团队1对1沟通时,如何克服恐惧说出真实想法?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 18:51 , Processed in 0.740067 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表