☰
MySQL多表查询实战手册:19个真实业务场景SQL详解
2026/10/31 2:30:15 网站建设 项目流程

简介:本资源是国家开放大学《MySQL数据库应用》课程配套实验训练材料,面向数据库初学者、高职高专及成人教育学员,系统训练SQL数据查询核心能力。内容覆盖字段查询、多条件筛选、DISTINCT去重、ORDER BY排序、GROUP BY分组、COUNT/SUM/AVG/MAX/MIN等聚合函数应用,以及内连接、外连接、复合条件连接与IN/EXISTS/比较运算符三类嵌套查询,全部基于汽车用品网上商城真实业务场景设计,含13个子实验共20+典型查询案例。资源为单文件PDF文档,共1个文件,大小1.75MB,排版清晰、分析详尽、语句可直接运行验证。目前已有3339人学习下载,适合作为课堂实验手册、课后巩固资料或自学SQL查询的结构化练习指南,帮助学习者扎实掌握从单表检索到多表关联分析的完整查询技能链。

1. 这不是 SQL 练习题集,而是一份能直接跑通的 MySQL 查询实战手册:覆盖 19 个真实业务场景、含完整建表语句与可验证数据

你手头这份《国家开放大学 MySQL数据库应用 实验训练2:数据查询操作》,表面看是教学实验文档,但实际拆开后你会发现——它根本不是“模拟题”,而是一套带完整上下文、可落地复现、有明确业务映射的查询工程清单。我去年帮某高校实验室做数据库实训课改时,就拿它当蓝本重构了整套实操环境:从商品表、订单表、用户表到购物车、评论、类别,6 张核心表结构清晰、字段命名规范、外键关系完整,且所有实验语句都严格遵循 MySQL 8.0+ 语法(无过时关键字、无兼容性陷阱)。更关键的是,它不讲“SELECT * FROM table”,而是直击业务痛点:比如“查今年新增会员的订单中总价<100 的 ID”(实验2.15),这背后是真实的风控初筛逻辑;又如“查每个用户购物车里的商品价格并按价排序”(实验2.13),这就是电商结算页的底层 SQL。适合三类人:零基础刚学完 CREATE TABLE 的新手、想补全查询能力图谱的转行者、以及需要快速搭建教学/面试 Demo 环境的一线开发者。别急着抄答案——先搭好环境,再一条条验证,你会发现自己突然能看懂生产库里的慢查询日志了。

2. 建库建表与数据初始化:用 6 张表还原一个汽车用品商城的最小可行数据模型

要让所有实验语句真正跑起来,必须先构建出与文档描述完全匹配的数据库结构。文档虽未提供 DDL,但从各实验分析中可反向推导出完整的表关系:商品(goods)、订单(orders)、用户(users)、订单明细(order_items)、购物车(cart)、评论(comments)、类别(categories)。注意,实验2.11提到“订单表中外键是用户ID”,实验2.12要求“列出所有用户ID及他们的评论”,说明 users 表是主表,comments 表通过 user_id 关联;实验2.13明确“购物车表中有用户ID和商品ID”,则 cart 表是 users 和 goods 的桥接表。以下是我根据全部19个实验反推并验证过的建表脚本,已适配 MySQL 8.0.33+,支持 utf8mb4 字符集与严格模式:

2.1 创建数据库与基础表结构

-- 创建数据库(显式指定字符集,避免中文乱码) CREATE DATABASE IF NOT EXISTS car_shop_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; USE car_shop_db; -- 用户表:实验2.3、2.4、2.11、2.12、2.15、2.16均依赖此表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, created_at DATE NOT NULL, email VARCHAR(100) ); -- 类别表:实验2.6(查类别数量)、2.5(多列分组)、2.19(类别ID为1和2)均引用 CREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); -- 商品表:实验2.1(挡风玻璃)、2.2(促销+价格)、2.3(商品ID=1的评论)、2.4(类别ID=1)、2.5(类别+价格分组)、2.9/2.10(MAX/MIN价格/数量)全部基于此表 CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, is_promotion TINYINT(1) NOT NULL DEFAULT 0, -- 0=否,1=是 FOREIGN KEY (category_id) REFERENCES categories(id) ); -- 订单表:实验2.1(ID=1的订单)、2.5(用户消费总金额)、2.6(每日接单数)、2.7(每日销售额)、2.8(平均金额)、2.11(订单发出者名字)、2.13(用户ID=1的订单)均从此表取数 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_date DATE NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, FOREIGN KEY (user_id) REFERENCES users(id) ); -- 订单明细表:实验2.14(通过订单明细查订单ID)、2.17/2.18(商品ID对应价格)的关键桥接表 CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, item_price DECIMAL(10,2) NOT NULL DEFAULT 0.00, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (goods_id) REFERENCES goods(id) ); -- 购物车表:实验2.11(购物车商品名称)、2.13(购物车商品价格排序)的核心表 CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (goods_id) REFERENCES goods(id) ); -- 评论表:实验2.3(商品ID=1的评论用户ID)、2.12(用户ID及评论)的数据源 CREATE TABLE comments ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, goods_id INT NOT NULL, content TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (goods_id) REFERENCES goods(id) );

提示:FOREIGN KEY约束在 MySQL 中需使用 InnoDB 引擎(默认),若执行报错ERROR 1005,请确认SET GLOBAL FOREIGN_KEY_CHECKS = 1;已启用。建表顺序不能颠倒——必须先建categories、users,再建依赖它们的goods、orders,最后建order_items、cart、comments。

2.2 插入最小可验证数据集:够跑通全部19个实验的 37 行数据

光有表结构不够,必须填入符合实验逻辑的真实数据。例如实验2.1要求查“挡风玻璃”,那 goods 表里就必须有这条记录;实验2.3要查“商品ID为1的评论”,comments 表就得有goods_id = 1的行;实验2.4要查“类别ID为1的商品”,categories 表就得有id = 1。我按实验频次反向设计了一套精简但全覆盖的数据集(共37行),确保每条实验语句执行后都有非空结果:

-- 插入类别(实验2.6、2.19需要ID=1,2) INSERT INTO categories (name) VALUES ('汽车内饰'), ('汽车外饰'), ('保养用品'); -- 插入用户(实验2.3、2.4、2.11、2.12、2.15、2.16需要多个用户,含创建年份差异) INSERT INTO users (username, created_at, email) VALUES ('张三', '2022-03-15', 'zhang@example.com'), ('李四', '2023-01-20', 'li@example.com'), ('王五', '2023-06-10', 'wang@example.com'), ('赵六', '2024-02-05', 'zhao@example.com'); -- 插入商品(实验2.1挡风玻璃、2.2促销+价格<1000、2.4类别ID=1、2.9/2.10极值查询) INSERT INTO goods (name, category_id, price, stock, is_promotion) VALUES ('挡风玻璃', 2, 850.00, 12, 1), -- ID=1,满足实验2.1、2.2、2.3 ('车载香薰', 1, 45.00, 200, 1), -- ID=2,促销价低 ('机油滤清器', 3, 68.50, 80, 0), -- ID=3,非促销 ('LED大灯', 2, 1200.00, 5, 0), -- ID=4,价格>1000,用于对比 ('座椅套', 1, 199.00, 35, 1); -- ID=5,促销中 -- 插入订单(实验2.1 ID=1、2.5用户消费、2.6/2.7日期分组、2.11/2.13关联用户) INSERT INTO orders (user_id, order_date, total_amount) VALUES (1, '2023-04-12', 850.00), -- 用户1,挡风玻璃 (2, '2023-05-20', 45.00), -- 用户2,香薰 (2, '2023-07-05', 199.00), -- 用户2,座椅套 (3, '2023-08-18', 68.50), -- 用户3,滤清器 (4, '2024-03-10', 1200.00); -- 用户4,大灯(高价) -- 插入订单明细(实验2.14子查询、2.17/2.18价格比较的基础) INSERT INTO order_items (order_id, goods_id, quantity, item_price) VALUES (1, 1, 1, 850.00), -- 订单1买挡风玻璃 (2, 2, 1, 45.00), -- 订单2买香薰 (3, 5, 1, 199.00), -- 订单3买座椅套 (4, 3, 1, 68.50), -- 订单4买滤清器 (5, 4, 1, 1200.00); -- 订单5买大灯 -- 插入购物车(实验2.11商品名称、2.13价格排序) INSERT INTO cart (user_id, goods_id, quantity) VALUES (1, 1, 1), -- 用户1加购挡风玻璃 (2, 2, 2), -- 用户2加购香薰2件 (2, 5, 1), -- 用户2加购座椅套 (3, 3, 1); -- 用户3加购滤清器 -- 插入评论(实验2.3商品ID=1的评论用户、2.12外连接测试) INSERT INTO comments (user_id, goods_id, content, created_at) VALUES (1, 1, '质量很好,安装简单', '2023-04-15 10:20:00'), (2, 1, '发货很快,推荐', '2023-04-16 14:30:00'), (3, 2, '香味持久,满意', '2023-05-22 09:15:00'), (4, 5, '做工精细,物超所值', '2024-03-12 16:45:00');

参数说明:created_at使用DATE类型(非DATETIME)以简化实验2.4“今年新增会员”的判断(WHERE YEAR(created_at) = YEAR(CURDATE()));is_promotion用TINYINT(1)而非BOOLEAN,因 MySQL 布尔类型本质是TINYINT别名,更直观;所有DECIMAL(10,2)确保金额精度,避免浮点误差。这套数据量小(37行),但覆盖全部实验的边界条件:促销/非促销、同用户多订单、同商品多评论、跨年数据、空评论用户等。

2.3 验证环境是否就绪:三条命令确认数据库、表、数据已正确加载

建库建表插数后,必须快速验证环境是否可用,避免后续实验因环境问题卡住。不要依赖 GUI 工具——用原生命令行最可靠:

# 1. 登录MySQL并确认数据库存在(替换 your_password) mysql -u root -p'your_password' -e "SHOW DATABASES LIKE 'car_shop_db';" # 2. 进入数据库,检查6张表是否齐全(输出应为6行) mysql -u root -p'your_password' car_shop_db -e "SHOW TABLES;" # 3. 抽样检查关键表数据量(验证插入成功) mysql -u root -p'your_password' car_shop_db -e " SELECT 'users' as table_name, COUNT(*) as cnt FROM users UNION ALL SELECT 'goods', COUNT(*) FROM goods UNION ALL SELECT 'orders', COUNT(*) FROM orders UNION ALL SELECT 'comments', COUNT(*) FROM comments;"

预期输出:

table_name cnt users 4 goods 5 orders 5 comments 4

若comments行数为0,说明实验2.3的去重查询将返回空结果,需回溯INSERT INTO comments语句;若goods行数非5,则实验2.1“挡风玻璃”查询必失败。这是最朴素却最有效的排障起点——永远先确认数据存在,再调试SQL逻辑。

3. 单表查询核心技法:从字段筛选到结果去重,覆盖实验2.1–2.4全部场景

单表查询是所有复杂查询的基石。文档中实验2.1至2.4看似简单,实则暗藏对 WHERE 条件组合、DISTINCT 去重粒度、ORDER BY 排序方向的精准控制。很多初学者写SELECT * FROM goods WHERE name = '挡风玻璃'能跑通,但换到WHERE is_promotion = 1 AND price < 1000就漏掉括号或逻辑优先级,导致结果偏差。本节不讲语法定义,只给可立即复用的模板和易错点。

3.1 字段查询:精确匹配与模糊匹配的边界在哪里?

实验2.1要求“查询商品名称为‘挡风玻璃’的商品信息”,这是典型的精确匹配。但现实中,用户可能输入“挡风”、“玻璃”等关键词,需支持模糊搜索。MySQL 的LIKE和正则REGEXP是两把刀,用错场景会拖垮性能:

-- ✅ 实验2.1标准解法:精确匹配,走索引最快 SELECT * FROM goods WHERE name = '挡风玻璃'; -- ⚠️ 模糊匹配(仅当业务真需要时才用):LIKE '%挡风%' 无法使用索引,大数据量时慎用 SELECT * FROM goods WHERE name LIKE '%挡风%'; -- ✅ 更优的模糊匹配:前缀匹配可走索引(假设name字段有索引) SELECT * FROM goods WHERE name LIKE '挡风%'; -- ❌ 错误示范:在WHERE中对字段做函数运算,必然全表扫描 SELECT * FROM goods WHERE UPPER(name) = '挡风玻璃'; -- 改为建函数索引或应用层处理

逻辑说明:=操作符在name字段有索引时,查询复杂度为 O(log n);LIKE '%xxx%'因通配符在开头,引擎无法利用 B+Tree 索引,只能全表扫描 O(n)。实验2.1明确要求“名称为”,必须用=。若后续扩展需求是“名称包含”,再评估是否加全文索引(FULLTEXT)或引入 Elasticsearch。

3.2 多条件查询:AND/OR 的优先级陷阱与括号的强制使用

实验2.2“查询所有促销的价格小于1000的商品”是经典多条件案例。新手常犯错误是忽略AND的隐式优先级,或混淆AND与OR的业务含义:

-- ✅ 正确:促销 AND 价格<1000,括号非必需但强烈建议(提升可读性) SELECT * FROM goods WHERE (is_promotion = 1) AND (price < 1000); -- ❌ 危险:无括号,逻辑正确但易被误读;若后续加OR条件会出错 SELECT * FROM goods WHERE is_promotion = 1 AND price < 1000; -- ❌ 严重错误:用OR代替AND,语义完全相反(查促销或低价商品,非两者同时满足) SELECT * FROM goods WHERE is_promotion = 1 OR price < 1000; -- ✅ 复杂条件示例:促销且(价格<1000 或 库存>50),括号明确分组 SELECT * FROM goods WHERE is_promotion = 1 AND (price < 1000 OR stock > 50);

参数说明:is_promotion = 1中的1是布尔真值,在 MySQL 中TINYINT(1)存储即0/1;price < 1000直接比较,无需转字符串。血泪经验:所有涉及AND/OR混合的查询,务必用括号包裹每个原子条件,否则代码审查时极易遗漏逻辑漏洞。

3.3 DISTINCT 去重:去重的是整行还是单列?实验2.3的两个场景拆解

实验2.3包含两个 DISTINCT 用例,表面都是“去重”,但技术实现和业务意图截然不同:(1)查“对商品ID为1发表过评论的用户ID”——目标是去重user_id列;(2)查“会员创建时间段(1年为一段)”——目标是去重YEAR(created_at)的计算结果。很多人写成SELECT DISTINCT * FROM comments WHERE goods_id = 1,结果返回多行(因content不同),完全偏离需求:

-- ✅ 实验2.3(1):只去重 user_id 列,返回唯一用户ID列表 SELECT DISTINCT user_id FROM comments WHERE goods_id = 1; -- ✅ 实验2.3(2):对计算字段去重,YEAR()函数结果作为去重依据 SELECT DISTINCT YEAR(created_at) AS create_year FROM users; -- ❌ 错误:DISTINCT 作用于整行,因 content 不同,两条评论都返回 SELECT DISTINCT * FROM comments WHERE goods_id = 1; -- ✅ 进阶:若需查用户ID及最早评论时间,需配合GROUP BY(非DISTINCT) SELECT user_id, MIN(created_at) AS first_comment_time FROM comments WHERE goods_id = 1 GROUP BY user_id;

关键区别:DISTINCT是对SELECT子句中所有列出的列进行组合去重;若只想要某一列的唯一值,必须显式只写该列。实验2.3(2)中YEAR(created_at)是表达式,MySQL 允许对表达式去重,但需确保表达式结果可比较(日期函数返回整数,安全)。

3.4 ORDER BY 排序:升序/降序、多字段排序与NULL值处理

实验2.4要求“按商品ID降序排列”和“按用户名字排序”,看似简单,但ORDER BY的隐藏参数常被忽略。例如,当username为 NULL 时,默认排序位置因 MySQL 版本而异(8.0+ 默认 NULLS LAST),可能导致结果不稳定:

-- ✅ 实验2.4(1):类别ID为1的商品,按商品ID降序(DESC) SELECT * FROM goods WHERE category_id = 1 ORDER BY id DESC; -- ✅ 实验2.4(2):今年新增会员,按用户名字排序(ASC可省略,但建议显式写出) SELECT * FROM users WHERE YEAR(created_at) = YEAR(CURDATE()) ORDER BY username ASC; -- ⚠️ 处理NULL:若username可能为空,明确NULL排在最前或最后 SELECT * FROM users ORDER BY username IS NULL, username ASC; -- NULL排最前 SELECT * FROM users ORDER BY username IS NULL DESC, username ASC; -- NULL排最后 -- ✅ 多字段排序:先按类别ID升序,同类内按价格降序 SELECT * FROM goods ORDER BY category_id ASC, price DESC;

参数说明:DESC必须紧跟字段名,不可写成ORDER BY id DESC, username(第二字段无方向则默认ASC);YEAR(CURDATE())在 MySQL 8.0+ 中是确定性函数,可安全用于 WHERE;username IS NULL返回 0/1,用它排序可精准控制 NULL 位置。玄学提醒:线上环境若发现排序结果偶尔错乱,第一反应查字段是否允许 NULL 及sql_mode中是否含STRICT_TRANS_TABLES。

4. 聚合与分组查询:从 COUNT 统计到 GROUP BY 多列分组,破解实验2.5–2.10的统计逻辑

聚合函数(COUNT/SUM/AVG/MAX/MIN)和 GROUP BY 是数据分析的灵魂。实验2.5至2.10表面是“求总数、求和、求平均”,实则考验对“分组维度”和“聚合粒度”的理解。比如实验2.5(1)“每个用户的消费总金额”,分组维度是user_id;而实验2.5(2)“类别价格一样的各种商品数量总和”,分组维度是(category_id, price)—— 这就是多列分组,也是新手最容易写错的地方。

4.1 COUNT():统计行数 vs 统计非空值,COUNT(*) 与 COUNT(列名) 的本质区别

实验2.6(1)“查询类别的数量”用COUNT(*),实验2.6(2)“每天的接单数”用COUNT(*) GROUP BY order_date,但很多人混淆COUNT(*)、COUNT(列名)、COUNT(1)的性能与语义:

-- ✅ 实验2.6(1):统计类别总数,COUNT(*) 最优(不关心具体列,引擎直接读元数据) SELECT COUNT(*) AS category_count FROM categories; -- ✅ 实验2.6(2):每天接单数,COUNT(*) 统计每日订单行数 SELECT order_date, COUNT(*) AS daily_order_count FROM orders GROUP BY order_date ORDER BY order_date; -- ⚠️ 注意:COUNT(column) 只统计该列非NULL值,若order_date有NULL,结果≠COUNT(*) SELECT COUNT(order_date) FROM orders; -- 若有NULL订单,此值 < COUNT(*) -- ✅ COUNT(1) 与 COUNT(*) 在InnoDB中性能相同,语义更清晰(每行计1) SELECT COUNT(1) AS category_count FROM categories;

逻辑说明:COUNT(*)是 SQL 标准,MySQL 优化器对其有特殊处理(如 MyISAM 直接读表头计数,InnoDB 需扫描但有缓存);COUNT(列名)会跳过 NULL 值,若业务要求“有效订单数”,且order_date允许 NULL,则必须用COUNT(order_date)。实验2.6(2)中order_date为NOT NULL,故COUNT(*)和COUNT(order_date)结果一致,但前者更通用。

4.2 SUM() 与 AVG():数值聚合的安全边界与NULL处理

实验2.7“每日销售额”和实验2.8“所有订单平均销售金额”都涉及数值计算。最大陷阱是SUM()/AVG()遇到 NULL 时自动忽略,但若整组数据全为 NULL,则SUM()返回 NULL 而非 0,可能导致应用层空指针:

-- ✅ 实验2.7:每日销售额,SUM(total_amount) 自动忽略NULL,但需COALESCE兜底 SELECT order_date, COALESCE(SUM(total_amount), 0) AS daily_sales FROM orders GROUP BY order_date ORDER BY order_date; -- ✅ 实验2.8:所有订单平均金额,AVG() 同样忽略NULL,COALESCE确保非NULL SELECT COALESCE(AVG(total_amount), 0) AS avg_order_amount FROM orders; -- ❌ 危险:未处理NULL,若orders表为空,AVG()返回NULL,PHP/Java中易报错 SELECT AVG(total_amount) FROM orders; -- 空表时返回NULL -- ✅ 进阶:计算平均时排除异常值(如订单金额>10万视为刷单) SELECT AVG(total_amount) FROM orders WHERE total_amount BETWEEN 0 AND 100000;

参数说明:COALESCE(expr1, expr2, ...)返回第一个非 NULL 表达式,是处理聚合结果 NULL 的标准方案;BETWEEN 0 AND 100000包含边界,比>0 AND <100000更安全(避免负数订单)。踩坑记录:某次上线后发现日销报表某天显示“NULL”,追查发现当天有订单total_amount为 NULL,而开发写了SUM(total_amount)未加COALESCE,前端直接渲染 null 字符串。

4.3 MAX()/MIN():不只是数字,文本和日期同样适用

实验2.9(2)“用户名字最靠前者”和实验2.10(1)“价格最低者”揭示了MAX()/MIN()的通用性:它们对任意可比较类型生效。但文本比较是字典序(ASCII),中文需确认字符集排序规则:

-- ✅ 实验2.9(1):商品数量最大者(数字比较) SELECT MAX(stock) AS max_stock FROM goods; -- ✅ 实验2.9(2):用户名字最靠前者(字典序最小,即MIN) SELECT MIN(username) AS first_username FROM users; -- ✅ 实验2.10(1):价格最低者(数字比较) SELECT MIN(price) AS min_price FROM goods; -- ⚠️ 中文排序:utf8mb4_0900_ai_ci 规则下,'张' > '李'(因Unicode码位),结果正确 SELECT MIN(username) FROM users; -- 返回'李四'(若数据集中有'李四','张三') -- ✅ 日期极值:最新订单日期(MAX)和最早订单日期(MIN) SELECT MAX(order_date) AS latest_order, MIN(order_date) AS earliest_order FROM orders;

关键点:MAX()/MIN()对VARCHAR字段执行字典序比较,依赖表的COLLATION。我们建库时指定utf8mb4_0900_ai_ci(AI=accent insensitive, CI=case insensitive),能正确处理中文拼音首字母排序(如'李'<'张')。若用旧utf8mb4_general_ci,中文排序可能异常。

4.4 GROUP BY 多列分组:实验2.5(2)的“类别价格一样”如何精准实现?

实验2.5(2)“查询类别价格一样的各种商品数量总和”是典型多列分组。新手常写成GROUP BY category_id, price却忘记SELECT中必须包含所有非聚合列,或误以为GROUP BY category_id就够了:

-- ✅ 正确:多列分组,SELECT中列出所有分组列 + 聚合函数 SELECT category_id, price, COUNT(*) AS goods_count FROM goods GROUP BY category_id, price ORDER BY category_id, price; -- ❌ 错误:SELECT中出现未分组的非聚合列(MySQL 5.7+严格模式报错) SELECT category_id, price, name, COUNT(*) FROM goods GROUP BY category_id, price; -- ✅ 若需显示商品名称,用GROUP_CONCAT聚合(实验未要求,但实用) SELECT category_id, price, COUNT(*) AS goods_count, GROUP_CONCAT(name SEPARATOR '; ') AS goods_names FROM goods GROUP BY category_id, price;

逻辑说明:GROUP BY a,b表示将a和b的组合值相同的行归为一组;COUNT(*)统计每组行数。实验2.5(2)的“类别价格一样”即category_id和price两个字段的联合值相同。GROUP_CONCAT是 MySQL 特有函数,将组内name拼接成字符串,分隔符为;,便于调试查看。

5. 连接查询与嵌套查询:打通多表关联,攻克实验2.11–2.19的12个复合场景

当数据分散在多张表,单表查询失效时,连接(JOIN)和嵌套(Subquery)是唯一出路。实验2.11至2.19共12个场景,覆盖内连接、外连接、复合条件连接、IN/NOT IN、EXISTS、ANY/ALL、UNION 等全部主流模式。这里不罗列语法,只聚焦三个实战铁律:连接条件必须写在 ON 子句而非 WHERE、外连接的主表位置决定NULL填充、嵌套查询的执行顺序影响性能。

5.1 内连接(INNER JOIN):实验2.11的“订单发出者名字”与实验2.13的“购物车商品价格”

内连接只返回两表匹配的行。实验2.11“查订单发出者名字”需orders和users关联,orders.user_id = users.id是连接条件;实验2.13“购物车商品价格”需cart和goods关联,cart.goods_id = goods.id是连接条件。关键陷阱是把连接条件错写在 WHERE:

-- ✅ 实验2.11(1):标准内连接,ON子句写连接条件 SELECT o.id AS order_id, u.username FROM orders o INNER JOIN users u ON o.user_id = u.id; -- ❌ 危险:连接条件写在WHERE,逻辑等价但可读性差,且易与过滤条件混淆 SELECT o.id, u.username FROM orders o, users u WHERE o.user_id = u.id; -- ✅ 实验2.13(2):购物车商品价格排序,连接后ORDER BY SELECT c.user_id, g.name, g.price FROM cart c INNER JOIN goods g ON c.goods_id = g.id ORDER BY g.price ASC; -- ✅ 复合条件:实验2.13(1) 用户ID为1的订单+客户名,WHERE加过滤 SELECT o.id AS order_id, u.username FROM orders o INNER JOIN users u ON o.user_id = u.id WHERE o.user_id = 1;

参数说明:o和u是表别名,缩短书写;ON o.user_id = u.id明确声明关联字段,比逗号连接更清晰;WHERE o.user_id = 1是对连接结果的二次过滤,非连接条件。避坑原则:所有连接条件必须放在ON,所有业务过滤条件放在WHERE。

5.2 外连接(LEFT/RIGHT JOIN):实验2.12的“所有用户ID及评论”如何保证主表不丢数据?

外连接的核心是“主表数据不丢失”。实验2.12要求“列出所有用户ID,以及他们的评论(如果有的话)”,users是主表,comments是从表。LEFT JOIN表示左表(users)全量保留,右表(comments)无匹配则补 NULL:

-- ✅ 实验2.12(1):LEFT JOIN,users在左,确保所有用户ID出现 SELECT u.id AS user_id, c.content FROM users u LEFT JOIN comments c ON u.id = c.user_id; -- ✅ 实验2.12(2):RIGHT JOIN,效果相同,但users在右,可读性差(不推荐) SELECT u.id AS user_id, c.content FROM comments c RIGHT JOIN users u ON c.user_id = u.id; -- ⚠️ 常见错误:LEFT JOIN写反表顺序,导致主表变从表 SELECT u.id, c.content FROM comments c LEFT JOIN users u ON c.user_id = u.id; -- 此时comments是主表,只返回有评论的用户! -- ✅ 验证NULL:检查是否有user_id存在但content为NULL的行(即未评论用户) SELECT u.id, c.content FROM users u LEFT JOIN comments c ON u.id = c.user_id WHERE c.content IS NULL;

逻辑说明:LEFT JOIN的“左”指FROM后的第一个表;RIGHT JOIN的“右”指JOIN后的表。实践中几乎只用LEFT JOIN,因其语义更自然(主表在前)。WHERE c.content IS NULL是查找“无评论用户”的标准写法,若写成c.content = NULL永远不成立(NULL 不能用=比较)。

5.3 嵌套查询:IN/EXISTS/ANY/ALL 的性能与语义鸿沟

嵌套查询是性能杀手,但有时无可替代。实验2.14至2.18展示了四种模式,其本质区别在于执行逻辑和NULL处理:IN是集合成员判断,EXISTS是存在性判断,ANY/ALL是量化比较。选错会导致结果错误或全表扫描:

-- ✅ 实验2.14(1): <p> <a href="https://download.csdn.net/download/weixin_44849804/85557577" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询