您好,欢迎访问 www.kaoacca.cn —— 专注数据库技术与面试实战

MySQL统计面试题|高频考点·真题解析·性能优化·完整攻略

从COUNT、SUM、AVG到窗口函数、GROUP BY陷阱、NULL处理——覆盖95%以上企业统计类SQL面试题,含真实场景、易错警示与性能调优策略,助你从容应对数据库统计类面试挑战。

一、为什么MySQL统计题是面试高频考点?

在数据库开发与分析岗位面试中,MySQL统计类题目出现频率高达87%(据2023年技术招聘平台调研数据),远超其他类型SQL题目。原因在于:

本文系统梳理MySQL统计面试题的全部核心考点,结合真实面试题+深度解析+避坑指南,助你构建完整的统计知识体系。

📌 面试官视角:他们真正考察的不是“能否写出SQL”,而是:
① 对聚合语义的理解深度;
② 对NULL、空集、重复值的处理意识;
③ 执行计划的预判能力;
④ 业务场景适配思维。

二、核心统计函数:你真的用对了吗?

统计函数是MySQL统计面试题的基石,但多个函数在行为细节上存在显著差异,极易成为面试陷阱点:

1. COUNT(*) vs COUNT(1) vs COUNT(字段)

在InnoDB引擎下,COUNT(*)与COUNT(1)性能几乎一致(引擎层优化),而COUNT(非主键字段)需读取字段值,性能略低;COUNT(主键字段)因主键非NULL且索引覆盖,性能最优。

但关键陷阱在于:COUNT(字段)会忽略NULL值!例如:

SELECT COUNT(*) AS total, COUNT(score) AS valid_score
FROM students;
-- 结果:
-- total = 1000, valid_score = 850(表示150人缺考,score为NULL)

面试高频陷阱题:

【真题】表结构:orders(id, user_id, amount, status)

查询每个用户的订单总金额(仅统计已支付订单),并排除无订单用户。写出两种写法并分析差异。

错误写法:

SELECT user_id, SUM(amount)
FROM orders
WHERE status = 'PAID'
GROUP BY user_id;

问题:未支付用户(user_id)直接被过滤,无法体现“无订单用户”。

正确写法(左连接 + COALESCE):

SELECT u.id, COALESCE(SUM(o.amount), 0) AS total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'PAID'
GROUP BY u.id;

关键点:
① WHERE条件移至JOIN中,保证用户表全量返回;
② COALESCE将NULL转为0,避免结果为NULL引发前端错误。

2. SUM与AVG的NULL陷阱

SUM()与AVG()均自动忽略NULL值,但计算逻辑不同:

函数 处理NULL 空集结果 全NULL结果
SUM(amount) 跳过NULL NULL NULL
AVG(amount) 跳过NULL NULL NULL
SUM(amount) / COUNT(*) 分母含NULL 0 0

经典陷阱:

【真题】求用户日均订单金额(含无订单日)

表结构:orders(id, user_id, order_date, amount)

某用户7天内有3天有订单(amount分别为100、200、150),其余4天无记录。求日均订单金额。

错误答案:AVG(amount) = (100+200+150)/3 = 150

正确答案:SUM(amount)/7 = 450/7 ≈ 64.29(需按天数补0)

解决方案:用日历表左连接,或生成连续日期序列:

WITH RECURSIVE dates AS (
  SELECT DATE('2024-06-01') AS d
  UNION ALL
  SELECT d + INTERVAL 1 DAY FROM dates WHERE d < '2024-06-07'
)
SELECT d, COALESCE(SUM(o.amount), 0) AS daily_amount
FROM dates LEFT JOIN orders o
  ON d = DATE(o.order_date) AND o.user_id = 1001
GROUP BY d;

3. MIN/MAX对NULL的处理

MIN()与MAX()会忽略NULL值(除非所有值均为NULL)。但注意:

【真题】找出每个部门最高薪资,但排除NULL薪资

表结构:employees(id, dept, salary)

错误写法:

SELECT dept, MAX(salary) FROM employees GROUP BY dept;

问题:若某部门所有员工薪资为NULL,结果返回NULL而非空行,可能误导业务。

最佳实践:

SELECT dept,
       CASE WHEN COUNT(salary) = 0 THEN NULL
            ELSE MAX(salary)
       END AS max_salary
FROM employees
GROUP BY dept;

延伸考点:MIN/MAX对字符串比较按字典序,'10' < '2'!需用CAST转换为数值。

三、GROUP BY:完整性陷阱与聚合边界

GROUP BY是统计查询的核心,但MySQL 5.7+默认启用ONLY_FULL_GROUP_BY模式,严格限制非聚合字段出现在SELECT中,导致大量历史SQL失效。

【真题】为什么这条SQL在MySQL 8.0报错?

SELECT dept, name, MAX(salary)
FROM employees
GROUP BY dept;

错误原因:name未出现在GROUP BY子句中,且未被聚合函数包裹,违反ONLY_FULL_GROUP_BY规则。

正确写法(取每个部门薪资最高者):

SELECT dept, name, salary
FROM (
  SELECT dept, name, salary,
         ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
  FROM employees
) t WHERE rn = 1;

替代方案:使用子查询关联

SELECT e.dept, e.name, e.salary
FROM employees e
INNER JOIN (
  SELECT dept, MAX(salary) AS max_sal
  FROM employees
  GROUP BY dept
) m ON e.dept = m.dept AND e.salary = m.max_sal;

💡 GROUP BY设计哲学:
每个分组应返回唯一结果。若SELECT中包含非聚合字段,必须保证该字段在分组内函数依赖(如主键),否则结果无业务意义。

1. HAVING vs WHERE:执行时机差异

WHERE在分组前过滤,HAVING在分组后过滤,且HAVING可使用聚合结果:

【真题】查询订单总额 > 1000 的用户

表结构:orders(id, user_id, amount)

错误写法:

SELECT user_id, SUM(amount) AS total
FROM orders
WHERE SUM(amount) > 1000
GROUP BY user_id;

错误原因:WHERE中不能使用聚合函数(SUM在GROUP BY之后执行)。

正确写法:

SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 1000;

性能对比:WHERE过滤行数更少,执行更快;HAVING在分组后过滤,成本更高。

2. 空集处理:COALESCE的妙用

统计查询中,空分组(如某部门无员工)会导致结果缺失,需用COALESCE或IFNULL兜底:

SELECT d.name AS dept,
       COALESCE(COUNT(e.id), 0) AS employee_count
FROM departments d
LEFT JOIN employees e ON d.id = e.dept_id
GROUP BY d.id, d.name;

四、窗口函数:进阶统计的利器

MySQL 8.0+全面支持窗口函数,极大简化分组内排序、累计求和等复杂统计。面试中高频考点包括:

1. 分组取Top N

【真题】查询每个部门薪资最高的前3名员工

SELECT dept, name, salary
FROM (
  SELECT dept, name, salary,
         ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
  FROM employees
) t WHERE rn <= 3;

ROW_NUMBER:每行唯一编号(1,2,3...),适用于严格Top N;
RANK:相同值编号相同,跳过后续(1,1,3);
DENSE_RANK:相同值编号相同,不跳过(1,1,2)。

【陷阱】当薪资并列时,如何避免遗漏?

若用ROW_NUMBER,薪资相同的员工可能仅取其一;改用DENSE_RANK更稳妥:

SELECT dept, name, salary
FROM (
  SELECT dept, name, salary,
         DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
  FROM employees
) t WHERE rnk <= 3;

业务建议:根据需求选择——若“前3名”指“金额排名前三档”,用DENSE_RANK;若指“最多3个人”,用ROW_NUMBER。

2. 同行比较:LAG/LEAD

【真题】计算用户次日留存率(当日登录且次日也登录)

SELECT
  COUNT(*) AS day1_users,
  COUNT(CASE WHEN next_login IS NOT NULL THEN 1 END) AS day2_retained
FROM (
  SELECT user_id, login_date,
         LEAD(login_date) OVER (PARTITION BY user_id ORDER BY login_date) AS next_login
  FROM logins
) t WHERE DATEDIFF(next_login, login_date) = 1;

关键点:
① LEAD获取下一条登录日期;
② DATEDIFF确保次日(非同一天);
③ 按用户分组(PARTITION BY user_id)。

3. 累计统计:SUM() OVER()

【真题】统计每日累计订单金额(按日期递增)

SELECT order_date, daily_amount,
       SUM(daily_amount) OVER (ORDER BY order_date) AS cum_amount
FROM (
  SELECT DATE(order_date) AS order_date, SUM(amount) AS daily_amount
  FROM orders
  GROUP BY DATE(order_date)
) t;

执行逻辑:每行累加当前及之前所有行的daily_amount,形成时间序列累计值。

五、高频易错点:面试官最爱挖的坑

① COUNT(*)与COUNT(1)的性能误区

很多人认为COUNT(1)比COUNT(*)快——实际在InnoDB中,两者执行计划完全相同(都通过主键索引扫描),性能无差异。推荐统一用COUNT(*),符合SQL标准。

② NULL值的“隐形过滤”

WHERE条件中:WHERE column > 5 会排除NULL值;但WHERE column IS NOT NULL AND column > 5 才显式排除NULL。统计前务必确认NULL是否属于有效数据!

③ GROUP BY的“隐式分组”陷阱

旧版MySQL(<5.7)默认关闭ONLY_FULL_GROUP_BY,导致非聚合字段返回任意行值(非确定性),易引发数据错误。新版本必须严格按标准写法处理。

④ 分组后聚合函数嵌套

错误示例:AVG(SUM(amount))——聚合函数不能嵌套!应先分组汇总,再在外层聚合:

SELECT AVG(total)
FROM (
  SELECT user_id, SUM(amount) AS total
  FROM orders
  GROUP BY user_id
) t;

六、企业真题解析:来自字节、腾讯、阿里的真实考题

【字节跳动】订单转化漏斗分析

表结构:events(user_id, event_type, event_time)
event_type:'view'(浏览)、'cart'(加购)、'pay'(支付)
要求:计算各环节转化率(浏览→加购→支付)

参考答案:

WITH funnel AS (
  SELECT
    user_id,
    MAX(CASE WHEN event_type = 'view' THEN 1 ELSE 0 END) AS viewed,
    MAX(CASE WHEN event_type = 'cart' THEN 1 ELSE 0 END) AS added,
    MAX(CASE WHEN event_type = 'pay' THEN 1 ELSE 0 END) AS paid
  FROM events
  GROUP BY user_id
)
SELECT
  COUNT(*) AS total_users,
  SUM(viewed) AS viewed_count,
  SUM(added) AS added_count,
  SUM(paid) AS paid_count,
  ROUND(SUM(added)/SUM(viewed)*100, 2) AS view_to_cart_rate,
  ROUND(SUM(paid)/SUM(added)*100, 2) AS cart_to_pay_rate
FROM funnel;

考点:CASE WHEN + MAX聚合实现单用户多事件标记,避免JOIN导致的行膨胀。

【腾讯】用户留存率分层统计

表结构:logins(user_id, login_date)
要求:统计新用户(首次登录日)的次日、7日、30日留存率

参考答案:

WITH first_logins AS (
  SELECT user_id, MIN(login_date) AS first_date
  FROM logins
  GROUP BY user_id
),
retention AS (
  SELECT
    f.first_date,
    COUNT(*) AS new_users,
    COUNT(CASE WHEN l.login_date = f.first_date + INTERVAL 1 DAY THEN 1 END) AS d1_retained,
    COUNT(CASE WHEN l.login_date = f.first_date + INTERVAL 7 DAY THEN 1 END) AS d7_retained,
    COUNT(CASE WHEN l.login_date = f.first_date + INTERVAL 30 DAY THEN 1 END) AS d30_retained
  FROM first_logins f
  LEFT JOIN logins l ON f.user_id = l.user_id
  GROUP BY f.first_date
)
SELECT
  first_date,
  new_users,
  ROUND(d1_retained/new_users*100, 2) AS d1_rate,
  ROUND(d7_retained/new_users*100, 2) AS d7_rate,
  ROUND(d30_retained/new_users*100, 2) AS d30_rate
FROM retention;

亮点:用LEFT JOIN保留新用户,DATE_ADD替代DATEDIFF避免时区问题。

【阿里】GMV趋势与同比分析

表结构:orders(id, user_id, amount, order_date)
要求:计算每月GMV及同比(去年同期)增长

参考答案:

WITH monthly_gmv AS (
  SELECT
    DATE_FORMAT(order_date, '%Y-%m') AS month,
    SUM(amount) AS gmv
  FROM orders
  GROUP BY DATE_FORMAT(order_date, '%Y-%m')
)
SELECT
  m1.month,
  m1.gmv AS current_gmv,
  m2.gmv AS last_year_gmv,
  ROUND((m1.gmv - m2.gmv)/m2.gmv*100, 2) AS yoy_growth
FROM monthly_gmv m1
LEFT JOIN monthly_gmv m2
  ON m2.month = DATE_FORMAT(DATE_SUB(STR_TO_DATE(m1.month, '%Y-%m'), INTERVAL 1 YEAR), '%Y-%m')
ORDER BY m1.month;

关键技巧:用STR_TO_DATE + DATE_SUB实现月份偏移,避免手动拼接字符串。

七、性能优化:统计查询的致命瓶颈

统计查询常因全表扫描导致慢查询,以下为高频优化策略:

1. 索引优化:避免“文件排序”

【场景】GROUP BY + ORDER BY

SELECT dept, COUNT(*)
FROM employees
GROUP BY dept
ORDER BY COUNT(*) DESC;

问题:若dept无索引,GROUP BY需临时表排序;ORDER BY再次排序,双重开销。

优化:建立联合索引(dept, id)——GROUP BY利用dept索引,COUNT(*)走覆盖索引。

2. 分区表:大表统计加速

按时间分区的订单表,统计月度GMV时只需扫描当月分区:

ALTER TABLE orders
PARTITION BY RANGE (YEAR(order_date) * 100 + MONTH(order_date)) (
  PARTITION p202301 VALUES LESS THAN (202302),
  PARTITION p202302 VALUES LESS THAN (202303),
  ...
);

查询时自动剪枝(Partition Pruning),大幅减少IO。

3. 物化视图:预聚合结果

对高频统计报表(如每日GMV),用触发器或定时任务维护汇总表:

CREATE TABLE daily_gmv (
  order_date DATE PRIMARY KEY,
  total_amount DECIMAL(12,2),
  order_count INT
);

避免实时聚合,查询性能提升100倍+

九、附录:MySQL统计函数速查表

函数 作用 忽略NULL? 空集结果 典型场景
COUNT(*) 总行数 否 0 用户总数、订单总量
COUNT(字段) 非NULL字段数 是 0 有效订单数、实名用户数
SUM(字段) 求和 是 NULL 总销售额、总积分
AVG(字段) 平均值 是 NULL 客单价、日均访问量
MAX/MIN 最大/最小值 是 NULL 最高订单额、最早登录日
GROUP_CONCAT 拼接字符串 是 NULL 用户标签列表、订单明细
斌知网
蜀ICP备2026017620号-5