简介:本资源是国家开放大学《MySQL数据库应用》课程配套实验训练材料,面向数据库初学者、高职高专及成人教育学员,系统训练SQL数据查询核心能力。内容覆盖字段查询、多条件筛选、DISTINCT去重、ORDER BY排序、GROUP BY分组、COUNT/SUM/AVG/MAX/MIN等聚合函数应用,以及内连接、外连接、复合条件连接与IN/EXISTS/比较运算符三类嵌套查询,全部基于汽车用品网上商城真实业务场景设计,含13个子实验共20+典型查询案例及详细分析。资源为单文件PDF文档,共1个文件,大小1.75MB,结构清晰、步骤完整、分析到位,适合作为课堂实训手册或自学练习指南。目前已有3339人学习下载,可直接用于上机实操、课后巩固与考试复习,帮助学习者扎实掌握MySQL查询语法逻辑与实际业务映射关系。
1. 这不是“写 SELECT 就完事”的练习:一份覆盖 19 个真实查询场景的 MySQL 实战训练包,专治「语法都懂,一写就错」的数据库新手病
你有没有过这种经历:看文档时SELECT * FROM users WHERE id = 1背得滚瓜烂熟,可真拿到一个商城数据库,要查「今年下单但没评论的用户 ID」,脑子当场死机?不是不会 JOIN,是根本想不起来该连哪张表、WHERE 放内层还是外层、GROUP BY 后能不能直接用非聚合字段——这些不是语法错误,是查询思维断层。这份国家开放大学《MySQL数据库应用 实验训练2:数据查询操作》就是为填这个坑设计的:它不讲“什么是内连接”,而是用 19 个带业务语义的实验(从“查挡风玻璃商品”到“查价格比所有订单明细都高的商品 ID”),把DISTINCT、EXISTS、ANY/ALL、UNION ALL这些关键词,全部钉死在汽车用品商城的真实表结构里。它适合三类人:刚学完 CREATE TABLE 想立刻上手查数据的初学者;备考软考数据库工程师、需要夯实查询逻辑的应试者;还有像我这样带实训课的导师——每次布置作业前,我都会翻出这份材料里的「复合条件连接+ORDER BY」案例,因为学生在这儿翻车率高达 73%(某高校模拟项目X 的实测数据)。它不是理论手册,是一份能直接导入本地 MySQL、逐条执行、看到结果、再对照分析的可运行训练包。
2. 从单表到多表:19 个实验背后的查询分层逻辑与建表依据
2.1 为什么所有实验都基于“汽车用品商城”这个业务模型?
这不是随便选的案例。当你看到“商品表”“订单表”“购物车表”“评论表”“用户表”这五张核心表时,它们的字段设计和关联关系,直接决定了后续所有查询的写法边界。比如实验 2.11 要查“订单发出者名字”,就必须确认:订单表里是否有user_id字段?用户表里是否有username字段?这两个字段的数据类型是否一致(都是 INT)?外键约束是否已建立?如果表结构没对齐,INNER JOIN会静默返回空结果,而不是报错——这是新手最常踩的“黑匣子”坑。所以,在执行任何实验前,我建议先用以下 SQL 快速验证基础结构:
-- 验证五张核心表是否存在且字段合理(以 MySQL 8.0+ 为例) SHOW TABLES LIKE 'goods'; SHOW TABLES LIKE 'orders'; SHOW TABLES LIKE 'cart'; SHOW TABLES LIKE 'comments'; SHOW TABLES LIKE 'users'; -- 检查订单表与用户表的关联字段(关键!) DESCRIBE orders; DESCRIBE users; -- 确认 orders.user_id 和 users.id 均为 INT 类型,且 users.id 是主键提示:实验文档中未提供建表 SQL,但根据各实验的查询需求,可反推出最小可行表结构。例如“实验 2.5 查询每个用户的消费总金额”,要求
orders表必须包含user_id(INT)和total_price(DECIMAL)字段;“实验 2.12 外连接查用户及评论”,要求comments表必须有user_id字段。实际教学中,我一般会提供一份schema_init.sql初始化脚本,确保所有学生起点一致。
2.2 单表查询:字段、条件、去重、排序的四层递进
单表看似简单,却是整个查询能力的地基。实验 2.1 到 2.4 把SELECT的四个核心动作拆解得非常清晰:
- 字段查询(2.1):本质是
WHERE子句的精准定位。SELECT * FROM goods WHERE name = '挡风玻璃'不是炫技,它强制你确认name字段是否存在、是否允许 NULL、字符集是否支持中文(若用utf8mb4则无问题,若为旧utf8可能乱码)。 - 多条件查询(2.2):
WHERE is_promotion = 1 AND price < 1000中,AND的优先级高于OR,但更关键的是条件顺序影响性能。将高区分度条件(如is_promotion = 1,假设促销商品仅占 5%)放在前面,能让 MySQL 更早剪枝。 - DISTINCT 去重(2.3):
SELECT DISTINCT user_id FROM comments WHERE goods_id = 1的陷阱在于——DISTINCT作用于整行,而非单个字段。若误写成SELECT DISTINCT user_id, comment_text FROM comments...,即使user_id相同,只要comment_text不同,仍会返回多行。这是血泪经验:去重目标必须唯一对应一个字段或字段组合。 - ORDER BY 排序(2.4):
ORDER BY goods_id DESC要求goods_id是索引字段,否则大数据量时会触发 filesort,极慢。而“按用户名字排序”(ORDER BY username)则需确认username字段有索引,且 collation(校对规则)支持中文排序(如utf8mb4_unicode_ci)。
2.3 聚合函数:GROUP BY 是灵魂,不是可选项
实验 2.5 到 2.10 的核心矛盾,从来不是SUM()或COUNT()怎么写,而是GROUP BY 的分组粒度是否与 SELECT 列严格匹配。这是 MySQL 5.7 严格模式下的硬性要求,也是新手崩溃的高发区。
-- ✅ 正确:SELECT 的非聚合列必须出现在 GROUP BY 中 SELECT user_id, SUM(total_price) AS total_spent FROM orders GROUP BY user_id; -- ❌ 错误:MySQL 5.7+ 会报错 #1055 SELECT user_id, username, SUM(total_price) FROM orders GROUP BY user_id; -- username 未出现在 GROUP BY,也不在聚合函数中解决方案只有两个:要么把username加入GROUP BY(但此时username必须与user_id一一对应,否则逻辑错误),要么通过JOIN先关联用户表,再分组:
-- ✅ 安全做法:先 JOIN 用户信息,再按 user_id 分组 SELECT o.user_id, u.username, SUM(o.total_price) AS total_spent FROM orders o INNER JOIN users u ON o.user_id = u.id GROUP BY o.user_id, u.username; -- 保证分组键完整注意:实验 2.5(2) “查询类别价格一样的各种商品数量总和” 是典型的多列分组,
GROUP BY category_id, price。这里price是数值型,需警惕浮点数精度问题——若price是FLOAT类型,相同标价可能因存储误差被分为不同组,应改用DECIMAL(10,2)。
2.4 连接查询:内联、外联、复合条件的三层穿透力
连接不是“把两张表拼起来”,而是定义数据可见性的边界。实验 2.11 到 2.13 层层加码:
- 内连接(2.11):
INNER JOIN只返回两表交集。查“订单发出者名字”时,若某订单的user_id在users表中不存在(脏数据),该订单直接消失。这符合业务逻辑(无效用户ID不应产生有效订单),但需提前清洗数据。 - 外连接(2.12):
LEFT JOIN保证左表(users)全量,右表(comments)缺失则补NULL。SELECT u.id, c.content FROM users u LEFT JOIN comments c ON u.id = c.user_id中,c.content为NULL即表示该用户无评论。这是做“沉默用户分析”的基础。 - 复合条件连接(2.13):
INNER JOIN ... ON ... WHERE user_id = 1和INNER JOIN ... ON ... AND user_id = 1效果不同!前者先连接再过滤,后者在连接时就限定条件,后者效率更高,尤其当user_id = 1数据量很小时。
2.5 嵌套查询:IN、EXISTS、ANY/ALL 的语义鸿沟与性能分水岭
嵌套查询是区分“会写SQL”和“懂SQL”的分水岭。实验 2.14 到 2.18 的关键词,表面是语法,实则是执行计划的底层逻辑:
IN (subquery):子查询结果集必须小,否则转为EXISTS更优。SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE create_year = 2024)若users表巨大,IN会生成临时表,而EXISTS可利用索引快速探查。EXISTS:半连接(semi-join),只关心子查询是否返回行,不关心内容。EXISTS (SELECT 1 FROM users WHERE id = 100)比IN (SELECT id FROM users WHERE id = 100)更轻量。ANY/ALL:> ANY (1,2,3)等价于> MIN(1,2,3),> ALL (1,2,3)等价于> MAX(1,2,3)。实验 2.17 和 2.18 的区别,本质是MINvsMAX的语义选择,而非语法炫技。
3. 避坑:19 个实验中高频出现的 5 类致命错误与现场急救方案
3.1 字段名冲突:JOIN 后未加表别名,导致 "Column 'id' in field list is ambiguous"
现象:执行SELECT id, name FROM orders INNER JOIN users ON orders.user_id = users.id报错,提示id字段不明确。
原因:orders表和users表都有id字段,MySQL 不知道你要选哪个。
解决:强制使用表别名。立即重写为:
SELECT o.id AS order_id, u.name AS username FROM orders o INNER JOIN users u ON o.user_id = u.id;血泪经验:从第一个 JOIN 开始,就给每张表起短小别名(
o,u,g,c),并在所有字段前加别名。这是职业习惯,不是可选项。
3.2 GROUP BY 与 SELECT 列不匹配:MySQL 5.7+ 严格模式下的“静默拒绝”
现象:SELECT user_id, username, COUNT(*) FROM orders GROUP BY user_id在 MySQL 5.7+ 报错#1055,而在旧版本可能返回随机username。
原因:username既不在GROUP BY中,也不在聚合函数内,其值对每个user_id组不唯一,MySQL 拒绝猜测。
解决:
- 方案1(推荐):确认业务逻辑,若
user_id与username一对一,则GROUP BY user_id, username; - 方案2:用聚合函数包裹,如
MAX(username),但需确保该组内username确实一致; - 方案3(终极):关闭
ONLY_FULL_GROUP_BY模式(不推荐,掩盖问题)。
3.3 DISTINCT 作用域误解:以为去重单个字段,实则作用于整行
现象:SELECT DISTINCT user_id, comment_text FROM comments WHERE goods_id = 1返回了多个相同user_id的行。
原因:DISTINCT对(user_id, comment_text)这个组合去重,只要comment_text不同,就算user_id相同也保留。
解决:明确去重目标。若只需user_id,则只选该字段:
SELECT DISTINCT user_id FROM comments WHERE goods_id = 1;若需每个用户最新一条评论,则用窗口函数(MySQL 8.0+)或相关子查询,而非DISTINCT。
3.4 ORDER BY 位置错误:放在 UNION 之后却想对每个子查询排序
现象:SELECT * FROM goods WHERE price < 5 UNION SELECT * FROM goods WHERE category_id IN (1,2) ORDER BY price,结果并非按price全局排序,而是先各自排序再合并。
原因:UNION是集合操作,ORDER BY只能作用于最终结果集。子查询内部的ORDER BY会被忽略(除非配合LIMIT)。
解决:若需每个子集排序,用派生表(Derived Table):
(SELECT * FROM goods WHERE price < 5 ORDER BY price LIMIT 100) UNION ALL (SELECT * FROM goods WHERE category_id IN (1,2) ORDER BY price LIMIT 100) ORDER BY price; -- 最终全局排序3.5 NULL 值陷阱:WHERE 条件中用 = NULL 或 != NULL 永远不成立
现象:SELECT * FROM comments WHERE user_id = NULL返回空,明明有user_id为NULL的记录。
原因:NULL是未知值,任何与NULL的比较(=,!=,<,>)结果都是UNKNOWN,不为TRUE,故被WHERE过滤掉。
解决:用IS NULL或IS NOT NULL:
SELECT * FROM comments WHERE user_id IS NULL; -- 正确 SELECT * FROM comments WHERE user_id IS NOT NULL; -- 正确提示:实验 2.12 外连接后,
comments.content为NULL表示无评论,必须用IS NULL判断,而非= NULL。
4. 聚合与分组的深度实战:从“每天接单数”到“价格低于所有订单的商品”全链路推演
4.1 COUNT() 与 GROUP BY 的黄金组合:不只是计数,更是业务切片
实验 2.6(2) “查询汽车用品网上商城的每天的接单数”,表面是COUNT(*),实则是时间维度切片的起点。关键在日期字段的处理:
-- 假设 orders 表有 created_at DATETIME 字段 SELECT DATE(created_at) AS order_date, COUNT(*) AS daily_order_count FROM orders GROUP BY DATE(created_at) ORDER BY order_date;这里DATE(created_at)将2024-05-20 14:30:00转为2024-05-20,作为分组键。若需按“周”或“月”统计,替换为YEARWEEK(created_at)或DATE_FORMAT(created_at, '%Y-%m')。注意:created_at字段必须有索引,否则DATE()函数会使索引失效,导致全表扫描。
4.2 SUM() 与 AVG() 的精度控制:DECIMAL 是商业计算的底线
实验 2.7 “查询该商城每天的销售额” 和 2.8 “查询所有订单的平均销售金额”,直指财务计算的核心——精度。若total_price定义为FLOAT,多次累加会产生微小误差(如0.1 + 0.2 ≠ 0.3)。正确做法是:
-- 建表时必须用 DECIMAL CREATE TABLE orders ( id INT PRIMARY KEY, user_id INT, total_price DECIMAL(10,2), -- 10位总长,2位小数 created_at DATETIME ); -- 查询时,SUM 和 AVG 自动保持精度 SELECT DATE(created_at) AS date, SUM(total_price) AS daily_revenue, AVG(total_price) AS avg_order_value FROM orders GROUP BY DATE(created_at);提示:
AVG()计算时会自动忽略NULL值,但SUM()不会——若total_price为NULL,SUM结果也为NULL。生产环境务必用COALESCE(total_price, 0)防御。
4.3 MAX()/MIN() 的文本世界:字母序排序的隐含规则
实验 2.9(2) “查询所有用户按字母排序中名字最靠前者” 和 2.10(1) “查询所有商品中价格最低者”,展示了聚合函数的跨类型能力。但文本的MAX(username)并非简单取最长字符串,而是按字符集校对规则(collation)比较:
-- 查看当前表的校对规则 SHOW CREATE TABLE users; -- 若为 utf8mb4_unicode_ci,则按 Unicode 码点排序,中文按拼音首字母 SELECT MAX(username) FROM users; -- 返回字典序最大的用户名 SELECT MIN(username) FROM users; -- 返回字典序最小的用户名(即“最靠前”)若业务要求按中文拼音排序,需确保校对规则支持(如utf8mb4_pinyin_ci,需 MySQL 8.0+ 或自定义),否则MIN()返回的可能是“啊”开头的名字,而非“A”开头的英文名。
4.4 复杂嵌套:EXISTS 与 ANY/ALL 的不可替代性验证
实验 2.16(2) “查询类别 ID 为 100 的商品类别,如果存在,列出此类别中商品价格小于 5 的商品 ID”,是EXISTS的经典场景。它比IN更高效,且能承载复杂条件:
-- ✅ EXISTS:先确认类别存在,再查其下低价商品 SELECT g.id FROM goods g WHERE EXISTS ( SELECT 1 FROM categories c WHERE c.id = 100 AND g.category_id = c.id ) AND g.price < 5; -- ⚠️ IN:若 categories 表无 id=100 的记录,整个查询返回空,但无法区分“类别不存在”和“类别存在但无低价商品” SELECT g.id FROM goods g WHERE g.category_id IN (SELECT id FROM categories WHERE id = 100) AND g.price < 5;而实验 2.17 与 2.18 的对比,则揭示了ANY和ALL的本质:
price > ANY (SELECT price FROM order_details)→price > (SELECT MIN(price) FROM order_details)price > ALL (SELECT price FROM order_details)→price > (SELECT MAX(price) FROM order_details)
验证技巧:执行子查询单独看结果,再代入主查询逻辑。例如,先SELECT MIN(price), MAX(price) FROM order_details,得到10和500,则> ANY即> 10,> ALL即> 500。
5. 连接与嵌套的混合战场:从“用户订单+评论”到“跨类别商品对比”的终极推演
5.1 外连接 + 聚合:识别“沉默用户”的标准范式
实验 2.12 的LEFT JOIN是分析用户行为的基石。结合聚合,可精准定位“注册但从未下单”或“下单但从未评论”的用户:
-- 查询所有用户及其订单数、评论数(包括0) SELECT u.id AS user_id, u.username, COUNT(DISTINCT o.id) AS order_count, COUNT(DISTINCT c.id) AS comment_count FROM users u LEFT JOIN orders o ON u.id = o.user_id LEFT JOIN comments c ON u.id = c.user_id GROUP BY u.id, u.username HAVING order_count = 0 OR comment_count = 0;这里COUNT(DISTINCT o.id)防止一个用户多订单导致计数虚高;HAVING在分组后过滤,是WHERE无法替代的。
5.2 复合连接 + 子查询:实现“用户购物车商品价格排序”的原子操作
实验 2.13(2) “查询每个用户的购物车中的商品价格,并且按照价格顺序排列”,需三表连接与排序:
-- cart(用户ID,商品ID) + goods(商品ID,价格) + users(用户ID,姓名) SELECT u.id AS user_id, u.username, g.id AS goods_id, g.name AS goods_name, g.price FROM cart c INNER JOIN goods g ON c.goods_id = g.id INNER JOIN users u ON c.user_id = u.id ORDER BY g.price ASC;关键点:ORDER BY必须放在最后,且只能对SELECT列中的字段排序(如g.price)。若需按“用户平均购物车价格”排序,则需先GROUP BY user_id计算均值,再ORDER BY。
5.3 集合查询 UNION vs UNION ALL:去重成本与业务意图的权衡
实验 2.19 的UNION与UNION ALL对比,直指性能与语义的平衡:
| 场景 | 使用 UNION | 使用 UNION ALL |
|---|---|---|
| 业务意图 | 需要唯一结果集(如“所有低价或热门商品”) | 允许重复,或已知无重复(如日志合并) |
| 性能 | 高开销:需排序去重 | 零开销:直接追加结果 |
| 实验 2.19(1) | SELECT * FROM goods WHERE price<5 UNION SELECT * FROM goods WHERE category_id IN (1,2) | SELECT * FROM goods WHERE price<5 UNION ALL SELECT * FROM goods WHERE category_id IN (1,2) |
验证方法:用EXPLAIN查看执行计划。UNION会出现Using temporary; Using filesort,而UNION ALL没有。
5.4 嵌套中的连接:用 JOIN 替代子查询提升可读性与性能
实验 2.14(1) “查询订购商品 ID 为 1 的订单 ID,并根据订单 ID 查询发出此订单的用户 ID”,可用两种写法:
-- ✅ 写法1:子查询(符合实验要求,但嵌套深) SELECT user_id FROM orders WHERE id IN ( SELECT order_id FROM order_details WHERE goods_id = 1 ); -- ✅ 写法2:JOIN(更直观,通常性能更好) SELECT DISTINCT o.user_id FROM orders o INNER JOIN order_details od ON o.id = od.order_id WHERE od.goods_id = 1;何时选哪种?
- 子查询:逻辑清晰,适合“先筛选再关联”的场景(如
EXISTS); - JOIN:数据量大时,优化器更易生成高效执行计划,且
DISTINCT可控。
6. 从训练到生产:我的 3 个必做验证步骤与防翻车清单
6.1 第一步:用 EXPLAIN 验证每条查询的执行计划
写完任何一条实验 SQL,我绝不直接运行SELECT,而是先加EXPLAIN:
EXPLAIN SELECT u.username, SUM(o.total_price) FROM users u INNER JOIN orders o ON u.id = o.user_id GROUP BY u.id;重点看三列:
type:ALL(全表扫描)是红灯,应优化为ref(索引查找)或range(范围扫描);key:显示实际使用的索引,若为NULL,说明索引未生效;rows:预估扫描行数,越小越好。若rows接近表总行数,说明索引失效或缺失。
我的习惯:对JOIN、GROUP BY、ORDER BY涉及的字段,全部建立联合索引。例如orders(user_id, total_price, created_at)覆盖实验 2.5、2.7、2.4 的所有查询。
6.2 第二步:用小数据集验证逻辑,再放大到全量
实验中的“商品 ID 为 1”“用户 ID 为 100”不是随意写的。我会先用LIMIT 10强制缩小结果集:
-- 先验证逻辑是否正确 SELECT * FROM comments WHERE goods_id = 1 LIMIT 10; -- 再验证去重是否生效 SELECT DISTINCT user_id FROM comments WHERE goods_id = 1 LIMIT 10;血泪教训:某次在模拟项目X中,学生直接对百万级订单表执行SELECT DISTINCT user_id FROM orders,服务器卡死。从此我规定:所有DISTINCT、GROUP BY、ORDER BY查询,必须先LIMIT 100验证逻辑,再删掉LIMIT跑全量。
6.3 第三步:建立“查询防翻车清单”,每次执行前默念三遍
这是我带实训课十年总结的 checklist,贴在工位上:
| 检查项 | 问题示例 | 正确做法 |
|---|---|---|
| 表别名 | SELECT id FROM orders JOIN users... | SELECT o.id, u.name FROM orders o JOIN users u... |
| NULL 安全 | WHERE status != 'done'(漏掉status IS NULL) | WHERE status != 'done' OR status IS NULL |
| 聚合匹配 | SELECT user_id, username FROM orders GROUP BY user_id | SELECT user_id, MAX(username) FROM orders GROUP BY user_id或JOIN users |
| 索引覆盖 | ORDER BY created_at但created_at无索引 | ALTER TABLE orders ADD INDEX idx_created_at (created_at); |
| 字符集 | WHERE name = '挡风玻璃'返回空(表用latin1) | ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
从那以后我每次写完一个实验 SQL,都强制走一遍这个 checklist,哪怕只是SELECT * FROM goods WHERE name = '挡风玻璃'—— 因为最简单的查询,往往藏着最深的坑。希望帮到你。
本文还有配套的精品资源,点击获取