SQL常见面试题总结(5)
本篇围绕四个实战高频主题展开:NULL 的统计语义、CASE WHEN 条件表达式、精确模式匹配与分页、文本拆分与归一化。它们单看都是小知识点,组合起来就是业务 SQL 的日常。
考察地图
- 空值处理:未完成数与完成率的多种写法、平均值统计中的 NULL 兜底。考察对
COUNT不计 NULL、三值逻辑的理解。 - 条件统计与筛选:得分等级占比、跨表最近活跃筛选、正则匹配。考察
CASE WHEN与LIKE/REGEXP的选型。 - 分页:
LIMIT offset, size取第 N 页。考察深分页意识。 - 文本函数:串列字段拆分、大小写归并。考察
SUBSTRING_INDEX、UPPER配合自连接。 - 建议自行查阅:「根据指定记录是否存在输出不同结果」(CTE 加
UNION ALL做分支输出,属困难题)——掌握本篇后可自行推演。
空值处理
统计未完成数和未完成率有哪几种写法?
结论:三种等价思路——COUNT(*) 减 COUNT(列)、COUNT(CASE WHEN ... THEN 1 END)、SUM(条件表达式),核心都是利用「聚合函数忽略 NULL」或「布尔表达式转 0/1」。
原理:COUNT(*) 数行数,COUNT(submit_time) 只数该列非 NULL 的行,差值即未完成数,这是最简洁的写法。CASE WHEN 写法显式构造「满足条件为 1、否则为 NULL」再计数,语义最直白、可移植性最好。SUM(submit_time IS NULL) 则利用 MySQL 把布尔结果当 1/0 处理的特性:
SELECT exam_id, |
延伸:「有未完成记录才输出」用 HAVING 在聚合后过滤。这套「条件计数」组合拳——SUM(条件)、COUNT(IF(条件,1,NULL))——是计算完成率、转化率、占比类指标的通用模板,务必练到肌肉记忆。
追问:
COUNT(1)和COUNT(*)有性能差别吗?提示:InnoDB 下语义和优化后执行基本一致,COUNT(列)才因 NULL 判断而不同。
平均值统计时如何给 NULL 兜底?
结论:先在外层查询前用 IF 或 IFNULL 把 NULL 替换成业务默认值(未交卷按试卷时长、按 0 分),再做 AVG。
原理:AVG 会跳过 NULL 行,导致分母变小、结果虚高——「平均用时」如果把未完成的记录直接丢掉,剩下的全是认真答完的人,指标失真。正确做法是在明细层完成替换,让每条记录都参与平均:
SELECT er.uid, |
延伸:IFNULL(col, 0) 是 IF(col IS NOT NULL, col, 0) 的简写;多列依次兜底用 COALESCE(a, b, 0)。选哪个取决于是否要区分「NULL」与「条件不满足」。
追问:
NULL + 1、NULL = NULL各返回什么?提示:都是 NULL,比较 NULL 要用IS NULL或空值安全等于<=>。
条件统计与筛选
如何用 CASE WHEN 做分级占比统计?
结论:CASE WHEN 把分数映射成等级标签,按「用户等级 + 分数等级」分组计数,再除以该用户等级的总数得到占比。
原理:CASE WHEN 按顺序匹配、命中即返回,所以分界点写法 [90,75,60] 从大到小判断即可,无需写双边区间。占比的分母是「同一 level 的完成总数」,单独用一个派生表算再 JOIN 回来:
SELECT g.level, g.score_grade, |
延伸:MySQL 8.0 可以把分母写成 SUM(COUNT(*)) OVER (PARTITION BY level),省掉一次自 JOIN。行转列(每个等级一列)则用 SUM(CASE WHEN ... THEN 1 ELSE 0 END) 透视。
追问:
CASE的两种形态(简单式CASE col WHEN v与搜索式CASE WHEN cond)能混用吗?提示:不能,简单式只能做等值比较。
跨表「最近一次活跃」怎么筛选?
结论:用户表 LEFT JOIN 两类活跃记录表,条件用 OR 合并两个表的月份判断,命中即保留。
原理:「试卷区或题目区任一在 9 月活跃」是跨表 OR 条件。用 LEFT JOIN 而非 INNER JOIN 是为了不漏掉「只在一个区活跃」的用户;两表一对多会造成用户行膨胀,最后 DISTINCT 去重。昵称前缀加后缀的模式 LIKE '牛客%号'、数值区间 BETWEEN 1200 AND 2500 直接放在 WHERE:
SELECT DISTINCT u.uid, u.nick_name, u.achievement |
延伸:LEFT JOIN 后把右表条件写进 WHERE(如 er.submit_time IS NOT NULL)会把左连接退化回内连接,这是高频 bug。要让条件不破坏左表完整性,应写进 ON 子句。
追问:这里的
DATE_FORMAT(列)会让索引失效吗?提示:对列套函数通常无法走索引,数据量大时改用范围比较。
精确模式匹配为什么用 REGEXP 而不是 LIKE?
结论:LIKE 只有 % 和 _ 两个通配符,表达不了「纯数字」「数字串」这类字符类约束;REGEXP(别名 RLIKE)用正则精确描述模式。
原理:「昵称是牛客+纯数字+号,或纯数字」用 LIKE '牛客%号' 会把「牛客 H 号」也放进来——% 匹配任意字符。正则 ^牛客[0-9]+号$ 用 [0-9]+ 限定数字段,^/$ 锚定首尾;「c 开头的类别(兼容大小写)」写 ^[cC]:
SELECT er.uid, er.exam_id, ROUND(AVG(er.score), 0) AS avg_score |
延伸:MySQL 8.0 正则引擎换成了 ICU,支持 REGEXP_LIKE 等函数和更完整的语法;LIKE 的优势是简单模式可走索引(前缀匹配),REGEXP 则一律全扫,性能敏感场景权衡使用。
追问:
LIKE 'c%'在默认 utf8mb4 collation 下能匹配 ‘C++’ 吗?提示:默认ci结尾排序规则不区分大小写,能匹配——这正是题目改用正则的原因。
分页
取第 N 页数据怎么写?
结论:LIMIT offset, size——第 3 页、每页 3 条即 LIMIT 6, 3,offset 从 0 起算,公式是 (页码 - 1) * 每页条数。
原理:先按业务口径筛人(求职方向为算法、注册当天完成算法类试卷),按最高分排序后再截页。排序键有并列时结果不稳定,最好追加唯一键做决胜:
SELECT u.uid, u.level, u.register_time, MAX(t.score) AS max_score |
延伸:LIMIT 100000, 20 这类深分页会扫描并丢弃前十万行,线上常见优化是「游标分页」——记住上一页最后一条的排序键,下一页用 WHERE id > 上次值 LIMIT 20,把偏移扫描变成索引定位。
追问:为什么深分页慢?提示:offset 不是跳过而是「取出来再扔掉」,代价随页码线性增长。
文本函数
串列的字段怎么拆分修复?
结论:SUBSTRING_INDEX(str, ',', n) 按分隔符截取,正数从左数、负数从右数;取中间段用两次嵌套。
原理:录错的行把 tag,difficulty,duration 三个值挤进了 tag 列。第一段是 SUBSTRING_INDEX(tag, ',', 1);最后一段是 SUBSTRING_INDEX(tag, ',', -1);中间那段先取「前两段」再从中取「最后一段」:
SELECT exam_id, |
延伸:识别脏数据用 difficulty = '' 这个特征。更通用的按位置取第 N 段可以写成 SUBSTRING_INDEX(SUBSTRING_INDEX(col, ',', N), ',', -1)。这类「一列存多值」本身就是反范式设计,修复后应改表结构,而不是长期依赖字符串函数兜底。
追问:拆分出的 duration 是字符串类型,比较大小时会怎样?提示:按字典序而非数值序,需要
CAST(... AS UNSIGNED)。
大小写混乱的维度怎么归并统计?
结论:UPPER()/LOWER() 统一大小写;要把「小写形式的作答数」挂到「已存在的大写形式」上,用同一张聚合结果表自连接。
原理:先按原始 tag 分组算出每个形态的作答数(CTE a),再让 a 与自身连接:连接条件是 UPPER(a.tag) = b.tag AND a.tag <> b.tag,即「a 是小写形态、b 是它对应的大写形态」。配上「作答数小于 3」的筛选,就实现了「冷门的小写形态归并到大写形态的口径下展示」:
WITH a AS ( |
延伸:注意默认 collation 不区分大小写,a.tag = b.tag 判断「形态不同」时可能误判,所以用 UPPER() 转换后的等值加原始值不等来锁定「同词不同形」。根治方案是入库前统一规范化,或在列上显式指定 COLLATE utf8mb4_bin 让大小写敏感。
追问:自连接里如果同一词存在三种形态(c++、C++、C++ 带空格)会发生什么?提示:会产生多对多匹配,需先用
TRIM/UPPER规范化再聚合。
超长昵称怎么截断展示(LENGTH 与 CHAR_LENGTH 的区别)?
结论:CHAR_LENGTH 数字符数、LENGTH 数字节数;多字节字符集下判断「昵称长度」必须用 CHAR_LENGTH,截取用 SUBSTR 加 CONCAT 补省略号。
原理:utf8mb4 编码下一个汉字占 3 字节、emoji 占 4 字节,LENGTH('你好') 返回 6 而 CHAR_LENGTH('你好') 返回 2——用错函数,中文昵称的长度判断直接偏三倍。需求「字符数大于 10 输出、大于 13 截前 10 字符加省略号」用 CASE WHEN 分层处理:
SELECT uid, |
延伸:SUBSTR 按字符取子串,与 CHAR_LENGTH 口径一致,不会出现截断半个汉字产生乱码的问题。展示层的截断更推荐放在应用代码里做,SQL 只做取数;不得不在 SQL 做时,注意截断后的列不要再参与 WHERE 或 JOIN。
追问:什么场景反而必须关心字节长度?提示:索引最大长度限制(InnoDB 767/3072 字节)、协议报文字段上限、存储成本估算。

