一、为什么MySQL统计题是面试高频考点?
在数据库开发与分析岗位面试中,MySQL统计类题目出现频率高达87%(据2023年技术招聘平台调研数据),远超其他类型SQL题目。原因在于:
- 业务强相关:企业决策高度依赖数据统计(如用户转化率、订单汇总、活跃度分析);
- 易出陷阱:COUNT(*) vs COUNT(1)、NULL处理、GROUP BY完整性等细节易被忽视;
- 性能敏感:不当统计写法可导致全表扫描,直接影响线上服务稳定性;
- 能力分层:初级开发者常陷于语法,高级工程师注重执行计划与优化。
本文系统梳理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 | 用户标签列表、订单明细 |