ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

SQL面试15题精讲:从基础查询到索引优化与SQL注入防御

SQL面试15题精讲:从基础查询到索引优化与SQL注入防御 1. 先别急着背题这15道SQL面试题到底在考什么如果你正在准备后端开发、数据分析或者数据开发岗位的面试SQL题基本是跑不掉的。我做面试官这几年发现一个特别普遍的现象很多人简历上写着“熟练使用SQL”真让他手写一道场景题连LEFT JOIN后数据翻倍的原因都答不上来。所以这份15道常用SQL面试题的清单不是让你死记硬背而是帮你把“会写”变成“写对”。这15道题按照面试中出现频率和考察维度大致分成五个梯队基础查询与聚合COUNT的坑、去重查询、GROUP BY与HAVING的配合。连接与子查询LEFT JOIN的重复问题、IN/EXISTS/JOIN的语义差异、互斥查询、差分集合。窗口函数与复杂分析ROW_NUMBER/RANK/DENSE_RANK的区别、第N高工资、累计求和。SQL优化与索引慢SQL排查路线、索引失效场景、联合索引最左前缀。安全与问题排查SQL注入原理、预编译为什么能防注入。你会发现这份清单基本覆盖了日常开发里最常踩坑的点。面试官抛出这些题并不是真想让你默写语法而是想看你在“数据不干净、关联有重复、查询性能差”这些真实场景下怎么思考和应变。2. 基础查询与聚合四道题看出你的基本功2.1 第1题COUNT(*)和COUNT(列)有什么区别这题看起来简单翻车率却很高。很多人脱口而出“没区别”那面试官大概率会追问一句如果这一列里存在NULL结果会不会变直接看例子。假设有一张员工表SELECT COUNT(*) FROM emp; SELECT COUNT(dept_id) FROM emp;COUNT(*)统计的是表中的行数哪怕整行全是NULL也会被算进去。COUNT(dept_id)统计的是dept_id这一列里“非NULL”的记录数。如果dept_id允许为空而某几个员工没有部门那么这两个数一定不相等。延伸考点还有一个常用变形COUNT(DISTINCT 列)用来统计某个列的去重非空数量。比如统计有多少个不同的部门编号可以写SELECT COUNT(DISTINCT dept_id) FROM emp;如果在这个基础上还要统计每个部门的员工数就顺势引出下一道题。2.2 第2题查出重复的订单号怎么写去重查询是面试里的高频题对应到业务里最常见的就是订单重复、用户重复注册、日志重复上报。题目一般会给你一张订单表说“订单号本应唯一但实际数据里出现了重复请把重复的找出来”。先定义清楚什么叫“重复”如果业务上判断重复的依据是订单号order_no那就按它分组再统计数量SELECT order_no, COUNT(*) AS cnt FROM orders GROUP BY order_no HAVING COUNT(*) 1;这里有个细节如果同一订单号可能被多个不同用户使用那“重复”的定义就不是只看订单号可能要GROUP BY order_no, customer_id。所以答这道题的时候先反问面试官“重复的判定维度是什么”反而是一个加分动作。如果面试官进一步问能不能把重复记录的完整信息都列出来可以用窗口函数SELECT * FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY order_no ORDER BY id) AS rn FROM orders ) t WHERE t.rn 1;这里rn 1表示每组里的第一条记录rn 1都是后面重复出来的适合先排查再清理。2.3 第3题把重复数据删得只剩一条怎么做这是第2题的进阶版面试里属于“手写题”的高频变体。很多人在这一步会把DELETE语句写得极其危险甚至直接在子查询里关联目标表报错后当场卡住。在 MySQL 8.0、SQL Server、PostgreSQL 等支持CTE的数据库里推荐用DELETE配合窗口函数WITH del AS ( SELECT id, ROW_NUMBER() OVER(PARTITION BY order_no ORDER BY id) AS rn FROM orders ) DELETE FROM del WHERE rn 1;这段SQL的意思是按订单号分组每组里的行按id从小到大排序保留第一条其余全部删除。执行前建议先跑一遍SELECTSELECT COUNT(*) FROM orders;然后备份或者至少开一个事务确认删除条数符合预期后再提交。如果面试环境不允许事务也要明确告诉面试官“生产环境一定会先备份”。MySQL 5.7 及以下版本不支持这种直接对CTE删除不少人会踩“不能在同一张表里子查询删除”的坑。这时候包一层临时表即可DELETE FROM orders WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER(PARTITION BY order_no ORDER BY id) AS rn FROM orders ) t WHERE t.rn 1 );外层子查询多包一层的目的是骗过 MySQL 的1093限制也是面试里一个很实用的“区分度”考点。2.4 第4题GROUP BY和HAVING的经典陷阱这题常见问法是统计每个部门的员工数并且只显示人数大于10的部门你怎么写正确写法SELECT dept_id, COUNT(*) AS emp_cnt FROM emp GROUP BY dept_id HAVING COUNT(*) 10;容易写错的地方有两个。第一把COUNT(*) 10写到WHERE里。SQL 的执行顺序是FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY。WHERE在分组聚合之前执行那时COUNT(*)还不存在所以写了就会报错。第二在SELECT里选择了一些没有出现在GROUP BY中的普通列。例如SELECT dept_id, emp_name, COUNT(*) FROM emp GROUP BY dept_id在 MySQL 的only_full_group_by模式下会直接报错在 SQL Server 里也属于不合法语句。道理很简单每个组可能有多个emp_name数据库不知道该返回哪一条。3. 连接与子查询笔试题里的“翻车重灾区”3.1 第5题LEFT JOIN之后数据翻倍怎么办这题我几乎每次面试都会问因为业务开发里太常见了。比如订单表和订单明细表一张订单对应多条明细SELECT o.order_id, o.total_amount, oi.item_id, oi.item_amount FROM orders o LEFT JOIN order_items oi ON o.order_id oi.order_id;orders里只有 100 条记录关联order_items后可能变成 300 条因为一张订单有多行明细。如果这时候想统计每张订单的明细金额合计直接SUM(oi.item_amount)会得到正确结果吗如果只是按订单分组其实可以。但如果你在外层继续SUM(o.total_amount)订单主表的金额就会被重复累加结果错得离谱。正确的做法是先对明细表做分组聚合再和主表关联SELECT o.order_id, o.total_amount, t.item_count, t.item_total_amount FROM orders o LEFT JOIN ( SELECT order_id, COUNT(*) AS item_count, SUM(item_amount) AS item_total_amount FROM order_items GROUP BY order_id ) t ON o.order_id t.order_id;很多新人觉得多一层子查询很麻烦但实际查询里“先缩小再连接”往往比“先连接再聚合”更快也更容易控制结果条数。答这道题时把“为什么要先在子查询里聚合”讲清楚面试官基本就会放过你。3.2 第6题IN、EXISTS、JOIN的区别怎么答才不踩雷这是一道经典中的经典。网上答案五花八门但很多都说得太绝对比如“数据量大用EXISTS数据量小用IN”。这种话面试官一听就知道你是背的因为现代数据库优化器早就会把IN改写成半连接并不存在“铁律”。更合理的回答框架是先讲语义差异再讲性能判断方法。IN适合“子查询结果集小”的场景表达上最直观。EXISTS是“只要存在就返回”适合判断关联数据是否存在的场景经常搭配相关子查询。JOIN除了判断存在性还可以把关联表的列带出来是最灵活的连接方式。举个例子查找有退款记录的订单SELECT o.order_id FROM orders o WHERE EXISTS ( SELECT 1 FROM refunds r WHERE r.order_id o.order_id );这段SQL和下面这段IN版本语义上等价SELECT order_id FROM orders WHERE order_id IN (SELECT order_id FROM refunds);如果refunds.order_id上建了索引EXISTS通常能利用索引快速判断但IN在优化器里也未必差。真正专业的回答是“写完之后看执行计划确认有没有出现不必要的全表扫描再决定用哪种。”3.3 第7题查询“买过A但没买过B的客户”怎么写这道题非常考验逻辑能力。假设有一张流水表t_order字段包括user_id、item_code数据里同一个用户可能买了 A也可能买了 B甚至两个都买过。请找出买过 A 但从来没买过 B 的用户。推荐写法是用GROUP BY配合条件聚合成布尔值SELECT user_id FROM t_order GROUP BY user_id HAVING SUM(CASE WHEN item_code A THEN 1 ELSE 0 END) 0 AND SUM(CASE WHEN item_code B THEN 1 ELSE 0 END) 0;第一句条件表示“至少买过一次 A”第二句表示“从未买过 B”。这种写法比套两层NOT EXISTS容易理解而且只扫一遍表。如果面试官追问“如果item_code存在NULL呢”你要意识到SUM(CASE WHEN ...)遇到NULL会跳过所以统计逻辑不受影响但如果你写COUNT就要小心NULL值被漏掉的问题。3.4 第8题找“存在但不存在于另一张表”的记录用什么写法这道题经常和“漏单”“未退款”的业务场景绑定。比如查“没有退款记录的订单”。有几种写法-- 写法一NOT EXISTS SELECT o.order_id FROM orders o WHERE NOT EXISTS ( SELECT 1 FROM refunds r WHERE r.order_id o.order_id ); -- 写法二LEFT JOIN IS NULL SELECT o.order_id FROM orders o LEFT JOIN refunds r ON o.order_id r.order_id WHERE r.order_id IS NULL; -- 写法三NOT IN小心 NULL 陷阱 SELECT o.order_id FROM orders o WHERE o.order_id NOT IN (SELECT order_id FROM refunds);三种写法的核心区别在于NOT IN有一条著名的坑如果子查询结果里有NULLNOT IN的整个查询会返回空集合。原因是NULL参与或比较时返回“未知”整行最终被过滤掉。所以我会建议面试时优先答NOT EXISTS或LEFT JOIN IS NULL顺带解释一句“如果子查询列上有NULL我不会选NOT IN”这个细节相当加分。写法可读性NULL 陷阱常见误区NOT EXISTS中等基本没有有人以为相关子查询一定慢LEFT JOIN ... IS NULL直观需要理解关联结果忘记判断被关联表的主键为空NOT IN最好读子查询含NULL时直接全空最容易出现隐藏 Bug4. 窗口函数近几年面试里最亮眼的送分点4.1 第9题ROW_NUMBER、RANK、DENSE_RANK有什么区别窗口函数几乎是近三年 SQL 面试的必考点而这三个排序函数的区别则是必备题。给你一张成绩表按课程分组对分数排序SELECT student_id, course, score, ROW_NUMBER() OVER(PARTITION BY course ORDER BY score DESC) AS row_num, RANK() OVER(PARTITION BY course ORDER BY score DESC) AS rank_num, DENSE_RANK() OVER(PARTITION BY course ORDER BY score DESC) AS dense_rank_num FROM exam_score;假设某个课程里有两个学生都考了 88 分排名结果可能是学生分数ROW_NUMBERRANKDENSE_RANKA92111B88222C88322D70443可以看到ROW_NUMBER即使分数相同也会给一个连续且不重复的编号常用于删除重复、分页等场景RANK遇到并列会跳过后续编号比如两个第二名之后直接是第四名DENSE_RANK不跳号两个第二名之后还是第三名更适合“比赛排名”这种业务。面试时不要只背结论最好现场写一段示例SQL演示三者的输出差异这比任何口头解释都有说服力。4.2 第10题第二高工资的三种写法这道题我在前面已经铺垫过它其实有多个版本第二高、第N高、每组第二高。最入门的是“第二高工资”。假设有一张员工表emp(id, name, salary)查询工资第二高的员工信息。第一种用MAX排除最高值SELECT MAX(salary) AS second_highest_salary FROM emp WHERE salary (SELECT MAX(salary) FROM emp);第二种用窗口函数逻辑更通用SELECT salary FROM ( SELECT salary, RANK() OVER(ORDER BY salary DESC) AS rk FROM emp ) t WHERE t.rk 2;如果查“第N高”直接把RANK后面的数字换成 N 就行。第三种LIMITOFFSET。MySQL 写法SELECT DISTINCT salary FROM emp ORDER BY salary DESC LIMIT 1 OFFSET 1;SQL Server 里对应写法是ORDER BY salary DESC OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY。这道题最容易翻车的点是只记得LIMIT 1 OFFSET 1却忘了加DISTINCT导致最高工资有并列时第二行仍然是最高工资。所以我在面试时会格外留意候选人有没有处理这个边界。4.3 第11题累计求和为什么一定要用窗口函数“统计每个用户截至当前的累计消费金额”是数据分析岗很常见的一道题。普通GROUP BY会把明细行压平而窗口函数可以在保留明细行的同时计算累计值SELECT order_id, user_id, order_date, amount, SUM(amount) OVER(PARTITION BY user_id ORDER BY order_date) AS running_total FROM orders;这里的执行逻辑是按用户分区在分区内按日期排序然后逐行累加金额。第一行是第一条订单金额第二行是前两笔之和以此类推。有一个细节特别容易写错如果只在OVER里写PARTITION BY user_id而不写ORDER BY order_date那么SUM会把整个分区的金额一次性加起来而不是逐行累计。这个ORDER BY决定了窗口是从“分区起始行”到“当前行”没有它就不会有累计效果。面试里还可以顺带讲一下LAG和LEAD比如计算日环比增长SELECT sales_date, amount, LAG(amount, 1) OVER(ORDER BY sales_date) AS prev_amount FROM daily_sales;能主动说出窗口函数的典型使用场景本身就是加分项。5. 优化与实践题慢SQL问题不能只背结论5.1 第12题一条慢SQL给你你打算怎么排查慢SQL优化题现在几乎每场必问但很多人只会说“加索引”这基本等于没答。面试官想听的是完整排查思路下面这套“四步法”可以直接照搬。第一步定位慢SQL。MySQL 可以看慢查询日志和performance_schemaSQL Server 可以通过sys.dm_exec_query_stats、sys.dm_exec_sql_text这类动态管理视图找高耗时SQL。一线实践中我经常是直接拿一条接口里最慢的语句开始分析。第二步查看执行计划。MySQL 用EXPLAINSQL Server 用“显示估计的执行计划”或SET STATISTICS IO ON。重点看有没有全表扫描ALL、有没有额外的排序操作Using filesort、有没有临时表Using temporary。第三步确认索引与数据量。分析是否缺索引、索引是否被函数或隐式转换“屏蔽”、统计信息是否过期。小表全表扫描可能并不慢不用盲目加索引。第四步改写SQL。比如精简查询列去掉SELECT *把多层子查询改成JOIN把OR改成UNION ALL把查询拆小分批处理。你可以这样回答面试官我不会一上来就优化SQL而是先确认这是偶发慢还是持续慢再结合执行计划判断瓶颈在 IO 还是 CPU最后再改SQL和索引。这样说明你有实战经验而不是背面试题。5.2 第13题索引失效的典型场景有哪些这道题的关键不是背出一堆名词而是能现场解释“为什么失效”。常见场景有这么几类。第一对索引列做函数或运算。比如SELECT * FROM orders WHERE DATE(create_time) 2024-01-01;这个写法会把create_time直接用函数包裹导致索引列失去有序性。更好的写法是范围条件SELECT * FROM orders WHERE create_time 2024-01-01 AND create_time 2024-01-02;第二隐式类型转换。比如mobile列是varchar查询时写成WHERE mobile 13800138000不带引号数据库可能会把列转为数值再比较索引就会失效。第三LIKE前导通配符。LIKE %关键词无法利用 B树叶子节点的顺序查找只能扫描但LIKE 关键词%是可以走索引的。第四OR连接非索引列。比如WHERE id 1 OR status 2如果status没有索引优化器可能选择整个表扫描。这种情况可以拆成UNION ALL或为两个列建联合索引。记住一个原则索引失效本质上不是“索引坏了”而是优化器认为“走索引的成本比全表扫描还高”。回答时不要说得太绝对体现出你会用执行计划验证会更有说服力。5.3 第14题联合索引的“最左前缀”怎么解释联合索引是优化类题目里的高级考点常见问法是“表里有多个查询条件我建了一个(a, b, c)的联合索引哪些查询能用到”先记住联合索引的物理特征底层 B树先按第一列a排序a相同再按b排序b相同再按c排序。就像电话簿先按姓氏排序姓氏相同再按名字排序一样。查询条件能否用到联合索引原因WHERE a ?能使用了索引最左列WHERE a ? AND b ?能按顺序匹配前两列WHERE a ? AND b ? AND c ?能完全匹配WHERE b ?不能跳过了最左列aWHERE a ? AND c ?部分能用a能定位c无法继续精确过滤WHERE a ? AND b ?部分能用a是范围条件后b通常不能精确定位最后两行是面试官最爱追问的点最左前缀并不等价于“只要查询里包含第一列就行”还跟条件顺序、范围匹配有关。如果面试问“联合索引里哪个字段放前面”一般建议把等值条件放在前面把范围条件或区分度低的排后面。6. 安全与风险题SQL注入没过关一律不合格6.1 第15题SQL注入和“万能密码”原理是什么现在安全类的SQL题越来越常出现尤其是做全栈和后端的岗位。SQL注入的原理其实非常简单系统把用户输入的内容直接当成SQL代码拼接执行了。最典型的万能密码长这样SELECT * FROM users WHERE user_name admin AND pwd 123 OR 1 1;如果输入的用户名是admin密码是123 OR 11最后拼出来的SQL条件就变成(user_name admin AND pwd 123) OR (1 1)因为OR 1 1恒为真整条SQL等价于从用户表里查出所有记录登录校验形同虚设。更严重的注入还能拼接DROP TABLE、UPDATE等语句直接拖库或删库。回答这道题时只要能准确说出“输入被拼进了SQL语句”这一点就已经拿到了大部分分数。千万别只说“用SQL注入攻击系统不安全”要多讲原理和危害。6.2 为什么参数化查询能防住注入参数化查询或叫预编译语句是防御SQL注入最核心的手段。以 Python 的pymysql为例cursor.execute( SELECT * FROM users WHERE user_name %s AND pwd %s, (user_name, pwd) )这里的%s是占位符真正的用户输入会作为参数传给数据库驱动数据库端会把它当成“数据值”而不是“可执行代码”。所以即使用户输入 OR 11它只会被当作一个普通的字符串值去和pwd列比较永远不会参与SQL语法解析。同类写法在 C# 里是SqlParameter在 Java 里是PreparedStatement在 MyBatis 里是#{}而不是${}。一个常见的误区是“我已经做过字符串过滤了把单引号去掉就行。”这种黑名单思路并不可靠因为SQL注入的变形方式太多了。最稳妥的方案永远是参数化查询再加上最小权限的数据库账号比如应用账号只给SELECT/INSERT/UPDATE/DELETE权限不给DROP/ALTER权限。此外还有一些输入没法参数化比如ORDER BY后面的排序列名、表名。这类情况只能做白名单校验不允许用户直接传任意字符串拼进去。7. 面试官不会明说但一定会观察的三个细节最后说点题外话。我平时问SQL题并不是想找一个能把15道题全部背得一字不差的候选人而是想看他遇到没见过的SQL场景时的反应。第一个细节是“先问业务再动手”。比如重复订单这道题有人上来就写GROUP BY有人先问“重复的定义是按订单号还是按客户”后者明显更贴近真实生产。第二个细节是“答不出来也要给思路”。比如没接触过窗口函数你可以说“我可能先想到用子查询加临时表实现但我也了解窗口函数能更高效地处理这类问题”。这种回答至少让面试官看到你有问题拆解能力。第三个细节是“会验证自己的结论”。老手写SQL通常会在生产环境执行前先看执行计划先跑SELECT COUNT(*)再动手DELETE。这种习惯比多背几道题值钱得多。这份15道题清单不是终点。建议你自己建几张有冗余、有重复、有NULL的表把每道题亲手执行一遍看看结果和预想是否一致。踩过一遍坑面试时才真的扛得住追问。
返回列表