<p>直接使用 MAX() - MIN() 即可计算每组极差,无需子查询或窗口函数;数值和日期类型支持,字符串等不支持;NULL 被自动忽略,全 NULL 组结果为 NULL。</p>

GROUP BY 后直接用 MAX() - MIN() 就行
SQL 里统计每组的极差(最大值减最小值),不需要子查询或窗口函数,MAX() 和 MIN() 可以在同一个 GROUP BY 查询里共存。数据库引擎会为每组分别计算这两个聚合值,再做减法。
常见错误是误以为要先查出每组的 MAX 和 MIN 再 JOIN,或者硬套 LAG()/LEAD() —— 完全没必要,徒增复杂度和性能开销。
- 只要字段类型支持减法运算(比如数值、日期),
MAX(col) - MIN(col)就合法 - 字符串、JSON、数组等类型不支持直接减,会报错,比如 PostgreSQL 报
operator does not exist: text - text - 如果字段含
NULL,MAX/MIN会自动忽略,不影响结果;但整组全是NULL时,结果为NULL
示例:
SELECT user_id, MAX(score) - MIN(score) AS diff FROM attempts GROUP BY user_id;
日期字段算时间跨度要注意单位
对 DATE 或 TIMESTAMP 字段用 MAX() - MIN(),结果不是“天数”就是“秒数”,具体取决于数据库方言,容易误读。
比如 MySQL 中 MAX(created_at) - MIN(created_at) 对 DATETIME 返回的是“格式化数字差”(如 20240101123000 - 20240101100000 = 23000),毫无业务意义;PostgreSQL 则返回 INTERVAL,可直接用 EXTRACT(EPOCH FROM ...) 转秒数。
- MySQL 推荐显式用
TIMESTAMPDIFF(DAY, MIN(ts), MAX(ts))或TIMESTAMPDIFF(SECOND, ...) - PostgreSQL 直接写
MAX(ts) - MIN(ts)得到INTERVAL,再用EXTRACT(day FROM ...)或EXTRACT(epoch FROM ...) - SQLite 用
julianday(MAX(ts)) - julianday(MIN(ts))得天数,注意它只认特定格式字符串
空组或单值组的 diff 是 0 还是 NULL?
这个问题实际取决于字段是否允许 NULL 以及该组数据分布。很多人以为单条记录的极差一定是 0,但其实不是:
- 如果该组只有一行,且字段非
NULL,则MAX = MIN = 值,差为 0 - 如果该组只有一行,但字段值是
NULL,则MAX和MIN都为NULL,NULL - NULL结果仍是NULL - 如果该组多行,但所有值都是
NULL,结果也是NULL
想统一成 0 可加 COALESCE(MAX(x) - MIN(x), 0),但得清楚这掩盖了“全空”的语义 —— 有些场景下,NULL 正是用来区分“无数据”和“有数据但差为 0”的。
性能上别被 MAX/MIN 的双重扫描骗了
看起来 MAX() 和 MIN() 各扫一遍,其实主流数据库(PostgreSQL、MySQL 8.0+、SQL Server)都会在一次扫描中同时维护两个聚合状态,执行计划里通常只显示一个 Aggregate 节点。
但有两个坑容易拖慢:
- 没给分组字段 + 聚合字段建联合索引,比如
GROUP BY category同时要MAX(price), MIN(price),最好有(category, price)索引 - 在大表上
GROUP BY前没过滤,比如忘了加WHERE status = 'active',导致聚合全表而非热数据子集
用 EXPLAIN 看执行计划时,重点确认是否出现 Using filesort 或临时表(Using temporary),那才是真瓶颈。
实际写的时候,最常被忽略的是字段类型兼容性和 NULL 语义——不是语法跑不通,而是结果跟预期对不上,尤其跨数据库迁移或对接 BI 工具时。










