
记得我第一次带复习课时摸底练习就让人哭笑不得。选择题大家都觉得不难可一到写查询语句要么条件位置放错要么多表关联写成了无连接的笛卡尔积要么统计结果莫名其妙翻了一倍。表面看是语法不熟实际上是对“题目到底在问什么”和“数据表之间是什么关系”这两件事没想清楚。这篇复习笔记就按我上课的思路来写先把考点框架立起来再用一套三表业务库把查询与统计的底层逻辑走一遍最后拆解四类必考题和一套自测题库。用 SQL 练习的读者可以直接上手用 Excel 做数据分析的读者也能对照理解因为筛选、分组、汇总这套逻辑在不同工具里是同一条主干。1. 先搞清楚复习对象这一节到底在考什么1.1 三种考查层次读得懂表、查得出数、说得清口径数据查询与统计这门课的题目出题人绕来绕去其实只考三个层次。第一层叫“读得懂表”。拿到一个数据库先看有几张表、每张表的主键是什么、表和表之间靠哪个字段关联、字段是字符型还是数值型、哪些字段可能为空。大部分人在连接查询上翻车不是不会写 JOIN而是压根没看出来订单表的用户编号要和用户表的主键做关联。所以复习第一步不是背语句而是练“看表”。第二层叫“查得出数”。这部分考的是把业务问题翻译成查询语句的能力。题目说“找出所有下过单的用户姓名”你的脑子里要立刻反应出姓名在 users 表下单记录在 orders 表两张表通过 uid 关联所以要用连接或者子查询。这条翻译链路只要慢半拍后面的语句一定写不顺畅。第三层叫“说得清口径”。很多统计结果对不上问题出在“统计对象”没锁定统计的是订单量还是订单人数是订单金额还是商品销量用 COUNT(*) 还是 COUNT(DISTINCT uid)我要求学生在每道题旁边写一句话“我在统计什么按什么维度分组”写不出来的题做错也就不冤了。复习的核心就是把这三个层次反复滚熟。1.2 一套贯穿全文的练习载体三表业务库为了不搞得抽象我所有例题都基于同一套模拟业务库你可以把它当作一套练习题的标准背景。一共三张表结构如下users用户表uid 主键name 用户名age 年龄city 城市reg_date 注册日期products商品表pid 主键name 商品名cate 分类price 单价orders订单表oid 主键uid 下单用户pid 商品qty 数量amount 订单金额order_date 下单日期示例数据不用列太多理解关系就行一个用户可以有多个订单一个商品也可以出现在多个订单里。也就是说users 到 orders 是一对多products 到 orders 也是一对多。这个“一对多”关系是整个查询与统计章节里最重要的底层结构后面会遇到的大量丢分案例追根溯源都是没处理好一对多。为什么用三张表而不是一张大宽表因为在真实场景里数据几乎不会放在同一张表里多表连接和分组统计正是从这里长出来的。把这三张表的关系在纸上画清楚后面做题就是套公式。2. 查询题的底层逻辑从“取数”到“加工”的完整链条2.1 连接查询与子查询的边界选择学生经常问我这道题到底该用 JOIN 还是子查询我的判断方法很简单看结果需要哪些字段。如果查询结果需要同时展示多张表的字段比如要输出“用户姓名 商品名称 订单金额”姓名在 users商品名在 products金额在 orders那就必须 JOIN。如果只是需要一个值作为过滤条件比如“订单金额高于全部订单平均金额”就可以先子查询算出平均金额再拿这个值去 WHERE 里比较。举个例子题目要查“订单金额高于全局平均金额的订单并显示商品名称”SELECT p.name, o.amount FROM orders o JOIN products p ON o.pid p.pid WHERE o.amount (SELECT AVG(amount) FROM orders);子查询先算出一个常量外层查询拿它逐行比较JOIN 则负责把商品名称带出来。两个环节各有任务配合起来才完整。连接查询还有一个经典陷阱ON 条件写漏。比如 users 和 orders 关联只写 FROM users, orders 而不写关联条件产生的是笛卡尔积一个用户会跟所有订单配对行数瞬间失控。机试时答案看上去“好像有点数据”实际全错。复习时要把“凡是多表查询先写关联条件再考虑其他”这句话刻进脑子里。2.2 WHERE、HAVING 的过滤时机WHERE 和 HAVING 的差别是这一节的必考点也是很多人绕不过去的弯。我把它压缩成一句话WHERE 在分组之前过滤行HAVING 在分组之后过滤组。关键词过滤时机适用场景常见错误WHERE分组之前对每一行判断原始行数据的条件筛选试图在 WHERE 里写聚合条件HAVING分组与聚合之后对每组判断对聚合结果设置条件把聚合条件错误地放进 WHERE比如“统计商品数量不少于 2 个的分类”SELECT cate, COUNT(*) AS cnt FROM products GROUP BY cate HAVING COUNT(*) 2;因为“商品数量不少于 2 个”是对每组统计之后才能判断的COUNT() 在 WHERE 阶段还不存在所以必须用 HAVING。如果你在 WHERE 后面写 COUNT() 2数据库会直接报错。有个细节值得注意SQL 的执行顺序不是按书写顺序来的。先执行 FROM再 WHERE然后 GROUP BY接着 HAVING最后才是 SELECT 和 ORDER BY。理解这个顺序很多“为什么不能这样写”的问题都会迎刃而解。Excel 里做数据透视表时对分类字段做“值筛选”本质上就对应着 HAVING。2.3 排序、去重与输出控制的细节排序题看起来简单丢分常常丢在细节上。ORDER BY 后面可以跟多个字段先按第一个字段排字段值相同再按第二个字段排。例如“金额降序金额相同时数量升序”SELECT oid, amount, qty FROM orders ORDER BY amount DESC, qty ASC;很多同学会把 DESC 和 ASC 的位置写错或者只给第一个字段写了排序方向导致第二个字段用了默认升序和题意不符。去重是第二个重灾区。DISTINCT 是对整个结果行去重不是对某一个字段去重。查询“有哪些用户下过单”写成 SELECT DISTINCT uid FROM orders 是对的但如果想同时显示用户姓名就必须连接用户表把 uid 和 name 一起 SELECT DISTINCT。如果有人误以为 DISTINCT 只作用于一个字段结果里明明重复的 uid 还可能因为 name 不同而保留那就错了。LIMIT 用来限制输出行数。注意顺序先 WHERE 过滤再 ORDER BY 排序最后 LIMIT 截断。常见错误是忘写 ORDER BY 直接 LIMIT取出来的“前几条”顺序没有保证。哪怕结果碰巧对了逻辑上也是不严谨的。3. 统计题的关键分组思维是唯一的分水岭3.1 分组的基本语义先分堆再统计GROUP BY 是整个统计章节的分水岭。我的类比很简单把一堆行按照某个字段的值分堆字段值相同的行放进同一个堆之后再对每个堆做聚合计算。比如 GROUP BY cate就是把所有商品按分类分堆然后统计每一堆有多少行、价格平均值是多少。分堆之后有一道铁律SELECT 里出现的非聚合列必须出现在 GROUP BY 里。换句话说SELECT 后面如果写了 cateGROUP BY 里必须也有 cate如果写了 pidGROUP BY 里就得有 pid。否则对于每个分类堆系统不知道该取哪一个 pid 来显示在不严格的模式下可能返回随机值在严格模式下直接报错。复习时不用纠结凡是违反这条规则的语句一律视为错误写法。还要纠正一个误解有人以为 GROUP BY 会自动排序。某些数据库在结果上看起来像排过序但那只是执行计划碰巧如此不能依赖。语句里需要明确顺序就在最后补 ORDER BY。3.2 聚合函数的选型与 NULL 陷阱聚合函数是统计题的基本零件选错一个整道题就废了。我把常用函数和易错点列在一起函数作用易错点COUNT(*)统计行数不忽略 NULL 行COUNT(列名)统计该列非 NULL 的个数自动忽略 NULLSUM求和全部为 NULL 时返回 NULLAVG求平均忽略 NULL不是把 NULL 当 0MAX / MIN求最大 / 最小忽略 NULLCOUNT(DISTINCT 列名)去重后计数用来统计“人数”等场景最容易踩的坑是 COUNT() 和 COUNT(列名) 的混用。COUNT() 数的是结果集有多少行哪怕某行所有字段都是 NULL它也会算进去。COUNT(qty) 则只数 qty 不为 NULL 的行。如果一张订单表里某个字段大量为空这两种写法会算出完全不同的结果。还有一个细节当 SUM 作用在全为 NULL 的列上时结果不是 0而是 NULL。如果题目要统计“未下单用户的累计金额”经常要用 COALESCE 或 IFNULL 把 NULL 兜底成 0否则展示层会显示“空”。3.3 条件统计、去重统计与时间维度的进阶基础的分组统计掌握后进阶考法主要集中在这三种场景。条件统计用 SUM 加 CASE WHEN 实现按条件计数。比如统计“2024 年 1 月的订单数量”可以写成SELECT SUM( CASE WHEN order_date 2024-01-01 AND order_date 2024-02-01 THEN 1 ELSE 0 END ) AS jan_order_count FROM orders;这种写法比 COUNT 加 WHERE 更灵活因为可以在同一个 SELECT 里同时统计多个条件。去重统计统计“下单用户数”必须用 COUNT(DISTINCT uid)。订单表是一行一条明细同一个用户会有多行如果直接 COUNT(*) 或者 COUNT(uid)就会把人头数成订单笔数翻好几倍。这个问题在 Excel 里同样存在直接对订单明细计数永远不等于客户数。时间维度是最容易被忽略的。按月、按年分组统计时要先确认数据库的日期函数再决定分组粒度。更重要的是日期边界条件用半开区间 [起, 止) 更稳妥也就是大于等于月初、小于下月初而不是用字符串截取月份再比较。这样既能保证效率也能避免边界数据被算漏或算重。4. 必考题型拆解从条件筛选到多表连接的四类作答路径4.1 条件查询题先圈定表再写条件这类题是基础中的基础但越是基础越容易掉以轻心。题目“查询年龄在 20 到 30 之间含两端的用户姓名和城市按年龄降序排列。”SELECT name, city FROM users WHERE age BETWEEN 20 AND 30 ORDER BY age DESC;读题时先抓三件事数据从哪张表来、要什么字段、有什么条件。这里只有 users 一张表不需要 JOIN直接 WHERE 过滤。BETWEEN AND 在 SQL 里包含两端如果你担心环境差异写成 age 20 AND age 30 更保险。要注意有些人在 SQL 里写 age 20 age 30这是把别的语言的逻辑符号带进来了标准 SQL 用 AND。这种错误在选择题里反复出现属于典型的“看着眼熟下手就错”。4.2 聚合统计题一个 SELECT 里同时出现多个聚合题目“统计订单总笔数、总销量、总金额、单笔最高金额。”SELECT COUNT(*) AS order_count, SUM(qty) AS total_qty, SUM(amount) AS total_amount, MAX(amount) AS max_amount FROM orders;这个题同时用了四个聚合函数它们各自独立计算互不干扰。COUNT(*) 数订单行数SUM(qty) 把数量列相加SUM(amount) 汇总金额MAX 取最高值。AS 起别名不仅是让结果显示更清楚在机试判卷时也能让阅卷人一眼看懂你的意图。容易错的地方是把 COUNT(*) 和 SUM(qty) 弄混一个数行数一个加数值。比如“总销量”应该 SUM(qty)不是 COUNT(qty)因为每个订单可能买多件COUNT 只能数出订单笔数。4.3 分组统计题GROUP BY 后的两行黄金代码题目“统计每个用户的累计下单金额只返回累计金额超过 500 的用户。”SELECT uid, SUM(amount) AS total_amount FROM orders GROUP BY uid HAVING SUM(amount) 500;这是最典型的分组统计加 HAVING 过滤。判断依据很清晰先按用户分组再用聚合函数算每组金额最后过滤组。这里有个隐藏考点为什么 SELECT 里只写 uid不写 name因为 name 在 users 表orders 表里没有。如果题目要求显示用户姓名就需要连接 users 表同时 GROUP BY uid, name。很多人一看“用户”两个字就直奔 users 表却忽略题目真正要统计的主语是订单金额数据源应该在 orders 表。先判断统计主语再决定用哪张表这是做统计题的基本思路。4.4 连接与子查询题一对多关系如何拆连接题最核心的坑是一对多带来的结果膨胀。题目“查询所有用户的姓名和累计下单金额没下过单的用户也要显示。”SELECT u.name, COALESCE(SUM(o.amount), 0) AS total_amount FROM users u LEFT JOIN orders o ON u.uid o.uid GROUP BY u.uid, u.name;LEFT JOIN 以左侧的 users 为准保留所有用户右侧 orders 没有匹配到的行金额为 NULL用 COALESCE 兜底成 0。GROUP BY 后面写了 u.uid 和 u.name因为 SELECT 里出现了非聚合字段 name。注意不要写 GROUP BY o.uid否则未下单用户会形成一个 NULL 组用户反而不见了。子查询题更考验逻辑题目“查询订单金额高于全表平均金额的订单对应的商品名称”。刚在 2.1 节已经写过答案这里补充一点子查询可以嵌套多层但每层只解决一个问题。先算平均金额这个“值”再用它做过滤最后连接商品表拿名称。一步想不清楚后面的语句全乱。5. 高频丢分点复习中最容易踩的五个坑5.1 GROUP BY 后乱取非分组列这是分组统计题的头号错误。比如按分类统计商品数量却把 pid 也写进 SELECT-- 错误示例 SELECT pid, cate, COUNT(*) FROM products GROUP BY cate;cate 是分组列pid 不是。每个分类堆里有多个不同的 pid数据库不知道该显示哪个。严格模式下直接报错不严格模式下返回的是这一堆里的某一条没有任何业务意义。复习策略很简单写 SELECT 之前先确认哪些列是分组列。SELECT 里出现的所有非聚合列必须一个不落地出现在 GROUP BY 里。宁可多写几个分组列也不要少写。5.2 COUNT(*) 与 COUNT(列) 的语义混淆如果一道题统计“订单数量”大多数人会写 COUNT()这没问题。但如果统计“填写了收货地址的订单数”就应当 COUNT(address)因为没填地址的订单该字段为 NULLCOUNT() 会把这些行也算进去。再往下走一步统计“下单用户数”要用 COUNT(DISTINCT uid)不是 COUNT(uid)。前者去重计数后者把同一个用户的多笔订单重复计算。考试时只要题干的量词是“人数”“客户数”“用户数”优先怀疑自己写的是不是 COUNT(DISTINCT)。5.3 LEFT JOIN 导致计数翻倍连接和聚合组合时最容易翻车。假设你要统计“每个用户下过的订单数量”先做 LEFT JOIN 再 COUNT 订单号逻辑没问题。但如果你统计的是“每个用户出现的次数”COUNT(users.uid)结果会把同一个用户由于多笔订单产生的多行重复计数。很简单的一个验证方法连接之后先跑一次不带聚合的查询看看行数是否变多了。一对多连接后左表行数必然会放大。统计左表字段时要想清楚到底要不要去重。这个习惯在 Excel 的 VLOOKUP 场景里也同样适用一对多匹配时匹配结果重复出现统计就会失真。5.4 日期、数值参与比较时的隐式转换数据查询与统计里日期和数值的坑最隐蔽。比如用字符串和日期字段比较或者对一个字段套了函数再比较都可能产生两类问题一是类型隐式转换导致结果不确定二是对字段做函数处理后索引失效查询效率急剧下降。日期比较的规范写法是用范围区间例如WHERE order_date 2024-01-01 AND order_date 2024-02-01不要写成 LEFT(order_date, 7) 2024-01这样既绕弯又低效。同理数值字段不要隐式转成字符串再比较。复习时把“尽量保持字段原样让条件去匹配字段”这一条记住能避开大量隐形失分。5.5 统计口径漂移订单量、订单人数还是订单金额很多时候结果算出来“差不多”但就是不对问题出在统计口径上。题目说“统计每个城市的订单量”指的是订单笔数应该 COUNT(订单号)“统计每个城市的客户数”指的是下单人数必须 COUNT(DISTINCT 用户编号)“统计每个城市的销售额”才用 SUM(金额)。我让学生做的练习是读题时把题干中的量词圈出来。是“笔数”“人数”还是“金额”动词是“查询”“统计”还是“筛选”每个量词都对应不同的函数和字段。机试判卷不会因为“差不多”给分口径错了整道综合题可能一分不得。6. 自测题库一套可以直接练手的复习卷6.1 基础热身10 分钟快速检查T-1 查询年龄在 20 到 30 之间含两端的用户姓名和城市按年龄降序排列。SELECT name, city FROM users WHERE age BETWEEN 20 AND 30 ORDER BY age DESC;考点条件筛选加排序。BETWEEN AND 包含边界WHERE 在前ORDER BY 在后。T-2 查询商品编号为 P101 且购买数量大于 2 的订单编号与数量按数量升序排列。SELECT oid, qty FROM orders WHERE pid P101 AND qty 2 ORDER BY qty ASC;考点字符型条件要加引号多个条件用 AND注意数量升序是 ASC。T-3 统计分类为“办公用品”的商品总数、最高价格、最低价格。SELECT COUNT(*) AS product_count, MAX(price) AS max_price, MIN(price) AS min_price FROM products WHERE cate 办公用品;考点聚合函数与 WHERE 条件组合。COUNT(*) 数行数MAX 和 MIN 取极端值。6.2 进阶层带坑的经典题T-4 按商品分类统计商品数量只保留商品数量大于等于 2 个的分类并按数量降序排列。SELECT cate, COUNT(*) AS cnt FROM products GROUP BY cate HAVING COUNT(*) 2 ORDER BY cnt DESC;考点分组、HAVING 过滤、排序三合一。最容易错的是把 HAVING 写成 WHERE。T-5 统计每个用户的下单次数和累计金额只显示下单次数大于 1 的用户。SELECT uid, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM orders GROUP BY uid HAVING COUNT(*) 1;考点分组统计加 HAVING。COUNT(*) 是订单笔数SUM(amount) 是总金额。T-6 查询从未下过单的用户姓名。SELECT name FROM users WHERE uid NOT IN (SELECT DISTINCT uid FROM orders);考点子查询。先拿到所有下过单的 uid再在 users 里排除。也可以用 LEFT JOIN 加 IS NULL 写法两种都行关键是理解集合的差集语义。6.3 综合层贴近业务的实战题T-7 统计 2024 年 1 月每个城市的订单总金额按总金额降序。SELECT u.city, SUM(o.amount) AS total_amount FROM orders o JOIN users u ON o.uid u.uid WHERE o.order_date 2024-01-01 AND o.order_date 2024-02-01 GROUP BY u.city ORDER BY total_amount DESC;这道题把所有核心环节都串起来了多表连接、日期边界、分组聚合、排序。日期条件用半开区间注意城市字段来自 users 表JOIN 条件校验一次。T-8 找出每个分类下销量最高的商品名称和销量。SELECT cate, name, total_qty FROM ( SELECT p.cate, p.name, SUM(o.qty) AS total_qty, ROW_NUMBER() OVER ( PARTITION BY p.cate ORDER BY SUM(o.qty) DESC ) AS rn FROM orders o JOIN products p ON o.pid p.pid GROUP BY p.cate, p.name ) t WHERE t.rn 1;窗口函数是加分项支持的数据库可以直接跑。如果课程没学窗口函数可以用“先按分类和商品聚合销量再和相关分类最大销量比较”的子查询改写。这道题的精髓在于“每个分类”四个字它决定了排名必须在分类内进行而不是全局排序取前几名。T-9 统计各城市的下单人数并找出下单人数最多的城市。SELECT u.city, COUNT(DISTINCT o.uid) AS buyer_cnt FROM orders o JOIN users u ON o.uid u.uid GROUP BY u.city ORDER BY buyer_cnt DESC LIMIT 1;这是整套复习里最经典的陷阱题。只要把 COUNT(DISTINCT o.uid) 写成 COUNT(*) 或 COUNT(o.uid)同一个用户下多笔订单就会被重复计算“下单人数”就变成了“下单笔数”。做这道题之前先把量词圈出来人数不是笔数不是金额。做这套复习卷的时候建议大家准备一张草稿纸每道题都按三步走第一步圈题干量词第二步找数据来源表第三步写查询骨架。把这三步练熟第 3 节的复习才算真正到位。我自己带复习时最爱说的一句话是数据查询与统计不是背出来的是顺着表结构和题目量词一步步推出来的。你把这套题反复做个两三遍遇到同类题目就不会再靠猜了。