作为数据分析师或后端开发人员,你可能经常需要处理累计数据的统计需求,比如计算累计销售额、统计网站用户的累计访问量,或者分析每个月的累计财务报表。这种随着数据行递增而不断累加的计算,就是我们常说的“运行总计”或“累计求和”。
在 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;
SALARY
DEPT_ID
——–
———————
34000
UI DEVELOPERS
33000
BACKEND DEVELOPERS
36000
BACKEND DEVELOPERS
36000
UI DEVELOPERS
37000
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 列求和。
输出结果:
SALARY
DEPT_ID
——–
———————
34000
UI DEVELOPERS
33000
BACKEND DEVELOPERS
36000
BACKEND DEVELOPERS
36000
UI DEVELOPERS
37000
UI DEVELOPERS
实际应用与性能提示:
虽然这种方法逻辑清晰,但在处理大数据集时性能较差。因为如果表中有 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 进行排序并累计。
输出结果:
SALARY
DEPTID
——–
———————
33000
BACKEND DEVELOPERS
36000
BACKEND DEVELOPERS
34000
UI DEVELOPERS
36000
UI DEVELOPERS
37000
UI DEVELOPERS
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 的表达能力远超你的想象。