SQL 深入解析:如何高效计算运行总计

作为数据分析师或后端开发人员,你可能经常需要处理累计数据的统计需求,比如计算累计销售额、统计网站用户的累计访问量,或者分析每个月的累计财务报表。这种随着数据行递增而不断累加的计算,就是我们常说的“运行总计”或“累计求和”。

在 2026 年的今天,虽然我们的开发工具包中已经加入了 AI 辅助编程和各类自动化框架,但 SQL 依然是数据处理的基石。在这篇文章中,我们将深入探讨如何在 SQL 中灵活地计算运行总计,并结合现代开发工作流,分享我们在实际项目中的性能调优经验和避坑指南。我们将以 Microsoft SQL Server 为核心环境,通过从基础到高级的多种实现方式,带大家一步步理解这一技术的核心原理。

准备工作:搭建实验环境

在开始编写查询之前,我们需要一个可用的数据集。为了模拟真实场景,我们将构建一个名为“geeks”的数据库,并在其中创建一个“department”表,用于存储员工的薪资信息。我们将通过这个数据集来演示不同的累计求和技巧。

首先,让我们创建并切换到我们的数据库:

-- 创建名为 geeks 的数据库
CREATE DATABASE geeks;

-- 切换当前上下文到该数据库
USE geeks;

接下来,我们需要定义表结构。这个表将包含员工 ID、姓名、所属部门以及薪资:

-- 创建部门员工表
CREATE TABLE department (
  id      INTEGER PRIMARY KEY, -- 员工ID,主键
  salary  INTEGER NOT NULL,    -- 薪资
  name    TEXT NOT NULL,       -- 姓名
  dept_id TEXT NOT NULL        -- 部门ID
);

为了让演示更加直观,让我们向表中插入一些具有代表性的数据。请注意,这里的数据混合了不同的部门,以便后续演示“分组累计”的效果:

-- 插入测试数据
INSERT INTO department VALUES (1, 34000, ‘ANURAG‘, ‘UI DEVELOPERS‘);
INSERT INTO department VALUES (2, 33000, ‘harsh‘, ‘BACKEND DEVELOPERS‘);
INSERT INTO department VALUES (3, 36000, ‘SUMIT‘, ‘BACKEND DEVELOPERS‘);
INSERT INTO department VALUES (4, 36000, ‘RUHI‘, ‘UI DEVELOPERS‘);
INSERT INTO department VALUES (5, 37000, ‘KAE‘, ‘UI DEVELOPERS‘);

数据插入成功后,我们可以先预览一下原始数据:

SELECT * FROM department;
ID

SALARY

NAME

DEPT_ID

—-

——–

——-

———————

1

34000

ANURAG

UI DEVELOPERS

2

33000

harsh

BACKEND DEVELOPERS

3

36000

SUMIT

BACKEND DEVELOPERS

4

36000

RUHI

UI DEVELOPERS

5

37000

KAE

UI DEVELOPERS### 方法一:使用相关子查询(历史视角)

在 SQL 的早期版本中,或者在某些不支持现代窗口函数的数据库里,我们通常使用“相关子查询”来解决这个问题。虽然这种方法现在看起来有些繁琐,甚至在我们现代 AI 辅助的编程环境中会被标记为“低效代码”,但理解它对于掌握 SQL 的执行逻辑非常有帮助。

核心逻辑:

对于主查询中的每一行数据(我们称之为 T1),子查询都会去扫描一次表(我们称之为 T2),计算所有 ID 小于或等于当前 T1.ID 的行的薪资总和。

让我们看看具体的代码实现:

SELECT *,
       (
         -- 子查询:计算当前行及之前所有行的总和
         -- 注意:这在数据量大时会导致性能灾难
         SELECT SUM(T2.[SALARY])
         FROM [department] AS T2
         WHERE T2.[ID] <= T1.[ID]
       ) AS [Running Total]
FROM [department] AS T1;

代码解析:

  • 外层查询:遍历表中的每一行(别名 T1)。
  • 内层查询:针对每一个 T1,去查找所有 ID 小于等于 T1.ID 的记录。
  • 累加计算:将符合条件的记录的 Salary 列求和。

输出结果:

ID

SALARY

NAME

DEPT_ID

Running Total —-

——–

——-

———————

————— 1

34000

ANURAG

UI DEVELOPERS

34000 2

33000

harsh

BACKEND DEVELOPERS

67000 3

36000

SUMIT

BACKEND DEVELOPERS

103000 4

36000

RUHI

UI DEVELOPERS

139000 5

37000

KAE

UI DEVELOPERS

176000

实际应用与性能提示:

虽然这种方法逻辑清晰,但在处理大数据集时性能较差。因为如果表中有 N 行数据,数据库大概需要执行 N*N/2 次比较操作。在我们的实际工作中,如果遇到遗留代码库中包含这种写法,通常会将其视为重构的首选目标。

方法二:使用 OVER 子句(现代标准)

从 SQL Server 2012 开始,引入了更强大的“窗口函数”概念。使用 INLINECODE2089ecbe 函数配合 INLINECODE8dfe13a9 子句,是计算运行总计最优雅、性能最高的方法。这种方法避免了自引用的重复扫描,查询优化器可以更高效地处理数据。

核心语法:
SUM(列名) OVER (ORDER BY 排序列)

让我们用这种方法重写上面的查询:

SELECT *,
       -- 使用窗口函数计算累计值
       -- 这种写法不仅简洁,而且通常能利用数据库的优化器
       SUM([SALARY]) OVER (
           ORDER BY [ID] -- 按 ID 顺序确定“累计”的方向
       ) AS [Running Total]
FROM department;

为什么这样更好?

  • 代码可读性高:一眼就能看出我们是基于 ID 的顺序进行求和。
  • 性能优越:数据库引擎通常只需要对数据进行一次扫描即可完成计算,而不是像相关子查询那样反复扫描。

方法三:深入 PARTITION BY(分组累计)

在实际业务中,我们往往不需要计算全局的累计值,而是需要计算分组内的累计值。例如:“每个部门内部的薪资累计总和”。这时候,我们就需要用到 PARTITION BY 子句。

场景描述:

我们希望看到每个员工在其所在部门内的薪资排名以及累计支出。这意味着,当一个部门的统计结束后,下一个部门的累计值会从零重新开始。

代码实现:

SELECT *,
       -- 结合 PARTITION BY 和 ORDER BY
       SUM([SALARY]) OVER (
           PARTITION BY DEPT_ID -- 先按部门分组
           ORDER BY Id          -- 再在组内按 ID 排序
       ) AS [Running Total]
FROM department;

深度解析:

  • PARTITION BY DEPT_ID:SQL 引擎会先将数据切成不同的“窗口”(即不同的部门)。
  • ORDER BY Id:在每一个独立的窗口内,数据按照 ID 进行排序并累计。

输出结果:

ID

SALARY

NAME

DEPTID

RunningTotal —-

——–

——-

———————

————— 2

33000

harsh

BACKEND DEVELOPERS

33000 3

36000

SUMIT

BACKEND DEVELOPERS

69000 1

34000

ANURAG

UI DEVELOPERS

34000 4

36000

RUHI

UI DEVELOPERS

70000 5

37000

KAE

UI DEVELOPERS

107000

2026 技术洞察:企业级开发的窗口函数最佳实践

在我们最近处理的一个大型金融 SaaS 平台的重构项目中,我们深刻体会到仅仅“会用”窗口函数是不够的。在 2026 年,随着数据量的爆炸式增长和对实时性要求的提高,我们需要从更高的维度审视 SQL 开发。

#### 1. 性能优化的微观视角:内存 vs 磁盘

让我们思考一下这个场景:当你在包含数亿行记录的分区表上运行上述 SUM() OVER() 语句时,会发生什么?

如果你的 INLINECODE97be3013 列(即上述例子中的 INLINECODE05a6c87b 或 DEPT_ID)没有建立完善的索引,SQL Server 的查询优化器将不得不在内存中执行“Sort(排序)”操作。如果数据量超过了内存授予的阈值,数据将会被溢出到磁盘。这也就是我们常说的“Spill to Disk”,它是性能杀手。

我们的建议:

在生产环境中,务必确保在 INLINECODEb471844e 和 INLINECODEe6e8b6a0 涉及的列上建立覆盖索引。例如:

-- 企业级索引策略示例
CREATE INDEX IX_Department_Salary_Id 
ON department(DEPT_ID, ID) 
INCLUDE (SALARY);

通过这样的索引,数据库引擎可以直接按照索引的物理顺序读取数据,完全避免了内存排序操作,使得累计求和的计算速度接近于线性扫描。

#### 2. 确定性与数据一致性陷阱

在使用 OVER 子句时,我们经常会遇到一个令人头疼的问题:非确定性排序

假设我们这样写:

SUM(SALARY) OVER (ORDER BY SALARY) -- ⚠️ 潜在风险

如果多名员工拥有相同的薪资(例如数据中的 36000),数据库引擎并没有内置的逻辑来决定谁排在前面,谁排在后面。这种不确定性在某些业务场景下(如计算库存消耗或资金流水)可能会导致对账失败。

我们的实战经验:

在编写任何涉及 ORDER BY 的窗口函数时,永远添加一个唯一的列(通常是主键 ID)作为“决胜局”。这是我们团队 Code Review 中的一条铁律:

-- 最佳实践:添加唯一键以确保排序稳定
SUM(SALARY) OVER (
    ORDER BY SALARY, ID -- 先按薪资,相同薪资则按 ID 排序
) 

进阶技巧:时间窗口与移动平均

现代数据分析不仅仅是“从开始到现在”的累计,更多时候我们需要关注“最近 N 天”或“当前行及前一行”的动态指标。这就是移动平均的核心。

我们可以通过 INLINECODE32e5b5fd 或 INLINECODEb5fb066c 来精确控制窗口框架。

示例:计算3天移动平均值

想象一下,我们在处理 IoT 传感器数据或股票 K 线,我们需要平滑掉瞬间的尖刺。

SELECT 
    name,
    salary,
    SUM(salary) OVER (
        ORDER BY id 
        ROWS BETWEEN 1 PRECEDING AND CURRENT ROW -- 当前行 + 前一行 (2行移动)
        -- 如果是 3 天移动平均,可以使用: ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ) AS [Moving Sum],
    AVG(salary) OVER (
        ORDER BY id 
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 当前行及前两行
    ) AS [3_Point_Moving_Avg]
FROM department;

ROWS vs RANGE 的区别:

这是很多高级面试中会考察的点。

  • ROWS:基于物理行的偏移量。例如 1 PRECEDING 就是物理上的上一行。它非常适合处理像时间序列这样连续、均匀分布的数据。
  • RANGE:基于逻辑值的偏移。例如,如果 INLINECODEa4d4eab0 的是日期,INLINECODE800226e2 意味着“昨天”,即使“昨天”没有数据(或者有多行数据),它也能正确处理逻辑上的时间间隔。在 2026 年的现代数据栈中,处理稀疏数据时,RANGE 显得尤为重要。

赋能未来开发:AI 与 SQL 的协同

现在,让我们聊聊 2026 年开发者的工作流变化。你可能会问:“有了像 Copilot 或 Cursor 这样的 AI 工具,我还需要深入理解这些细节吗?”

我们的答案是:你需要比以往任何时候都更深刻地理解原理,才能驾驭 AI。

在“氛围编程”时代,你作为 Architect 或 Senior Engineer,你的角色不再是逐字逐句敲出 SQL,而是:

  • Prompt Engineering for SQL:当你要求 AI “帮我计算每个部门的累计薪资”时,AI 可能会给出基于 INLINECODEbc5b210e 的方案,也可能给出基于 INLINECODE587c8301 的方案。只有你懂得其中的性能差异,你才能判断哪个是“Gold Standard”。
  • Sanity Checking (完备性检查):AI 生成的代码往往忽略了边界情况。例如,AI 可能会忘记处理 INLINECODEff3fabb7 值,或者忽略了 INLINECODE7e8ccdb7 的确定性。你需要成为那个把关人,确保生成的代码在数百万并发下依然稳定。
  • Iterative Refinement (迭代优化):你可以这样与 AI 协作:

You*: "Write a running total query for this table."
AI: Generates basic OVER clause.*
You*: "Now, optimize it for a table with 50 million rows. Suggest an index strategy."
AI: Provides index creation DDL and explains the execution plan.*

总结

在这篇文章中,我们不仅探索了在 SQL Server 中计算运行总计的历史方法与现代标准,更深入探讨了在企业级应用中如何保证高性能和数据一致性。

  • 基础回顾:我们废弃了低效的相关子查询,拥抱了窗口函数。
  • 核心技能:掌握了 PARTITION BY 实现分组累计,这是业务报表开发的基石。
  • 高阶实战:通过 INLINECODEc6566715 实现了移动平均,理解了 INLINECODE61952657 与 RANGE 在处理稀疏数据时的区别。
  • 未来视角:在 AI 辅助编程的时代,深入理解 SQL 的执行计划和索引策略,是我们与 AI 高效协作、构建高性能系统的核心能力。

掌握这些技巧后,你将能够在报表生成、数据分析和仪表盘开发中应对各种复杂的累计统计需求。我们建议你在自己的环境中尝试运行这些代码,观察不同的 INLINECODEff853ad8 和 INLINECODEc274aaa8 组合如何改变结果集。实践是掌握 SQL 运行之美的最佳途径。

希望这篇指南对你有所帮助!如果你在实际工作中遇到了更复杂的数据统计场景,不妨尝试组合使用这些窗口函数,你会发现 SQL 的表达能力远超你的想象。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/40790.html
点赞
0.00 平均评分 (0% 分数) - 0