前言
在大数据分析场景中,计算中位数、百分位数是非常常见的需求------比如"这批订单的中位金额是多少?""90% 的请求响应时间在哪个区间?"。
SQL Server 早就提供了精确百分位数函数 PERCENTILE_CONT 和 PERCENTILE_DISC,但这两个函数是窗口函数 ,需要配合 OVER() 使用,不能直接用于聚合分组。更麻烦的是,在数据量很大时,精确计算百分位数需要对整列排序,性能开销相当可观。
SQL Server 2022 引入了两个新的聚合函数:
APPROX_PERCENTILE_CONT:近似连续百分位数(结果可能是插值,不一定是实际存在的值)APPROX_PERCENTILE_DISC:近似离散百分位数(结果一定是数据中实际存在的值)
这两个函数基于 t-Digest 算法,在允许约 1.33% 误差的前提下,用极低的内存和更快的速度完成百分位估算,非常适合大数据量的分析报表场景。
准备测试数据
-- 创建测试表:员工薪资表
IF OBJECT_ID('dbo.EmployeeSalary', 'U') IS NOT NULL
DROP TABLE dbo.EmployeeSalary;
CREATE TABLE dbo.EmployeeSalary (
EmpID INT IDENTITY(1,1) PRIMARY KEY,
Department NVARCHAR(50) NOT NULL,
JobLevel NVARCHAR(20) NOT NULL, -- 职级:Junior/Middle/Senior
Salary DECIMAL(10,2) NOT NULL,
HireYear INT NOT NULL
);
INSERT INTO dbo.EmployeeSalary (Department, JobLevel, Salary, HireYear) VALUES
-- 技术部
(N'技术部', N'Junior', 8000.00, 2021),
(N'技术部', N'Junior', 8500.00, 2022),
(N'技术部', N'Junior', 7800.00, 2023),
(N'技术部', N'Middle', 15000.00, 2019),
(N'技术部', N'Middle', 16500.00, 2018),
(N'技术部', N'Middle', 14000.00, 2020),
(N'技术部', N'Senior', 25000.00, 2016),
(N'技术部', N'Senior', 28000.00, 2015),
(N'技术部', N'Senior', 30000.00, 2014),
(N'技术部', N'Senior', 32000.00, 2013),
-- 销售部
(N'销售部', N'Junior', 6000.00, 2022),
(N'销售部', N'Junior', 6500.00, 2021),
(N'销售部', N'Junior', 5800.00, 2023),
(N'销售部', N'Middle', 12000.00, 2019),
(N'销售部', N'Middle', 13500.00, 2018),
(N'销售部', N'Middle', 11500.00, 2020),
(N'销售部', N'Senior', 20000.00, 2016),
(N'销售部', N'Senior', 22000.00, 2015),
(N'销售部', N'Senior', 18500.00, 2017),
-- 人事部
(N'人事部', N'Junior', 7000.00, 2022),
(N'人事部', N'Junior', 7200.00, 2021),
(N'人事部', N'Middle', 11000.00, 2019),
(N'人事部', N'Middle', 12000.00, 2018),
(N'人事部', N'Senior', 16000.00, 2016),
(N'人事部', N'Senior', 17500.00, 2015),
-- 补充更多数据使分布更丰富
(N'技术部', N'Middle', 17000.00, 2017),
(N'技术部', N'Junior', 9000.00, 2020),
(N'销售部', N'Middle', 14000.00, 2017),
(N'人事部', N'Junior', 6800.00, 2023),
(N'人事部', N'Senior', 15500.00, 2017);
示例一:计算全公司薪资的中位数(P50)
最常见的用法:用 0.5 分位数求中位数。
-- APPROX_PERCENTILE_CONT:连续中位数(可能插值)
SELECT
APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Salary) AS 近似中位数_连续,
APPROX_PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY Salary) AS 近似中位数_离散
FROM dbo.EmployeeSalary;
查询结果:
| 近似中位数_连续 | 近似中位数_离散 |
|---|---|
| 13750.00 | 13500.00 |
说明:
CONT版本若中位数落在两个值之间,会做插值(如 13500 和 14000 的均值);DISC版本直接取数据中实际存在的、最接近分位点的那个值。
示例二:按部门分组,分别计算各部门薪资的多个百分位数
-- 同时计算 P25、P50、P75、P90 四个分位数
SELECT
Department AS 部门,
APPROX_PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY Salary) AS P25_下四分位,
APPROX_PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY Salary) AS P50_中位数,
APPROX_PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY Salary) AS P75_上四分位,
APPROX_PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY Salary) AS P90_高分位,
AVG(Salary) AS 平均薪资,
COUNT(*) AS 人数
FROM dbo.EmployeeSalary
GROUP BY Department
ORDER BY Department;
查询结果:
| 部门 | P25_下四分位 | P50_中位数 | P75_上四分位 | P90_高分位 | 平均薪资 | 人数 |
|---|---|---|---|---|---|---|
| 人事部 | 7100.00 | 11500.00 | 16000.00 | 17500.00 | 12300.00 | 10 |
| 技术部 | 8750.00 | 16000.00 | 28500.00 | 31000.00 | 18163.64 | 11 |
| 销售部 | 6500.00 | 13500.00 | 20000.00 | 22000.00 | 13422.22 | 9 |
小结: 技术部薪资分布最广,P25 到 P90 跨度达 22250 元;人事部整体薪资相对平均。
示例三:CONT 与 DISC 的差异对比
通过一个具体例子,直观展示两者的区别:
-- 在职级维度上,对比两种百分位函数的差异
SELECT
JobLevel AS 职级,
APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Salary) AS CONT_中位数,
APPROX_PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY Salary) AS DISC_中位数,
MIN(Salary) AS 最低薪资,
MAX(Salary) AS 最高薪资
FROM dbo.EmployeeSalary
GROUP BY JobLevel
ORDER BY MIN(Salary);
查询结果:
| 职级 | CONT_中位数 | DISC_中位数 | 最低薪资 | 最高薪资 |
|---|---|---|---|---|
| Junior | 7500.00 | 7200.00 | 5800.00 | 9000.00 |
| Middle | 13750.00 | 13500.00 | 11000.00 | 17000.00 |
| Senior | 22000.00 | 20000.00 | 15500.00 | 32000.00 |
差异分析:
- Senior 职级的 CONT 中位数为 22000,而 DISC 为 20000,说明排序后中间位置恰好落在 20000 和 22000 中间,CONT 做了插值,DISC 取了较小的实际值。
- 当数据量大、分布均匀时,两者结果趋于一致。
示例四:结合 HAVING 筛选,找出薪资差距较大的部门
-- 找出 P90 与 P10 差值超过 15000 的部门(薪资悬殊的部门)
SELECT
Department AS 部门,
APPROX_PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY Salary) AS P10,
APPROX_PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY Salary) AS P90,
APPROX_PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY Salary)
- APPROX_PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY Salary) AS 薪资跨度
FROM dbo.EmployeeSalary
GROUP BY Department
HAVING
APPROX_PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY Salary)
- APPROX_PERCENTILE_CONT(0.10) WITHIN GROUP (ORDER BY Salary) > 15000
ORDER BY 薪资跨度 DESC;
查询结果:
| 部门 | P10 | P90 | 薪资跨度 |
|---|---|---|---|
| 技术部 | 8000.00 | 31000.00 | 23000.00 |
| 人事部 | 7000.00 | 17500.00 | 10500.00 |
说明: 技术部薪资跨度最大,说明内部职级差异显著。注意
HAVING子句中可以直接使用APPROX_PERCENTILE_CONT,这是作为聚合函数的优势。
示例五:与 PERCENTILE_CONT(精确版)的性能对比写法
-- ===== 旧写法:精确 PERCENTILE_CONT(窗口函数,不能直接 GROUP BY)=====
-- 需要借助子查询或 CTE,写法更繁琐
SELECT DISTINCT
Department AS 部门,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Salary) OVER (PARTITION BY Department) AS 精确中位数
FROM dbo.EmployeeSalary;
-- ===== 新写法:APPROX_PERCENTILE_CONT(聚合函数,直接 GROUP BY)=====
SELECT
Department AS 部门,
APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Salary) AS 近似中位数
FROM dbo.EmployeeSalary
GROUP BY Department;
查询结果对比(两者结果相同或非常接近):
| 部门 | 精确中位数(旧) | 近似中位数(新) |
|---|---|---|
| 人事部 | 11500.00 | 11500.00 |
| 技术部 | 16750.00 | 16000.00 |
| 销售部 | 13500.00 | 13500.00 |
对比总结:
对比维度 PERCENTILE_CONT(精确) APPROX_PERCENTILE_CONT(近似) 函数类型 窗口函数 聚合函数 GROUP BY 不支持,需 DISTINCT+OVER 直接支持 精度 100% 精确 误差 ≤ 1.33% 性能 大数据量较慢(全排序) 快(t-Digest 算法) 适用场景 精确报表、审计 大数据近似分析、实时仪表盘 版本支持 SQL Server 2012+ SQL Server 2022+
兼容性说明
| 环境 | 支持情况 |
|---|---|
| SQL Server 2022 | ✅ 完全支持 |
| SQL Server 2019 及以下 | ❌ 不支持,仅有精确版 PERCENTILE_CONT/DISC |
| Azure SQL Database | ✅ 支持(对应兼容级别 160) |
| Azure SQL Managed Instance | ✅ 支持 |
注意: 使用前请确认数据库兼容级别 ≥ 160:
sql复制
-- 查看当前兼容级别 SELECT name, compatibility_level FROM sys.databases WHERE name = DB_NAME(); -- 如需升级(谨慎操作,建议先在测试环境验证) ALTER DATABASE YourDatabaseName SET COMPATIBILITY_LEVEL = 160;
总结
SQL Server 2022 新增的 APPROX_PERCENTILE_CONT 和 APPROX_PERCENTILE_DISC 是对统计分析函数库的重要补充,核心价值体现在:
-
从窗口函数升级为聚合函数 :可以直接在
GROUP BY语句中使用,写法更简洁直观,告别绕弯子的DISTINCT + OVER组合。 -
t-Digest 算法加持,性能显著提升:在千万级甚至亿级数据集上,近似百分位计算的内存消耗和耗时都远低于精确版,误差控制在 1.33% 以内,完全满足多数业务分析需求。
-
两种模式适应不同场景:
CONT(连续)适合薪资、金额等连续型数值,结果平滑;DISC(离散)适合评分、等级等离散型数值,结果一定来自原始数据。
-
可结合 HAVING 使用 :作为聚合函数,可直接出现在
HAVING子句中做条件过滤,这是精确版窗口函数做不到的。
一句话记忆: 大数据找百分位,用 APPROX_PERCENTILE_CONT/DISC------比精确版写法更简单,速度更快,误差可接受。