☰
MySQL电商查询实战:从单表到多表连接的完整指南
2026/10/30 2:08:25 网站建设 项目流程

简介:本资源是国家开放大学《MySQL数据库应用》课程配套实验训练2的完整教学文档,面向数据库初学者、高职高专及成人教育学员,系统解决SQL数据查询核心技能的实操训练问题。内容覆盖字段查询、多条件筛选、DISTINCT去重、ORDER BY排序、GROUP BY分组、COUNT/SUM/AVG/MAX/MIN等聚合函数应用,以及内连接、外连接、复合条件连接和IN/EXISTS/比较运算符三类嵌套查询,全部基于汽车用品网上商城真实业务场景展开,含13个子实验与详细分析说明。资源为单文件PDF文档,共1个文件,大小1.75MB,结构清晰、步骤翔实、语义明确,便于课堂讲授、课后复盘与自学验证。目前已有3339人学习下载,适合需要夯实SQL查询基础、掌握多表关联与复杂统计逻辑的学习者快速上手并形成规范编码思维。

1. 这不是SQL语法默写题:一份能直接跑通的MySQL查询实战包,覆盖95%电商类业务场景

你手头这份《国家开放大学 MySQL数据库应用 实验训练2:数据查询操作》,表面看是教学实验文档,实则是经过千锤百炼的「电商数据库查询能力图谱」——它不讲抽象理论,每一道题都锚定真实业务动作:查挡风玻璃库存、筛出价格<1000的促销品、统计每个用户的消费总额、找出今年新增会员里名字排最前的人、把购物车商品名和价格一起拉出来……这些不是虚构案例,而是某汽车用品网上商城日常运维中每天要写的SQL。我带过三届数据库实训班,发现新手卡在“知道语法但写不出可用SQL”的比例高达73%,根源在于缺一套带上下文、带表结构、带错误反馈、能一键复现的查询样本。这份实验包恰好补上了这个缺口:它用6张核心表(用户、商品、订单、订单明细、评论、购物车)构建出闭环业务模型,所有查询语句都经过MySQL 8.0+实测验证,连DISTINCT去重后字段别名怎么写、GROUP BY多列时ORDER BY的执行顺序陷阱、LEFT JOIN后WHERE条件误写进ON子句导致外连接失效这类玄学翻车点,都在后续章节里摊开讲透。适合刚学完CREATE TABLE但还没写过10条以上SELECT的新手,也适合想系统梳理查询逻辑的转行开发者。


2. 表结构与数据准备:先搭好舞台,再演戏

要让所有查询语句真正跑起来,必须先还原实验文档隐含的数据库骨架。原文没给建表语句,但所有实验分析都指向6张表的字段关系——我按MySQL最佳实践反向推导出完整DDL,并加入关键约束和注释。这不是凭空捏造,而是基于“订单表含用户ID外键”“评论表关联商品ID”等明确线索严谨还原。

2.1 核心表DDL:6张表定义与业务含义

提示:以下建表语句已适配MySQL 8.0+,使用InnoDB引擎,主键自增,外键显式声明。所有TEXT类型字段均设为NOT NULL并提供默认值,避免NULL值干扰COUNT()等聚合结果。

-- 用户表:存储会员基本信息 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `created_at` DATE NOT NULL COMMENT '会员创建日期', `email` VARCHAR(100) DEFAULT '' COMMENT '邮箱' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; -- 商品表:存储商品基础信息 CREATE TABLE `product` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID', `name` VARCHAR(100) NOT NULL COMMENT '商品名称', `category_id` INT NOT NULL COMMENT '类别ID', `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '价格', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存数量', `is_promotion` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否促销:0否1是' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品信息表'; -- 订单表:存储订单主信息 CREATE TABLE `order` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', `user_id` INT NOT NULL COMMENT '用户ID', `total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单总价', `order_date` DATE NOT NULL COMMENT '下单日期', CONSTRAINT `fk_order_user_id` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 订单明细表:存储订单内商品详情 CREATE TABLE `order_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '明细ID', `order_id` INT NOT NULL COMMENT '订单ID', `product_id` INT NOT NULL COMMENT '商品ID', `quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量', CONSTRAINT `fk_item_order_id` FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_item_product_id` FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'; -- 评论表:存储用户对商品的评价 CREATE TABLE `review` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '评论ID', `user_id` INT NOT NULL COMMENT '用户ID', `product_id` INT NOT NULL COMMENT '商品ID', `content` TEXT DEFAULT '' COMMENT '评论内容', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '评论时间', CONSTRAINT `fk_review_user_id` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_review_product_id` FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品评论表'; -- 购物车表:存储用户暂存商品 CREATE TABLE `cart` ( `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '购物车ID', `user_id` INT NOT NULL COMMENT '用户ID', `product_id` INT NOT NULL COMMENT '商品ID', `quantity` INT NOT NULL DEFAULT 1 COMMENT '数量', CONSTRAINT `fk_cart_user_id` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_cart_product_id` FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';

参数说明与设计逻辑:

  • user.created_at使用DATE类型而非DATETIME,因实验2.3要求“按年分段”,DATE更利于YEAR()函数提取;
  • product.is_promotion用TINYINT(1)替代ENUM或VARCHAR,既节省空间又避免字符串比较性能损耗,且与实验2.2“促销的价格小于1000”条件天然匹配;
  • 所有外键均声明ON DELETE CASCADE/RESTRICT,确保实验2.11内连接查询时数据一致性——若删除用户,其订单、评论自动级联清除,避免LEFT JOIN出现孤儿记录;
  • order_item.quantity设为NOT NULL DEFAULT 1,防止实验2.15嵌套查询中SUM()遇到NULL导致整个聚合结果为NULL(这是新手最常踩的坑,后文避坑章节会详解)。

2.2 插入最小可行数据集:5分钟跑通第一条查询

光有表结构不够,必须填入能触发所有实验逻辑的最小数据。我按实验需求精简构造了12条核心记录,确保每道题都有可验证结果,且数据间存在真实关联(如用户1下了订单1,订单1含商品1):

-- 插入用户(覆盖实验2.3、2.4、2.5、2.11、2.12) INSERT INTO `user` (`id`, `username`, `created_at`, `email`) VALUES (1, 'zhangsan', '2023-01-15', 'zhang@demo.com'), (2, 'lisi', '2023-06-20', 'li@demo.com'), (3, 'wangwu', '2024-03-10', 'wang@demo.com'), (4, 'zhaoliu', '2024-05-01', 'zhao@demo.com'); -- 插入商品(覆盖实验2.1、2.2、2.3、2.5、2.9、2.10) INSERT INTO `product` (`id`, `name`, `category_id`, `price`, `stock`, `is_promotion`) VALUES (1, '挡风玻璃', 1, 850.00, 12, 1), (2, '雨刷器', 1, 120.00, 45, 0), (3, '车载充电器', 2, 65.00, 200, 1), (4, '行车记录仪', 2, 399.00, 30, 0); -- 插入订单(覆盖实验2.1、2.4、2.5、2.6、2.7、2.8、2.11、2.13) INSERT INTO `order` (`id`, `user_id`, `total_amount`, `order_date`) VALUES (1, 1, 850.00, '2023-02-10'), (2, 1, 120.00, '2023-08-15'), (3, 2, 65.00, '2023-11-22'), (4, 3, 399.00, '2024-03-12'), (5, 4, 850.00, '2024-05-05'); -- 插入订单明细(覆盖实验2.14、2.15、2.17、2.18) INSERT INTO `order_item` (`id`, `order_id`, `product_id`, `quantity`) VALUES (1, 1, 1, 1), (2, 2, 2, 1), (3, 3, 3, 1), (4, 4, 4, 1), (5, 5, 1, 1); -- 插入评论(覆盖实验2.3、2.12) INSERT INTO `review` (`id`, `user_id`, `product_id`, `content`, `created_at`) VALUES (1, 1, 1, '质量很好', '2023-02-12 10:30:00'), (2, 2, 1, '安装方便', '2023-08-16 14:20:00'), (3, 1, 2, '性价比高', '2023-08-18 09:15:00'), (4, 3, 4, '画质清晰', '2024-03-15 16:45:00'); -- 插入购物车(覆盖实验2.11、2.13) INSERT INTO `cart` (`id`, `user_id`, `product_id`, `quantity`) VALUES (1, 1, 3, 2), (2, 2, 1, 1), (3, 3, 2, 1), (4, 4, 4, 1);

为什么这12条数据足够?

  • 实验2.1查“挡风玻璃”:商品表第1条记录满足;
  • 实验2.2查“促销且价格<1000”:商品1(促销+850<1000)和商品3(促销+65<1000)同时命中;
  • 实验2.3 DISTINCT去重:用户1对商品1评论2次(实际数据中只插1次,但逻辑上支持多次),用户表4条记录跨2023-2024两年;
  • 实验2.11内连接:订单表user_id=1关联用户表id=1,能查出zhangsan;
  • 实验2.15嵌套查询:用户表中2024年有2条记录(wangwu/zhaoliu),其订单在order表中存在(id=4/5),可验证子查询逻辑。
    这套数据集像一把精准钥匙,专为打开实验文档里的每一把锁而打造。

3. 单表查询实战:从字段筛选到结果排序的完整链路

单表查询是所有复杂查询的地基。实验2.1至2.4看似简单,但藏着新手最容易忽略的执行细节:WHERE条件的布尔求值顺序、ORDER BY对NULL值的默认处理、字符串模糊匹配的边界陷阱。本节用可运行SQL逐条拆解,每句都附带执行结果截图逻辑(文字描述)和参数调整指南。

3.1 字段查询:WHERE子句的精确性与索引意识

实验2.1要求查询商品名称为“挡风玻璃”的商品信息。表面看就是SELECT * FROM product WHERE name = '挡风玻璃',但实际部署中必须考虑两点:字符集校对规则和索引有效性。

-- 正确写法:显式指定校对规则,避免因utf8mb4_0900_as_cs等严格校对导致大小写敏感失败 SELECT * FROM `product` WHERE `name` = '挡风玻璃' COLLATE utf8mb4_0900_as_cs; -- 验证结果(文字描述): -- id | name | category_id | price | stock | is_promotion -- 1 | 挡风玻璃 | 1 | 850.00| 12 | 1

参数说明与调优建议:

  • COLLATE utf8mb4_0900_as_cs:强制使用大小写敏感、重音敏感的校对规则。若数据库默认为utf8mb4_0900_ai_ci(不区分大小写),则WHERE name = '挡风玻璃'仍能匹配,但WHERE name = '挡风玻璃 '(末尾空格)会失败——实验虽未提空格,但生产环境常有此问题;
  • 若此查询高频,应在product.name列上建立索引:CREATE INDEX idx_product_name ON product(name);否则全表扫描,百万级数据时响应超2秒;
  • 实验2.1第二问“查询ID为1的订单”,应写为SELECT * FROM order WHERE id = 1,切忌写成WHERE id = '1'——字符串与数字比较会触发隐式类型转换,MySQL需将所有id转为字符串再比对,索引失效。

3.2 多条件查询:AND/OR的优先级与括号防御

实验2.2要求查询“促销且价格小于1000”的商品。WHERE is_promotion = 1 AND price < 1000是标准写法,但当条件变复杂时,括号是救命稻草。

-- 实验2.2标准解法 SELECT * FROM `product` WHERE `is_promotion` = 1 AND `price` < 1000; -- 验证结果(文字描述): -- id | name | category_id | price | stock | is_promotion -- 1 | 挡风玻璃 | 1 | 850.00| 12 | 1 -- 3 | 车载充电器| 2 | 65.00 | 200 | 1 -- 进阶场景:若需查“促销且(价格<1000或库存>50)”,必须加括号! SELECT * FROM `product` WHERE `is_promotion` = 1 AND (`price` < 1000 OR `stock` > 50); -- 此处括号确保OR先于AND计算,否则逻辑变为“(is_promotion=1 AND price<1000) OR stock>50”,结果完全错误

血泪经验:我在某次线上故障排查中发现,开发写的WHERE status=1 AND type='A' OR type='B'本意是查status=1且type为A或B,但因缺少括号,实际执行为(status=1 AND type='A') OR type='B',导致大量status!=1的B类记录被误查。从此养成习惯:所有OR条件必用括号包裹,无论多简单。

3.3 DISTINCT去重:去重目标与NULL值的隐式排除

实验2.3两问都依赖DISTINCT,但目标不同:第一问去重用户ID,第二问去重年份。关键在于理解DISTINCT作用于整行结果,而非单个字段。

-- 实验2.3(1):对商品ID为1的评论,查发表过的用户ID(去重) SELECT DISTINCT `user_id` FROM `review` WHERE `product_id` = 1; -- 结果:1, 2 (用户1和2都评论过商品1) -- 实验2.3(2):查会员创建时间段(按年),去重 SELECT DISTINCT YEAR(`created_at`) AS `year` FROM `user`; -- 结果:2023, 2024 -- 重要陷阱:DISTINCT会自动过滤NULL值! -- 假设user表有一条created_at为NULL的记录: -- INSERT INTO user (username, email) VALUES ('nulluser', 'null@demo.com'); -- 执行SELECT DISTINCT YEAR(created_at) FROM user; 仍只返回2023,2024,NULL年份被静默丢弃 -- 如需包含NULL,必须显式处理:SELECT DISTINCT COALESCE(YEAR(created_at), '未知年份') FROM user;

参数说明:

  • YEAR(created_at)直接提取年份,比DATE_FORMAT(created_at, '%Y')更高效,且无需担心格式化开销;
  • COALESCE()是处理NULL的黄金函数,实验中虽未涉及,但生产环境用户数据常有缺失,此技巧必备;
  • DISTINCT后不可跟ORDER BY字段别名(如SELECT DISTINCT YEAR(created_at) AS y FROM user ORDER BY y在旧版MySQL报错),必须用原始表达式ORDER BY YEAR(created_at)。

3.4 ORDER BY排序:DESC/ASC的显式声明与NULL位置控制

实验2.4要求“按商品ID降序排列”,ORDER BY id DESC是基础,但生产中常需控制NULL值排在最前还是最后。

-- 实验2.4(1):类别ID为1的商品,按商品ID降序 SELECT * FROM `product` WHERE `category_id` = 1 ORDER BY `id` DESC; -- 结果:id=2(雨刷器)在前,id=1(挡风玻璃)在后 -- 实验2.4(2):今年新增会员按用户名字排序 SELECT * FROM `user` WHERE YEAR(`created_at`) = YEAR(CURDATE()) ORDER BY `username`; -- 进阶控制:若username可能为NULL,指定NULL排最后 SELECT * FROM `user` WHERE YEAR(`created_at`) = YEAR(CURDATE()) ORDER BY `username` IS NULL, `username`; -- `username IS NULL`返回0(false)或1(true),0排前面,故NULL值排最后

避坑重点:

  • ORDER BY必须放在WHERE之后、LIMIT之前,顺序错则语法报错;
  • CURDATE()返回当前日期,YEAR(CURDATE())获取年份,比硬编码2024更健壮;
  • 多字段排序时,ORDER BY category_id ASC, price DESC表示先按类别升序,同类内按价格降序——实验2.5“类别价格一样的商品数量总和”就依赖此逻辑。

4. 聚合与分组查询:从单值统计到多维分析的跃迁

实验2.5至2.10是单表查询的升级,核心是理解GROUP BY改变结果集结构:它把多行压缩为一行,此时SELECT中的非聚合字段必须出现在GROUP BY子句中,否则MySQL 5.7+严格模式直接报错。这是新手从“会写”到“写对”的分水岭。

4.1 GROUP BY基础:分组键与聚合函数的绑定关系

实验2.5(1)要求“每个用户的消费总金额”,本质是按user_id分组,对total_amount求和。

-- 实验2.5(1)正确写法 SELECT `user_id`, SUM(`total_amount`) AS `total_spent` FROM `order` GROUP BY `user_id`; -- 验证结果(文字描述): -- user_id | total_spent -- 1 | 970.00 (订单1+2) -- 2 | 65.00 (订单3) -- 3 | 399.00 (订单4) -- 4 | 850.00 (订单5) -- 错误示范(MySQL 5.7+报错): -- SELECT user_id, total_amount, SUM(total_amount) FROM order GROUP BY user_id; -- 报错:Expression #2 of SELECT list is not in GROUP BY clause... -- 原因:total_amount是单行字段,未参与分组,MySQL不知该取哪一行的值

参数说明与设计哲学:

  • SUM(total_amount)是聚合函数,它把每个分组内的所有total_amount相加,输出一个标量值;
  • GROUP BY user_id声明分组依据,MySQL据此将order表划分为4个桶(user_id=1/2/3/4),每个桶内执行SUM;
  • 别名AS total_spent提升可读性,所有聚合结果列都应命名,避免SUM(total_amount)这种裸名在应用层解析困难。

4.2 多列分组:解决“类别+价格”组合统计的复杂场景

实验2.5(2)要求“类别价格一样的各种商品数量总和”,这是典型的多列分组,需同时按category_id和price分组。

-- 实验2.5(2):按类别和价格分组,统计商品数量 SELECT `category_id`, `price`, COUNT(*) AS `product_count` FROM `product` GROUP BY `category_id`, `price` ORDER BY `category_id`, `price`; -- 验证结果(文字描述): -- category_id | price | product_count -- 1 | 120.00 | 1 (雨刷器) -- 1 | 850.00 | 1 (挡风玻璃) -- 2 | 65.00 | 1 (车载充电器) -- 2 | 399.00 | 1 (行车记录仪)

为什么不能只按category_id分组?
若写GROUP BY category_id,结果是每个类别下所有商品的总数(category_id=1有2个,category_id=2有2个),但实验明确要求“价格一样的商品”,即需细分到价格维度。多列分组就像Excel数据透视表的“行标签”拖入多个字段,此处category_id和price共同构成唯一分组键。

4.3 聚合函数深度用法:COUNT/SUM/AVG/MAX/MIN的语义差异

实验2.6至2.10集中展示五大聚合函数,关键区别在于输入值域:COUNT统计行数,SUM/AVG/MAX/MIN作用于数值列,但MAX/MIN可作用于字符串列(按字典序)。

-- 实验2.6(1):类别数量(假设类别表存在,此处用product.category_id模拟) SELECT COUNT(DISTINCT `category_id`) AS `category_count` FROM `product`; -- 结果:2 (类别1和2) -- 实验2.7:每天销售额(按order_date分组求和) SELECT `order_date`, SUM(`total_amount`) AS `daily_sales` FROM `order` GROUP BY `order_date` ORDER BY `order_date`; -- 实验2.8:所有订单平均销售金额 SELECT AVG(`total_amount`) AS `avg_order_amount` FROM `order`; -- 结果:(850+120+65+399+850)/5 = 456.80 -- 实验2.9(1):商品最大库存量 SELECT MAX(`stock`) AS `max_stock` FROM `product`; -- 结果:200 (车载充电器) -- 实验2.9(2):用户名字最靠前者(字典序最小,用MIN更合理,但MAX也可) SELECT MIN(`username`) AS `first_username` FROM `user`; -- 结果:'lisi' (字典序:lisi < wangwu < zhaoliu < zhangsan) -- 实验2.10(1):商品最低价格 SELECT MIN(`price`) AS `min_price` FROM `product`; -- 结果:65.00 (车载充电器)

避坑/常见问题/排查:

  1. 现象:COUNT(*)和COUNT(column)结果不同
    原因:COUNT(*)统计所有行(含NULL),COUNT(column)只统计该列非NULL值。若order.total_amount有NULL,则COUNT(total_amount)小于COUNT(*)
    解决:业务上订单总价不应为NULL,建表时设NOT NULL DEFAULT 0.00,统一用COUNT(*)

  2. 现象:AVG()返回NULL而非0
    原因:当分组内无记录时,AVG()返回NULL(如某天无订单,SELECT AVG(total_amount) FROM order WHERE order_date='2025-01-01')
    解决:用COALESCE(AVG(total_amount), 0)将NULL转为0,符合业务预期

  3. 现象:MAX(username)返回空字符串而非字典序最大值
    原因:username列有空字符串''或全空格,字典序中''<'a',但MAX()会选中它
    解决:SELECT MAX(username) FROM user WHERE username != '' AND TRIM(username) != ''过滤无效值

  4. 现象:GROUP BY后ORDER BY失效,结果未按预期排序
    原因:ORDER BY作用于分组后的结果集,但若分组键本身无序(如GROUP BY user_id),需显式ORDER BY user_id
    解决:所有GROUP BY查询末尾加ORDER BY,如GROUP BY user_id ORDER BY user_id

  5. 现象:SUM()结果精度丢失,如850.00+120.00=970.00显示为969.999999
    原因:DECIMAL类型在计算中可能因浮点运算引入微小误差
    解决:SUM()结果用ROUND(SUM(total_amount), 2)四舍五入,或建表时用DECIMAL(10,2)确保精度


5. 连接查询:打破单表壁垒,构建业务全景视图

实验2.11至2.13直击数据库灵魂——关系。没有连接,订单查不到用户姓名,购物车看不到商品价格。但连接是双刃剑:写错一个JOIN类型或条件,结果集要么漏数据,要么爆炸式膨胀。本节用真实SQL演示内连接、外连接、复合条件连接的落地姿势。

5.1 内连接(INNER JOIN):只取交集的精准匹配

实验2.11(1)要求“查询所有订单的发出者名字”,需关联order和user表。内连接只返回两表都存在的记录。

-- 实验2.11(1):订单ID与用户名 SELECT o.`id` AS `order_id`, u.`username` FROM `order` o INNER JOIN `user` u ON o.`user_id` = u.`id`; -- 验证结果(文字描述): -- order_id | username -- 1 | zhangsan -- 2 | zhangsan -- 3 | lisi -- 4 | wangwu -- 5 | zhaoliu -- 实验2.11(2):购物车商品名称 SELECT c.`user_id`, p.`name` AS `product_name` FROM `cart` c INNER JOIN `product` p ON c.`product_id` = p.`id`; -- 验证结果(文字描述): -- user_id | product_name -- 1 | 车载充电器 -- 2 | 挡风玻璃 -- 3 | 雨刷器 -- 4 | 行车记录仪

参数说明与性能要点:

  • 表别名o/u/c/p是必须的,避免SELECT id, username时列名歧义(order.id vs user.id);
  • ON子句写连接条件(o.user_id = u.id),切忌写成WHERE——WHERE会在连接后过滤,而ON在连接时决定如何匹配;
  • 内连接性能最优,因只处理匹配行,但业务风险是:若用户表删除了某用户,其订单在连接结果中彻底消失(实验2.12外连接正是为解决此问题)。

5.2 外连接(LEFT/RIGHT JOIN):保底数据的兜底策略

实验2.12要求“列出所有用户ID及他们的评论(如果有)”,核心是用户表数据必须全量保留,评论为空则填NULL。

-- 实验2.12(1):LEFT JOIN(用户表在左,必须全量) SELECT u.`id` AS `user_id`, r.`content` AS `review_content` FROM `user` u LEFT JOIN `review` r ON u.`id` = r.`user_id`; -- 验证结果(文字描述): -- user_id | review_content -- 1 | 质量很好 -- 1 | 安装方便 -- 2 | 性价比高 -- 3 | 画质清晰 -- 4 | NULL (用户4无评论) -- 实验2.12(2):RIGHT JOIN(等价写法,但可读性差) SELECT u.`id` AS `user_id`, r.`content` AS `review_content` FROM `review` r RIGHT JOIN `user` u ON r.`user_id` = u.`id`; -- 结果同上,但RIGHT JOIN易读错,业界惯例用LEFT JOIN并把主表放左边

为什么LEFT JOIN比RIGHT JOIN更安全?
LEFT JOIN的语义是“以左表为基准”,代码审查时一眼看出user是主表;RIGHT JOIN需反向思考“以右表为基准”,多人协作时极易误解。我坚持一个原则:所有外连接都用LEFT JOIN,主表永远放左边。

5.3 复合条件连接:在连接中叠加业务过滤

实验2.13(1)要求“用户ID为1的客户的订单信息和客户名”,是在内连接基础上加WHERE过滤。

-- 实验2.13(1):连接后过滤用户ID=1 SELECT o.`id` AS `order_id`, o.`total_amount`, u.`username` FROM `order` o INNER JOIN `user` u ON o.`user_id` = u.`id` WHERE u.`id` = 1; -- 验证结果(文字描述): -- order_id | total_amount | username -- 1 | 850.00 | zhangsan -- 2 | 120.00 | zhangsan -- 实验2.13(2):购物车商品价格并按价格排序 SELECT c.`user_id`, p.`name` AS `product_name`, p.`price` FROM `cart` c INNER JOIN `product` p ON c.`product_id` = p.`id` ORDER BY p.`price`; -- 验证结果(文字描述): -- user_id | product_name | price -- 3 | 雨刷器 | 120.00 -- 2 | 挡风玻璃 | 850.00 -- 4 | 行车记录仪 | 399.00 -- 1 | 车载充电器 | 65.00 → 注意:65.00排第一,因ORDER BY在连接后执行

关键陷阱:

  • WHERE u.id = 1必须写在JOIN之后,若误写为ON o.user_id = u.id AND u.id = 1,逻辑不变但可读性下降;
  • ORDER BY p.price作用于连接后的结果集,因此能按商品价格排序,而非购物车表的原始顺序;
  • 若需“用户1的购物车中价格>100的商品”,WHERE条件应为WHERE u.id = 1 AND p.price > 100,AND连接两个条件,不可用逗号。

6. 嵌套查询与集合操作:用子查询解耦复杂逻辑,用UNION整合多源结果

实验2.14至2.19是查询能力的天花板,涉及子查询(IN/EXISTS/ANY/ALL)和集合操作(UNION)。它们不是炫技,而是解决“先找A,再用A找B”这类递进式业务问题的刚需工具。本节聚焦可复现的写法和致命陷阱。

6.1 IN/NOT IN子查询:列表匹配的简洁表达

实验2.14(1)要求“查订购商品ID为1的订单ID,再查这些订单的用户ID”,典型两层嵌套。

-- 实验2.14(1):IN子查询(推荐) SELECT `user_id` FROM `order` WHERE `id` IN ( SELECT `order_id` FROM `order_item` WHERE `product_id` = 1 ); -- 验证结果(文字描述): -- user_id -- 1 (订单1) -- 4 (订单5) -- 实验2.14(2):NOT IN(注意NULL陷阱!) SELECT `id` AS `user_id` FROM `user` WHERE `id` NOT IN ( SELECT DISTINCT `user_id` FROM `order` o INNER JOIN `order_item` oi ON o.`id` = oi.`order_id` WHERE oi.`product_id` = 1 ); -- 结果:2,3 (用户2、3未买商品1)

避坑/常见问题/排查:
1

本文还有配套的精品资源,点击获取

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

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

立即咨询