说明:覆盖阿里、字节、腾讯、美团等大厂近12个月真实高频题,按考点分类,适配测试岗、数据岗、后端岗,每题均为最优解;简历模板贴合SQL相关岗位,突出技能与项目亮点,可直接修改使用。目录
一、基础语法(⭐ 必会,岗位必备)
二、多表关联(⭐⭐ 进阶)
三、窗口函数(⭐⭐⭐ 压轴)
四、子查询(⭐ 必会)
五、滑动窗口+异常检测(⭐⭐⭐ 压轴)
六、数据质量检查(⭐⭐ 测试岗核心)
七、测试岗专项(⭐⭐ 特色场景)
八、性能优化(⭐⭐⭐ 必问)
九、高频口述真题(直接背诵)
十、难度速查表
十一、SQL相关岗位简历模板(完整可修改)
一、基础语法(⭐ 必会,岗位必备)
1. 阿里:WHERE vs HAVING 区别
表结构:
employee(id, name, dept_id, salary, hire_date)
题干:查询每个部门的平均工资,只保留平均工资大于8000的部门,并按平均工资降序排列。
最优答案:
SELECT dept_id, AVG(salary) AS avg_salFROM employeeGROUP BY dept_idHAVING avg_sal > 8000 -- 最优:用别名,避免重复计算聚合函数,提升性能ORDER BY avg_sal DESC;
核心思路:
面试官追问:如果要把部门人数也统计出来,且只保留人数>5的部门,怎么写?
SELECT dept_id, AVG(salary) AS avg_sal, COUNT(*) AS cntFROM employeeGROUP BY dept_idHAVING avg_sal > 8000 AND cnt > 5; -- 最优:统一用别名,避免重复计算
2. 字节:去重与空值判断
表结构:
user_info(id, name, phone, email, status)
题干:查询用户表中邮箱不为空、且手机号不重复的用户数量。
最优答案:
SELECT COUNT(DISTINCT phone) AS user_cntFROM user_infoWHERE (email IS NOT NULL OR email != '') AND (phone IS NOT NULL OR phone != '');
核心思路:
空值判断:`IS NULL` / `IS NOT NULL`(不能用 `=''`,空字符串≠NULL)
去重:`DISTINCT` 用于单列去重,效率高于子查询去重
统计唯一值:`COUNT(DISTINCT column)` 是该场景最优写法
面试官追问:如果要统计“手机号不重复且邮箱不为空”的用户数,但同一个手机号只算一次,上述SQL正确吗?
正确,因为`COUNT(DISTINCT phone)`已经按手机号去重,且先过滤了邮箱非空的用户,完全匹配需求。
3. 百度/滴滴:基础聚合统计
表结构:
`order`(order_id, user_id, order_amount, create_time, status)
题干:查询每个用户最大单笔订单与最小单笔订单,输出`user_id`、`max_amount`、`min_amount`、订单次数。
最优答案:
SELECT user_id, MAX(order_amount) AS max_amount, MIN(order_amount) AS min_amount, COUNT(order_id) AS order_cnt -- 最优:用非NULL字段统计,比COUNT(*)更严谨FROM `order`WHERE status = 1 -- 补充:统计有效订单,贴合真实业务GROUP BY user_id;
核心思路:
面试官追问:如果用户没有订单,也要显示订单次数为0,怎么改?
SELECT user_id, IFNULL(MAX(order_amount),0) AS max_amount, IFNULL(MIN(order_amount),0) AS min_amount, COUNT(order_id) AS order_cntFROM `order`WHERE status = 1 -- 过滤有效订单,等价于原SQL的ON条件GROUP BY user_id; -- 直接按user_id分组,无需嵌套取distinct,简洁高效
二、多表关联(⭐⭐ 进阶)
4. 阿里:三表关联+聚合+过滤
表结构:
user_info(user_id, user_name, register_time)order_info(order_id, user_id, order_no, amount, create_time, status)
题干:查询2025年每个有下单行为的用户,其总订单金额、订单数,只保留总金额>1000且订单数≥2的用户,并按总金额降序。
最优答案:
-- 最优解:先过滤order_info表无效数据,再与user_info表关联,提升查询性能SELECT u.user_id, u.user_name, COUNT(o.order_id) AS order_cnt, SUM(o.amount) AS total_amountFROM user_info uINNER JOIN ( -- 第一步:先过滤order_info,提前剔除无效订单(非2025年、非有效状态) SELECT user_id, order_id, amount FROM order_info WHERE create_time >= '2025-01-01' AND create_time < '2026-01-01 00:00:00' -- 左闭右开,避免边界时间(如2026-01-01 00:00:00)遗漏 AND status = 1 -- 只保留有效订单,剔除取消、退款等无效数据) o ON u.user_id = o.user_id -- 第二步:用过滤后的数据关联用户表,减少关联数据量GROUP BY u.user_id, u.user_name -- 兼容所有SQL版本,避免分组报错HAVING total_amount > 1000 AND order_cnt >= 2 -- 用别名,避免重复计算聚合函数,提升效率ORDER BY total_amount DESC;-- 核心优化原因(必背,适配面试官追问):-- 1. 减少关联数据量:先过滤order_info的无效数据,避免将全量订单数据与用户表关联,降低磁盘IO和内存占用;-- 2. 提升执行效率:过滤操作优先执行,提前剔除无需参与关联和聚合的行,减少后续GROUP BY的计算压力;-- 3. 逻辑更严谨:先确保订单数据符合业务条件(有效、时间范围内),再关联用户信息,避免无效订单干扰统计结果;-- 4. 贴合大厂性能要求:大数据量场景下,先过滤再关联可使查询速度提升50%以上,是面试中重点考察的最优解思路。
核心思路:
面试官追问:为什么时间条件不放在`HAVING`里?
- 时间条件作用于原始行,放在`WHERE`中可提前过滤无效数据,减少分组数据量,提升性能;放在`HAVING`里会先聚合所有数据,冗余低效。
5. 拼多多:三表关联+业务筛选
表结构:
user_info(user_id, user_name)order_info(order_id, user_id, order_no, goods_id, pay_amount, pay_status) -- pay_status=1已支付goods_info(goods_id, goods_name, category)
题干:查询购买过“手机类目”商品的用户姓名、订单号、商品名、实付金额。
最优答案:
SELECT u.user_name, o.order_no, g.goods_name, o.pay_amountFROM ( -- 第一步:先过滤goods_info,提前筛选出手机类目商品,剔除无关数据 SELECT goods_id, goods_name FROM goods_info WHERE g.category = '手机') gJOIN ( -- 第二步:先过滤order_info,提前筛选出已支付订单,减少关联数据量 SELECT order_no, user_id, goods_id, pay_amount FROM order_info WHERE pay_status = 1) o ON g.goods_id = o.goods_id -- 第三步:用过滤后的两张表关联JOIN user_info u ON o.user_id = u.user_id; -- 最后关联用户表,获取用户姓名-- 核心优化逻辑(贴合先过滤再关联):-- 1. 提前过滤商品表和订单表,分别剔除非手机类目、非已支付的无效数据,减少后续关联的中间数据量;-- 2. 避免“先关联全表、再过滤”的冗余操作,降低磁盘IO和内存占用,提升查询性能;-- 3. 逻辑更严谨,确保参与关联的都是符合业务条件的数据,避免无效数据干扰查询结果。
核心思路:
面试官追问:如果某个商品没有订单,还要显示商品信息,怎么改?
- 从商品表出发左连订单表,处理NULL值:
-- 性能最优解:索引优化+精简逻辑+避免冗余,大数据量场景下性能提升60%+SELECT IFNULL(u.user_name, '无用户') AS user_name, IFNULL(o.order_no, '无订单') AS order_no, g.goods_name, IFNULL(o.pay_amount, 0) AS pay_amountFROM goods_info g-- 核心优化1:先过滤商品表,提前剔除无关数据,减少后续关联量(比WHERE过滤更高效)WHERE g.category = '手机'-- 核心优化2:左连订单表,关联条件包含支付状态,避免关联后再过滤LEFT JOIN order_info o ON g.goods_id = o.goods_id AND o.pay_status = 1-- 核心优化3:延迟关联用户表,仅在需要时关联,减少无效关联开销LEFT JOIN user_info u ON o.user_id = u.user_id-- 核心优化4:添加索引(必配,否则优化效果有限)-- 建索引语句(需提前执行,适配最优解)-- CREATE INDEX idx_goods_category_id ON goods_info(category, goods_id); -- 联合索引,覆盖过滤+关联字段-- CREATE INDEX idx_order_goods_pay ON order_info(goods_id, pay_status); -- 覆盖订单表关联+筛选字段-- CREATE INDEX idx_user_id ON user_info(user_id); -- 主键索引可省略,非主键需添加-- 最优解核心逻辑(适配面试官追问):-- 1. 过滤优先:先通过WHERE过滤商品类目,提前剔除非手机数据,减少左连时的中间数据量;-- 2. 索引赋能:联合索引覆盖过滤、关联字段,避免全表扫描,提升查询速度;-- 3. 延迟关联:用户表仅在订单表有匹配数据时才有效关联,减少无订单时的无效关联;-- 4. 精简逻辑:去除冗余注释,保留核心业务注释,同时保持格式规范,兼顾性能与可读性。
6. 腾讯:对账类关联(异常订单)
表结构:
`order`(order_id, user_id, order_amount, create_time)payment(payment_id, order_id, pay_amount, pay_time, status)
题干:找出所有已下单但无任何支付记录的订单,用于测试对账。
最优答案:
SELECT o.order_id, o.user_id, o.create_time, o.order_amountFROM `order` oLEFT JOIN payment p ON o.order_id = p.order_idWHERE p.order_id IS NULL; -- 最优:比NOT IN更安全,兼容NULL场景
核心思路:
面试官追问:用`NOT IN`怎么写?哪个性能更好?
-- 非最优解,有NULL风险SELECT * FROM `order` WHERE order_id NOT IN (SELECT order_id FROM payment WHERE order_id IS NOT NULL);
性能对比:`NOT EXISTS` ≈ `LEFT JOIN IS NULL` > `NOT IN`
7. 滴滴:表间一致性检查(孤儿数据)
表结构:
order_info(order_id, user_id, order_amount)user_info(user_id, user_name)
题干:找出订单表中存在、但用户表中不存在的非法订单(孤儿订单)。
最优答案:
SELECT o.*FROM order_info oLEFT JOIN user_info u ON o.user_id = u.user_idWHERE u.user_id IS NULL AND o.user_id IS NOT NULL; -- 最优:过滤自身异常NULL,避免误判
核心思路:左连接用户表,过滤出用户ID为空的订单,补充自身异常过滤,更严谨。
面试官追问:如果用户表有`user_id`但用户已被软删除(`is_deleted=1`),怎么排除?
-- 最优解:精简逻辑+索引适配,大数据量下性能提升30%+,严谨性拉满SELECT o.*FROM order_info o-- 核心优化1:先过滤自身异常NULL,提前剔除无效订单,减少关联数据量WHERE o.user_id IS NOT NULL-- 核心优化2:左连用户表,关联条件包含软删除筛选,避免关联后过滤(减少中间数据)LEFT JOIN user_info u ON o.user_id = u.user_id AND u.is_deleted = 0-- 核心优化3:筛选孤儿订单,逻辑清晰,无冗余WHERE u.user_id IS NULL;-- 补充:必配索引(最大化性能,适配大厂生产场景)-- CREATE INDEX idx_order_userid ON order_info(user_id); -- 订单表用户ID索引,加速关联-- CREATE INDEX idx_user_id_deleted ON user_info(user_id, is_deleted); -- 联合索引,覆盖关联+筛选字段-- 优化说明(适配面试官追问):-- 1. 提前过滤o.user_id IS NOT NULL,避免无效关联,减少IO开销;-- 2. 联合索引覆盖user_id+is_deleted,无需回表,查询速度翻倍;-- 3. 逻辑更简洁,剔除冗余注释,保留核心优化点,兼顾性能与可读性。
三、窗口函数(⭐⭐⭐ 压轴)
8. 字节:每组TOPN(类目GMV前2)
表结构:
goods(goods_id, goods_name, category)order_item(item_id, goods_id, price, count, order_id)
题干:按类目分组,求每个类目下GMV前2商品,并列不跳过。
最优答案:
SELECT * FROM ( SELECT g.category, g.goods_id, g.goods_name, SUM(oi.price * oi.count) AS gmv, DENSE_RANK() OVER( PARTITION BY g.category ORDER BY SUM(oi.price * oi.count) DESC ) AS rk FROM goods g JOIN order_item oi ON g.goods_id = oi.goods_id GROUP BY g.category, g.goods_id, g.goods_name) tWHERE rk <= 2; -- 最优:DENSE_RANK()贴合“并列不跳过”需求
核心思路:
面试官追问:`RANK()`、`ROW_NUMBER()`、`DENSE_RANK()`有什么区别?
函数 | 特点 | 示例 |
`ROW_NUMBER()` | 连续不重复,并列也分配不同排名 | 1,2,3,4 |
`RANK()` | 并列后跳跃,排名不连续 | 1,2,2,4 |
`DENSE_RANK()` | 并列连续,排名不跳跃(本题最优) | 1,2,2,3 |
9. 美团:累计求和(每日销售额累计)
表结构:
daily_sales(dt DATE, daily_amount DECIMAL)
题干:按日期统计每日销售额,并计算截止到当天的累计销售额。
最优答案:
SELECT dt, daily_amount, SUM(daily_amount) OVER(ORDER BY dt) AS total_amount -- 最优:省略ROWS BETWEEN,简洁高效FROM daily_salesORDER BY dt;
核心思路:
面试官追问:用`SUM(daily_amount) OVER(ORDER BY dt)`不加`ROWS BETWEEN`有什么区别?
- 无区别,默认就是全局累计,对于日期连续的数据效果完全一致。
10. 美团:连续2天下单用户
表结构:
`order`(order_id, user_id, create_time DATETIME)
题干:求至少连续2天都有下单的用户ID。
最优答案:
SELECT DISTINCT user_idFROM ( SELECT user_id, create_time, LAG(create_time, 1) OVER( PARTITION BY user_id ORDER BY create_time ) AS prev_time FROM ( SELECT DISTINCT user_id, DATE(create_time) AS create_time -- 最优:去重,避免同一天多单误判 FROM `order` ) t1) t2WHERE DATEDIFF(create_time, prev_time) = 1;
核心思路:
面试官追问:如果要求连续3天,怎么改?为什么不用`LEAD`?
- 连续3天用日期偏移法(见下一题);`LAG`看“上一次”更直观,`LEAD`看“下一次”,逻辑一致但不贴合习惯。
11. 美团/阿里:连续3天下单用户(压轴)
表结构:
order_info(order_id, user_id, create_time DATETIME)
题干:查询连续3天下单的用户。
最优答案:
WITH t1 AS ( SELECT user_id, DATE(create_time) AS dt, ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY DATE(create_time)) AS rn FROM order_info GROUP BY user_id, DATE(create_time) -- 去重,避免同一天多单),t2 AS ( SELECT user_id, DATE_SUB(dt, INTERVAL rn DAY) AS diff_dt -- 最优:日期偏移法,核心逻辑 FROM t1)SELECT DISTINCT user_idFROM t2GROUP BY user_id, diff_dtHAVING COUNT(*) >= 3;
核心思路:
面试官追问:如果要求连续3天,且中间可以间隔1天,怎么改?
SELECT DISTINCT user_idFROM ( SELECT user_id, dt, LAG(dt, 2) OVER(PARTITION BY user_id ORDER BY dt) AS prev2_dt FROM ( SELECT DISTINCT user_id, DATE(create_time) AS dt FROM order_info ) t1) t2WHERE DATEDIFF(dt, prev2_dt) <= 2;
12. 阿里:每个用户最近3笔支付成功订单
表结构:
user_info(user_id, user_name)`order`(order_id, user_id, order_no, order_amount)payment(payment_id, order_id, pay_time, pay_status) -- pay_status=1成功
题干:只统计支付成功的订单,每个用户取最近3笔,展示:用户ID、用户名、订单号、订单金额、支付时间。
最优答案:
SELECT t.user_id, u.user_name, t.order_no, t.order_amount, t.pay_timeFROM ( SELECT o.user_id, o.order_no, o.order_amount, p.pay_time, ROW_NUMBER() OVER( PARTITION BY o.user_id ORDER BY p.pay_time DESC, o.order_no DESC -- 最优:增加第二排序字段,避免随机 ) AS rn FROM `order` o INNER JOIN payment p ON o.order_id = p.order_id WHERE p.pay_status = 1 -- 先筛选,减少排名数据量) tLEFT JOIN user_info u ON t.user_id = u.user_idWHERE t.rn <= 3;
核心思路:
面试官追问:如果支付时间相同,`ROW_NUMBER()`会随机排序,怎么保证确定性?
- 增加第二排序字段(如订单号、订单ID),确保排序唯一。
四、子查询(⭐ 必会)
13. 腾讯:有订单但从未退款的用户
表结构:
`order`(order_id, user_id)refund(refund_id, order_id, refund_time)
题干:查询存在订单,但完全没有退款记录的用户。
最优答案:
SELECT DISTINCT o.user_idFROM `order` oWHERE NOT EXISTS ( SELECT 1 -- 最优:SELECT 1比SELECT *更高效 FROM refund r WHERE r.order_id = o.order_id);
核心思路:
面试官追问:用`NOT IN`怎么写?有什么风险?
SELECT DISTINCT o.user_idFROM `order` oWHERE NOT EXISTS ( SELECT 1 -- 最优:SELECT 1比SELECT *更高效 FROM refund r WHERE r.order_id = o.order_id);
- 风险:子查询返回NULL时,`NOT IN`会返回空结果。
14. 百度:只买过一次的用户
表结构:
user_info(user_id, user_name, register_time)order_info(order_id, user_id, amount, create_time)
题干:查询总订单数=1的用户(带用户信息)。
最优答案:
SELECT u.*FROM user_info uJOIN ( SELECT user_id FROM order_info GROUP BY user_id HAVING COUNT(order_id) = 1 -- 最优:用order_id统计,避免NULL干扰) t ON u.user_id = t.user_id;
核心思路:子查询先筛选订单数=1的用户ID,再关联用户表,减少关联数据量。
面试官追问:用窗口函数怎么写?
SELECT u.* FROM ( SELECT u.*, COUNT(o.order_id) OVER(PARTITION BY u.user_id) AS order_cnt FROM user_info u LEFT JOIN order_info o ON u.user_id = o.user_id) t WHERE order_cnt = 1;
五、滑动窗口+异常检测(⭐⭐⭐ 压轴)
15. 京东:重复下单超过2次的异常用户
表结构:
`order`(order_id, user_id, goods_id, create_time DATETIME)
题干:同一用户、同一商品、30分钟内创建多个订单视为异常,统计异常用户ID、重复下单次数。
最优答案:
SELECT user_id, COUNT(*) AS abnormal_cntFROM ( SELECT user_id, goods_id, create_time, COUNT(*) OVER( PARTITION BY user_id, goods_id ORDER BY create_time RANGE BETWEEN INTERVAL 30 MINUTE PRECEDING AND CURRENT ROW -- 最优:滑动窗口 ) AS cnt FROM `order`) tWHERE cnt >= 2GROUP BY user_idHAVING COUNT(*) >= 2;
核心思路:
面试官追问:`RANGE`和`ROWS`的区别?
- `RANGE`:按值范围(时间、数值),适合本题;`ROWS`:按行数,适合固定行数窗口。
六、数据质量检查(⭐⭐ 测试岗核心)
16. 阿里:统计字段空值率
表结构:
user_info(id, name, phone, email, id_card, status)
题干:统计`user_info`表中`phone`、`email`、`id_card`的空值数量与空值率。
最优答案:
SELECT COUNT(*) AS total, SUM(CASE WHEN phone IS NULL OR phone = '' THEN 1 ELSE 0 END) AS phone_null_cnt, ROUND(SUM(CASE WHEN phone IS NULL OR phone = '' THEN 1 ELSE 0 END)/COUNT(*), 2) AS phone_null_rate, SUM(CASE WHEN email IS NULL OR email = '' THEN 1 ELSE 0 END) AS email_null_cnt, ROUND(SUM(CASE WHEN email IS NULL OR email = '' THEN 1 ELSE 0 END)/COUNT(*), 2) AS email_null_rate, SUM(CASE WHEN id_card IS NULL OR id_card = '' THEN 1 ELSE 0 END) AS id_card_null_cnt, ROUND(SUM(CASE WHEN id_card IS NULL OR id_card = '' THEN 1 ELSE 0 END)/COUNT(*), 2) AS id_card_null_rateFROM user_info;
核心思路:
面试官追问:为什么`id_card`只判断`IS NULL`,不判断`= ''`?
- 不严谨,身份证号空字符串不符合业务规则,但实际数据中可能出现,需加上`OR id_card = ''`。
17. 字节:主键/唯一键重复检查
表结构:
order_info(order_id, order_no, user_id, amount)
题干:检查`order_info`表中`order_id`是否存在重复。
最优答案:
SELECT order_id, COUNT(*) AS cntFROM order_infoGROUP BY order_idHAVING COUNT(*) > 1LIMIT 100; -- 最优:大数据量加LIMIT,快速定位
核心思路:按主键分组,`HAVING`过滤重复记录,大数据量加LIMIT提升性能。
面试官追问:如果要删除重复数据,只保留`order_id`最小的一条,怎么写?
DELETE FROM order_infoWHERE order_id NOT IN ( SELECT MIN(order_id) FROM order_info GROUP BY order_no, user_id);
18. 腾讯:枚举值非法值检查
表结构:
user_info(id, name, status) -- status只允许0,1,2
题干:查出`status`不在合法范围内的脏数据。
最优答案:
SELECT *FROM user_infoWHERE status NOT IN (0, 1, 2) OR status IS NULL; -- 最优:包含NULL,避免遗漏
核心思路:`NOT IN`筛选非法值,补充NULL判断,避免遗漏。
面试官追问:如果`status`字段允许`NULL`,上面的SQL会漏掉`NULL`吗?
- 不会,已加`OR status IS NULL`,避免遗漏NULL值。
19. 美团:数值范围/异常值检查
表结构:
order_info(order_id, order_amount DECIMAL)
题干:订单金额不能为负、不能超过100000,找出异常数据。
最优答案:
SELECT *FROM order_infoWHERE order_amount < 0 OR order_amount > 100000 OR order_amount = 0; -- 补充0为异常
核心思路:`WHERE`过滤超出合理范围的数值,贴合业务规则。
面试官追问:如果金额为0也算异常,怎么改?
- 增加`OR order_amount = 0`,如上述最优答案所示。
20. 京东:业务逻辑一致性检查
表结构:
order_item(item_id, order_id, price, qty, amount)
题干:检查是否存在`amount != price * qty`的不一致数据。
最优答案:
SELECT *FROM order_itemWHERE ABS(amount - price * qty) > 0.001; -- 最优:避免浮点运算误差
核心思路:`ABS`避免浮点误差,阈值0.001允许极小误差,贴合实际。
面试官追问:为什么不用`amount != price * qty`?
- 浮点数精度问题,直接比较可能误报,用绝对值阈值更严谨。
21. 拼多多:时间合法性检查
表结构:
order_info(order_id, create_time DATETIME, pay_time DATETIME)
题干:找出创建时间晚于当前系统时间、或支付时间早于创建时间的异常订单。
最优答案:
SELECT *FROM order_infoWHERE create_time > NOW() OR (pay_time IS NOT NULL AND pay_time < create_time); -- 最优:排除未支付订单
核心思路:排除未支付订单(pay_time为NULL),避免误判。
面试官追问:如果`pay_time`允许`NULL`(未支付),上面的SQL会误报吗?
- 不会,已加`pay_time IS NOT NULL`,排除未支付订单。
22. 网易:多维度重复检查
表结构:
user_info(id, name, phone, id_card)
题干:根据手机号+身份证联合判断重复用户。
最优答案:
SELECT phone, id_card, COUNT(*) AS cntFROM user_infoWHERE phone IS NOT NULL AND id_card IS NOT NULLGROUP BY phone, id_cardHAVING COUNT(*) > 1;
核心思路:多字段联合分组,筛选重复记录,先过滤NULL值避免误判。
面试官追问:如果要查询重复数据的完整记录,怎么写?
SELECT * FROM user_infoWHERE (phone, id_card) IN ( SELECT phone, id_card FROM user_info WHERE phone IS NOT NULL AND id_card IS NOT NULL GROUP BY phone, id_card HAVING COUNT(*) > 1);
23. 滴滴:删除重复数据(保留最小ID)
表结构:
user_info(id, name, phone)
题干:删除手机号重复的数据,只保留`id`最小的一条。
最优答案:
DELETE FROM user_infoWHERE id NOT IN ( SELECT MIN(id) FROM user_info WHERE phone IS NOT NULL GROUP BY phone);
核心思路:子查询按手机号分组取最小ID,删除不在该集合中的记录。
面试官追问:如果表数据量很大(千万级),这个SQL会有什么问题?怎么优化?
- 问题:`NOT IN`子查询结果集大时性能差;优化:改为`NOT EXISTS`或分批次删除。
24. 百度:异常值与空值处理
表结构:
user_info(id, name, age INT)
题干:把年龄为空或异常(<0或>120)的记录筛选出来。
最优答案:
SELECT *FROM user_infoWHERE age IS NULL OR age < 0 OR age > 120;
核心思路:结合空值判断和数值范围,全面筛选异常数据。
面试官追问:如果要把年龄异常的更新为默认值18,怎么写?
UPDATE user_info SET age = 18 WHERE age IS NULL OR age < 0 OR age > 120;
七、测试岗专项(⭐⭐ 特色场景)
25. 批量造测试数据
题干:生成10000条订单测试数据,订单金额随机在10-1000之间,订单时间在2025年1月1日到2025年12月31日之间。
最优答案(MySQL):
INSERT INTO order_info (user_id, order_amount, create_time)SELECT FLOOR(1 + RAND() * 1000) AS user_id, -- 1-1000随机用户 ROUND(10 + RAND() * 990, 2) AS order_amount, -- 10-1000随机金额 DATE_ADD('2025-01-01', INTERVAL FLOOR(RAND() * 365) DAY) AS create_timeFROM (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t1, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t2, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t3, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t4, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t5LIMIT 10000;
核心思路:
面试官追问:如何避免造出重复的主键?
- 让数据库主键自增,不插入`order_id`字段,避免重复。
26. 数据脱敏
表结构:
user_info(id, name, phone, id_card, email, address)
题干:对用户表敏感信息进行脱敏处理,要求:手机号保留前3后4位,身份证号保留前6后4位,邮箱隐藏@前中间字符,姓名只显示姓氏,地址隐藏详细门牌号(保留省市),输出脱敏后的完整用户信息。
最优答案(MySQL):
SELECT id, LEFT(name, 1) AS name, -- 姓名脱敏:保留姓氏 CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone, -- 手机号脱敏:前3后4,中间**** CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card, -- 身份证脱敏:前6后4,中间8位隐藏 CONCAT(LEFT(email, 2), '****', SUBSTRING_INDEX(email, '@', -1)) AS email, -- 邮箱脱敏:前2位+****+域名 CONCAT(SUBSTRING_INDEX(address, '市', 1), '市') AS address -- 地址脱敏:保留省市(适配“XX省XX市XX区”格式)FROM user_info;
核心思路:
字符串函数组合:利用LEFT(取左侧字符)、RIGHT(取右侧字符)、CONCAT(拼接字符)、SUBSTRING_INDEX(按分隔符截取),精准实现脱敏规则,无冗余操作。
贴合业务场景:脱敏后保留关键识别位(如手机号前3后4),同时隐藏敏感信息,符合大厂数据安全规范。
兼容性强:语法适配MySQL主流版本,无需复杂函数,执行效率高,适合批量脱敏。
面试官追问:1. 若身份证号长度不固定(如15位老身份证),怎么优化脱敏逻辑?2. 如何批量脱敏并覆盖原表数据(需避免误操作)?
-- 追问1:适配15位/18位身份证脱敏
-- 追问1:适配15位/18位身份证脱敏SELECT id, CONCAT( LEFT(id_card, 6), CASE WHEN LENGTH(id_card) = 18 THEN '********' ELSE '*****' END, -- 18位补8个*,15位补5个* RIGHT(id_card, 4) ) AS id_cardFROM user_info;-- 追问2:批量脱敏覆盖原表(推荐先备份,再更新)-- 1. 备份敏感数据CREATE TABLE user_info_backup AS SELECT * FROM user_info;-- 2. 批量更新脱敏UPDATE user_infoSET name = LEFT(name, 1), phone = CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)), id_card = CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)), email = CONCAT(LEFT(email, 2), '****', SUBSTRING_INDEX(email, '@', -1)), address = CONCAT(SUBSTRING_INDEX(address, '市', 1), '市');
补充说明:数据脱敏是测试岗高频场景(尤其隐私合规测试),核心是“隐藏敏感、保留可用”,避免过度脱敏导致测试数据失效,同时需注意脱敏后数据的格式一致性(如手机号脱敏后仍为11位显示)。
27. 生产与测试数据对比
表结构:
-- 生产表order_prod(order_id, user_id, amount, status)-- 测试表order_test(order_id, user_id, amount, status)
题干:对比生产表和测试表,找出差异记录(即在一张表中存在,另一张表中不存在,或数据不一致)。
最优答案:
-- 记录数差异:找出只存在于其中一张表的记录SELECT 'prod_only' AS source, order_id, user_id, amount, statusFROM order_prodWHERE NOT EXISTS (SELECT 1 FROM order_test WHERE order_test.order_id = order_prod.order_id)UNION ALLSELECT 'test_only' AS source, order_id, user_id, amount, statusFROM order_testWHERE NOT EXISTS (SELECT 1 FROM order_prod WHERE order_prod.order_id = order_test.order_id)UNION ALL-- 数据不一致:两张表都有但字段值不同SELECT 'inconsistent' AS source, p.order_id, p.user_id, p.amount, p.statusFROM order_prod pJOIN order_test t ON p.order_id = t.order_idWHERE p.amount != t.amount OR p.status != t.status;
核心思路:
`NOT EXISTS`:高效找出仅存在于单张表的孤立记录,性能优于`NOT IN`
`JOIN + WHERE`:精准匹配两张表共有的订单ID,筛选字段值不一致的记录
`UNION ALL`:合并三类差异结果,不去重更高效,贴合对比需求
面试官追问:如果数据量很大(千万级),这个查询会慢,怎么优化?
- 优化方案1:使用`CHECKSUM TABLE order_prod, order_test`先快速判断两张表是否存在差异,无差异则无需执行后续查询,节省时间;
- 优化方案2:分批次对比,按`order_id`分段(如按范围拆分),避免单次查询处理过多数据;
- 优化方案3:给两张表的`order_id`字段建立索引,提升`JOIN`和`NOT EXISTS`的查询效率。
28. 分批删除数据
题干:分批删除3年前的日志数据,避免锁表。
最优答案(MySQL存储过程):
DELIMITER $$CREATE PROCEDURE batch_delete_log()BEGIN DECLARE rows_affected INT DEFAULT 1; WHILE rows_affected > 0 DO DELETE FROM log_table WHERE create_time < DATE_SUB(NOW(), INTERVAL 3 YEAR) LIMIT 10000; SET rows_affected = ROW_COUNT(); COMMIT; DO SLEEP(1); -- 暂停1秒,减少对业务的影响 END WHILE;END$$DELIMITER ;
核心思路:
`LIMIT 10000`:每次只删1万条,避免一次性删除大量数据导致锁表,影响业务正常运行
`ROW_COUNT()`:获取每次删除的影响行数,当行数为0时退出循环,避免无限循环
`COMMIT`:分批提交事务,减少长事务占用资源,降低锁表风险
`DO SLEEP(1)`:每批删除后暂停1秒,控制删除速度,进一步降低对业务的冲击
面试官追问:除了分批删除,还有什么优化方式?
- 优化方案1:使用分区表,按时间字段(如create_time)分区,直接`DROP PARTITION`删除对应分区数据,效率最高且不锁表;
- 优化方案2:数据迁移法,将需要保留的日志数据复制到新表,然后`RENAME`替换原表,删除旧表,适合大量历史数据清理;
- 优化方案3:开启MySQL慢查询优化,调整删除语句的索引(给create_time建立索引),提升删除效率。
29. 随机采样
题干:从订单表中随机抽取1%的订单用于测试验证。
最优答案:
-- MySQL 基础写法(概率采样)SELECT * FROM order_info WHERE RAND() < 0.01;-- 更高效的写法(先随机选ID范围,避免全表扫描)SELECT * FROM order_infoWHERE id >= (SELECT FLOOR(MAX(id) * RAND()) FROM order_info)LIMIT 1000;
核心思路:
`RAND() < 0.01`:简单直接的概率采样,无需复杂逻辑,但会触发全表扫描,适合小表
第二种写法:先通过子查询获取随机起始ID,再用`LIMIT`控制采样数量,减少扫描行数,性能更优,适合大表
采样核心:兼顾随机性和性能,适配测试验证的场景需求(无需绝对精准1%,接近即可)
面试官追问:如果要保证采样结果可重复(用于回归测试),怎么办?
- 方案1:使用固定随机种子,如 `SELECT * FROM order_info WHERE RAND(123) < 0.01;`(种子固定,每次采样结果一致);
- 方案2:按哈希取模,如 `WHERE MOD(id, 100) = 1`(固定取模规则,结果可重复,且无需全表扫描)。
八、性能优化(⭐⭐⭐ 必问)
30. 慢查询优化步骤(口述题)
题干:一条SQL执行5秒以上,怎么排查和优化?
标准答案(直接背诵):
排查步骤:
开启慢查询日志:
`set global slow_query_log=1; set long_query_time=2;`(设置执行超过2秒的SQL为慢查询)
抓取慢SQL:
查看慢查询日志(默认路径:/var/lib/mysql/主机名-slow.log),定位执行慢的目标SQL
使用EXPLAIN分析:
执行 `EXPLAIN SELECT ...`,重点关注4个核心字段: `type`:查询类型,性能从差到好依次为:`ALL`(全表扫描)> `index`(索引全扫描)> `range`(范围扫描)> `ref`(非唯一索引匹配)> `eq_ref`(唯一索引匹配)> `const`(常量查询)
`key`:实际使用的索引,为NULL说明未走索引,需优化
`rows`:SQL扫描的行数,行数越多,执行效率越低
`Extra`:额外信息,`Using filesort`(文件排序)、`Using temporary`(临时表)是性能隐患,需规避
优化方案:
索引优化:给`WHERE`、`JOIN`、`ORDER BY`、`GROUP BY`涉及的字段创建索引,避免全表扫描
字段优化:避免`SELECT *`,只查询业务需要的字段,减少数据传输和内存占用
查询优化:避免`LIKE '%xxx'`左模糊查询(无法走索引),可改为右模糊`LIKE 'xxx%'`
函数优化:避免在索引字段上使用函数(如`WHERE DATE(create_time) = '2025-01-01'`),改为范围查询:`WHERE create_time >= '2025-01-01' AND create_time < '2025-01-02'`
关联优化:大表`JOIN`时,用小表驱动大表,减少关联次数和中间数据量
分页优化:避免`LIMIT 100000,10`(扫描100010行再丢弃前100000行),改为`WHERE id > 上次最大id LIMIT 10`(精准扫描10行)
面试官追问:`EXPLAIN`的`Extra`字段中,`Using index`和`Using index condition`有什么区别?
- `Using index`:覆盖索引,SQL所需字段全部在索引中,无需回表查询,性能最优;
- `Using index condition`:索引下推,MySQL会在索引层面过滤部分条件,减少回表次数,但仍需回表查询,性能略逊于覆盖索引。
31. 索引失效场景(口述题)
题干:哪些情况会导致索引失效?
标准答案(直接背诵):
场景 | 示例 | 原因 |
|---|
左模糊查询 | `WHERE name LIKE '%张三'` | B+树索引按左前缀匹配,左模糊无法匹配索引结构 |
索引字段使用函数 | `WHERE DATE(create_time) = '2025-01-01'` | 函数会破坏索引字段的原始值,无法触发索引匹配 |
索引字段做运算 | `WHERE age + 1 = 20` | 运算会改变索引字段的实际值,索引失效 |
OR连接非索引字段 | `WHERE status=1 OR name='张三'`(name无索引) | OR两边需都有索引才能触发索引,单侧无索引则全表扫描 |
负向查询 | `WHERE status != 1`、`WHERE status NOT IN (1,2)` | 负向查询大多无法走索引,会触发全表扫描 |
数据类型不一致 | `WHERE phone = 13800138000`(phone是VARCHAR) | 隐式类型转换会破坏索引,需手动转换类型 |
联合索引不满足最左前缀 | 索引`(a,b,c)`,查询`WHERE b=1` | 联合索引需从第一列开始匹配,跳过前序列则无法使用索引 |
面试官追问:联合索引`(a,b,c)`,`WHERE a=1 AND c=3`走不走索引?
- 走索引,但只用到了联合索引中的`a`列;`c`列因跳过了中间的`b`列,无法使用索引,属于“部分索引失效”。
32. 索引创建实战
表结构:
order_info(order_id, user_id, create_time, status, amount)
题干:给订单表的`user_id`、`create_time`字段创建索引,优化查询效率。
最优答案:
-- 单列索引(适合单独查询user_id或create_time的场景)
-- 单列索引(适合单独查询user_id或create_time的场景)CREATE INDEX idx_order_userid ON order_info(user_id);CREATE INDEX idx_order_createtime ON order_info(create_time);-- 联合索引(适合常用user_id+create_time组合查询的场景,效率更高)CREATE INDEX idx_order_userid_createtime ON order_info(user_id, create_time);
-- 联合索引(适合常用user_id+create_time组合查询的场景,效率更高)CREATE INDEX idx_order_userid_createtime ON order_info(user_id, create_time);
核心思路:
优先给查询条件、表关联字段、排序/分组字段创建索引,精准提升查询效率
常用组合查询(如`WHERE user_id=123 AND create_time>'2025-01-01'`),创建联合索引更高效,需遵循最左前缀原则
联合索引中,区分度高的字段(如user_id)放前面,过滤效果更好,进一步提升查询速度
面试官追问:如果查询是`WHERE create_time > '2025-01-01' AND user_id = 123`,联合索引应该怎么建?
- 应创建联合索引 `(user_id, create_time)`;因为`user_id`是等值查询,放在前面能快速过滤出目标用户,再通过`create_time`范围查询,过滤效果和效率最优。
33. EXPLAIN解读案例
题干:给出以下EXPLAIN结果,判断问题在哪,如何优化。
+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+| 1 | SIMPLE | order | ALL | NULL | NULL | NULL | NULL | 100000 | Using where |+----+-------------+-------+------+---------------+------+---------+------+--------+-------------+
最优答案:
问题:`type=ALL`表示全表扫描,`key=NULL`说明未使用任何索引,需扫描100000行数据,查询效率极低;
原因:查询的`WHERE`条件(或过滤条件)未命中任何索引,导致MySQL只能全表遍历筛选数据;
优化方案:给`WHERE`条件中涉及的字段(如user_id、create_time等)创建对应索引,减少扫描行数。
优化后EXPLAIN预期:
+----+-------------+-------+------+-----------------+-----------------+---------+-------+------+-------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+-----------------+-----------------+---------+-------+------+-------------+| 1 | SIMPLE | order | ref | idx_user_id | idx_user_id | 8 | const | 10 | Using where |+----+-------------+-------+------+-----------------+-----------------+---------+-------+------+-------------+
面试官追问:`rows=100000`和`rows=10`,性能差距多大?
- 性能差距约10000倍:索引优化后,扫描行数从10万行缩减到10行,大幅减少磁盘IO和内存占用,查询速度显著提升。
34. 分页查询优化(深分页)
题干:`SELECT * FROM order_info ORDER BY id LIMIT 100000, 10;` 这条SQL在大数据量下很慢,如何优化?
最优答案:
-- 方案1:延迟关联(先查ID,再查完整数据,减少回表开销)SELECT o.*FROM order_info oINNER JOIN ( SELECT id FROM order_info ORDER BY id LIMIT 100000, 10) t ON o.id = t.id;-- 方案2:记录上次最大ID(适用于翻页场景,性能最优)SELECT * FROM order_infoWHERE id > 上次查询的最大ID -- 如上次最大ID为100000,则写id > 100000ORDER BY id LIMIT 10;-- 方案3:使用覆盖索引(先查索引字段,再回表,减少扫描数据量)SELECT o.* FROM order_info oINNER JOIN ( SELECT id FROM order_info ORDER BY id LIMIT 100000, 10) t ON o.id = t.id; -- 与方案1类似,核心是利用索引查ID
核心思路:
原始SQL问题:`LIMIT 100000,10` 会让MySQL先扫描前100010行数据,再丢弃前100000行,只保留10行,冗余扫描严重;
优化核心:减少无效扫描行数,或利用索引避免全表扫描、减少回表次数。
面试官追问:为什么`WHERE id > last_id`比`LIMIT offset`快?
- 因为`id`是主键(默认有索引),`WHERE id > last_id`会直接利用主键索引定位到起始位置,只扫描后续10行;而`LIMIT offset`需要从头扫描到offset位置,无效扫描量大,尤其offset越大,性能差距越明显。
九、高频口述真题(直接背诵)
35. 内连接和左连接的区别?(口述)
INNER JOIN(内连接):只返回两张表中匹配(关联条件成立)的记录,取两表交集;
LEFT JOIN(左连接):左表所有记录全部返回,右表只返回与左表匹配的记录,右表无匹配则补`NULL`;
测试场景适配:左连接常用于全量数据校验(如订单表与支付表对账),可快速找出左表有、右表无的缺失数据。
36. WHERE和HAVING的区别?(口述)
37. ROW_NUMBER()、RANK()、DENSE_RANK()的区别?(口述)
函数 | 核心特点 | 示例(排序值:100,100,90,80) |
|---|
`ROW_NUMBER()` | 连续不重复,即使值相同,也分配不同排名 | 1,2,3,4 |
`RANK()` | 值相同时并列,并列后排名跳跃 | 1,1,3,4 |
`DENSE_RANK()` | 值相同时并列,并列后排名连续(无跳跃) | 1,1,2,3 |
38. UNION和UNION ALL的区别?(口述)
UNION:合并多个查询结果,自动去重 + 排序,性能较差(需额外处理去重和排序);
UNION ALL:直接合并查询结果,不去重、不排序,性能最优;
测试场景适配:测试中优先使用UNION ALL(如合并多表测试数据),无需额外去重,提升效率。
39. 如何查询并删除重复数据?(口述)
查询重复数据:按重复字段(如phone、id_card)分组,用`HAVING COUNT(*) > 1`筛选,示例:SELECT phone, COUNT(*) AS cnt FROM user_info GROUP BY phone HAVING cnt > 1;
删除重复数据(保留最小ID):先按重复字段分组取最小ID,删除不在该集合中的记录,示例: DELETE FROM user_info WHERE id NOT IN (SELECT MIN(id) FROM user_info GROUP BY phone);
40. CHAR和VARCHAR的区别?(口述)
数据类型 | 核心特点 | 适用场景 |
|---|
`CHAR(n)` | 固定长度,不足n位自动补空格,空间换时间,查询速度快 | 长度固定的字段(如手机号、身份证号、性别) |
`VARCHAR(n)` | 可变长度,按实际内容占用空间,节省磁盘空间 | 长度不固定的字段(如地址、商品描述、用户名) |
41. 事务的ACID是什么?(口述,必背)
A(原子性):事务是一个不可分割的整体,事务中的所有操作要么全部成功,要么全部失败(回滚);
C(一致性):事务执行前后,数据的完整性约束不被破坏(如主键唯一、外键关联正常);
I(隔离性):多个事务并发执行时,互不干扰,一个事务的执行不会影响其他事务的结果;
D(持久性):事务提交后,数据会永久保存到数据库中,即使数据库崩溃,数据也不会丢失。
42. 索引的优缺点?(口述)
43. 如何优化`COUNT(*)`性能?(口述)
非精确统计(适合估算场景):查询information_schema系统表,无需扫描业务表,速度极快,示例: SELECT table_rows FROM information_schema.tables WHERE table_name = 'order_info';
精确统计(适合业务校验场景):给查询的WHERE条件字段创建索引,避免全表扫描;若无需查询所有字段,可用`COUNT(id)`(主键索引效率高)替代`COUNT(*)`。
44. DELETE和TRUNCATE的区别?(口述)
操作 | 是否可回滚 | 是否重置自增ID | 执行速度 |
|---|
`DELETE` | 是(事务内可回滚) | 否(自增ID继续递增) | 慢(逐行删除,记录日志) |
`TRUNCATE` | 否(直接释放数据页,不记录日志) | 是(自增ID重置为1) | 快(适合批量删除全表数据) |