导读:本期聚焦于甜甜圈创作的《SQL如何查找分组内的众数?窗口函数结合统计逻辑实战详解》,敬请观看详情。数据表中每个分组出现次数最多的值该怎么用SQL求出来?这是面试和实际业务里都常见的问题。由于主流数据库并没有内置的MODE聚合函数(Oracle除外),直接一条语句拿到分组众数并不容易。本文介绍几种可靠思路:利用GROUP BY先做频次统计再用窗口函数ROW_NUMBER或RANK筛选第一名,这是最通用的方案;也可以借助COUNT嵌套子查询按组取最大计数;在Oracle里还能直接用STATS_MODE函数。文中会给出建表数据和完整SQL,对比各方案在并列众数、NULL值、大数据量场景下的表现差异,并说明不同数据库的兼容性写法,帮助你在MySQL、PostgreSQL、SQL Server中都能落地。

在一批订单数据里找出每个客户最常购买的商品,在日志表里统计每台机器出现频率最高的错误码,这类需求本质上都是求分组内的众数。众数就是一组数据中出现次数最多的那个值。看起来简单,但很多人动手写SQL时才发现,主流数据库并没有提供现成的众数聚合函数,直接GROUP BY之后再怎么写就成了难题。本文从数据准备开始,一步步演示如何用分组统计配合窗口函数解决这个问题,并对比几种不同写法的适用场景。

SQL如何查找分组内的众数?窗口函数结合统计逻辑实战详解

准备测试数据:一张订单表

为了让后面的示例有据可依,先建一张简单的订单表,包含客户编号和购买的商品名称两列。我们的目标是找出每个客户买得最多的商品。

CREATE TABLE orders (
    customer_id INT,
    product     VARCHAR(50)
);

INSERT INTO orders VALUES
(1, '苹果'),
(1, '苹果'),
(1, '香蕉'),
(1, '苹果'),
(2, '香蕉'),
(2, '香蕉'),
(2, '橘子'),
(2, '橘子'),
(3, '牛奶'),
(3, '面包');

观察一下数据:客户1买了3次苹果、1次香蕉,众数是苹果;客户2香蕉和橘子各2次,属于并列众数;客户3牛奶和面包各1次,也是并列。这个特意设计的并列情况,正好可以用来检验不同SQL写法的行为差异,后面会看到有的写法只返回一个结果,有的会全部返回。

另外要注意,如果真实业务表中存在NULL值,比如product列为空,统计时需要先想清楚NULL算不算一个有效取值。多数场景下建议在WHERE中过滤掉NULL,避免统计结果被无意义的空值干扰。

基础思路:GROUP BY计数后再取最大值

最直观的思路分两步走。第一步,按客户和商品分组,数出每种商品被买了多少次;第二步,从每个客户的统计结果里挑出计数最大的那一行。先看第一步的SQL。

SELECT
    customer_id,
    product,
    COUNT(*) AS cnt
FROM orders
GROUP BY customer_id, product
ORDER BY customer_id, cnt DESC;

这条语句执行后,每个客户每种商品各占一行,cnt列就是出现次数。接下来问题转化为经典的每组最大值问题。传统做法是用相关子查询,判断当前行的计数是否等于该客户计数中的最大值。

SELECT t.customer_id, t.product, t.cnt
FROM (
    SELECT customer_id, product, COUNT(*) AS cnt
    FROM orders
    GROUP BY customer_id, product
) t
WHERE t.cnt = (
    SELECT MAX(cnt)
    FROM (
        SELECT customer_id, product, COUNT(*) AS cnt
        FROM orders
        GROUP BY customer_id, product
    ) m
    WHERE m.customer_id = t.customer_id
);

这个写法兼容性最好,几乎所有数据库都支持,但它对同一张表做了两次分组扫描,相关子查询还可能被反复执行,数据量大时性能不理想。而且写法冗长,逻辑分了好几层,可读性较差。这就引出了更优雅的窗口函数方案。

窗口函数方案:ROW_NUMBER与RANK的选择

窗口函数的核心价值在于,它可以在保留明细行的基础上为每行打上计算标记。我们先按客户和商品统计出频次,然后按客户分区,对频次倒序排列,给每行编号,最后取编号为1的行,众数就出来了。

SELECT customer_id, product, cnt
FROM (
    SELECT
        customer_id,
        product,
        cnt,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY cnt DESC
        ) AS rn
    FROM (
        SELECT customer_id, product, COUNT(*) AS cnt
        FROM orders
        GROUP BY customer_id, product
    ) s
) r
WHERE rn = 1;

ROW_NUMBER时,即使出现并列(比如客户2的香蕉和橘子都是2次),也只会返回其中一条,因为ROW_NUMBER保证编号唯一。这在业务上只要求一个代表值时很合适,但结果带有随机性,具体返回哪条取决于数据库内部的排序处理。

如果希望并列众数全部列出来,把ROW_NUMBER换成RANK即可。RANK对相同的排序值给出相同的名次,并列第一都会被标记为1,外层筛选rn = 1就能把所有并列众数都取到。改写后的关键片段如下。

RANK() OVER (
    PARTITION BY customer_id
    ORDER BY cnt DESC
) AS rn

对比一下子查询方案,窗口函数方案只做一次分组统计,ROW_NUMBER或RANK在分组结果集上计算,整体执行效率明显更高,而且逻辑集中在一层嵌套里,维护起来更轻松。这也是目前业界处理分组众数最主流的写法。需要提醒的是,窗口函数要求MySQL 8.0及以上版本,如果还在用MySQL 5.7,就只能退回子查询方案。

其他数据库的补充写法与注意事项

除了通用方案,部分数据库提供了更直接的特性。Oracle内置了STATS_MODE聚合函数,一条语句就能搞定。

SELECT customer_id, STATS_MODE(product) AS mode_product
FROM orders
GROUP BY customer_id;

PostgreSQL 9.4+则支持自定义聚合,官方文档中就有用C语言或PL/pgSQL实现mode函数的例子,社区里也有现成的pg_tools扩展可以用。SQL Server和MySQL没有原生支持,窗口函数方案仍是首选。

实际落地时还有几个细节值得留意。第一,并列众数的业务定义要提前和需求方确认,是要任意一个还是全部返回,这决定了用ROW_NUMBER还是RANK。第二,如果数据存在NULL,GROUP BY会把NULL单独算作一组,若不希望NULL参与众数竞争,需要在统计前过滤。第三,当分组数量特别多时,可以在频次子查询上针对分组列建立索引,减少分组扫描的开销。

总结一下,分组众数的通用解法就是频次统计加窗口排序筛选:GROUP BY负责算次数,窗口函数负责在组内排名,外层过滤排名第一的记录。掌握这个组合套路后,类似的最常见值、组内TopN问题也都能举一反三地解决。

SQL众数窗口函数GROUP BY分组统计修改时间:2026-09-07 02:16:33

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260907/51912.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。