ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

MySQL实战练习题:电商三表设计与四阶SQL能力跃迁

MySQL实战练习题:电商三表设计与四阶SQL能力跃迁 1. 为什么这套MySQL练习题能真正帮你“把语法刻进肌肉里”我带过不少刚学完SQL基础的开发者他们能背出SELECT、WHERE、JOIN的语法结构也能在纸上写出简单的查询语句但一到真实业务场景——比如要从订单表里找出“近30天下单次数≥5次、且平均客单价超过200元的老客户”就卡在中间不知道该先筛时间还是先分组不确定HAVING和WHERE到底谁管谁更别提怎么把用户行为路径浏览→加购→下单→复购用窗口函数串起来。这不是记不住是没在真实数据流里反复“拧螺丝”。这套练习题的设计逻辑就是从这个断层切入的。它不叫“MySQL入门100题”也不叫“面试高频50问”它叫“练习表题目答案”——三个要素缺一不可。练习表不是随便造的假数据而是模拟了电商、内容平台、SaaS系统三类高频业务域的真实字段设计逻辑用户表里有is_vip、last_login_time、reg_source订单表里有order_status含pending,shipped,refunded等状态机字段、payment_method、discount_amount商品表里有category_path如/electronics/smartphones/iphone、stock_quantity、on_sale_flag。这些字段不是为了炫技而是你明天写报表、做AB测试、搭用户分群时真正在数据库里要面对的。关键词里虽然空着但整套题的底层锚点非常清晰以“可执行的最小闭环”为单位组织训练。每道题都自带可运行的建表语句、INSERT数据脚本、标准答案SQL、以及关键注释——不是只告诉你“答案是SELECT * FROM users WHERE age 18”而是标注“此处WHERE必须在GROUP BY之前执行否则会报错1140”不是只给JOIN结果而是说明“LEFT JOIN users ON orders.user_id users.id 比 INNER JOIN多返回37条记录因为存在未注册用户直接下单的灰度场景”。这种颗粒度才能让练习从“看懂”走向“手熟”再走向“直觉反应”。它适合三类人刚学完增删改查想验证理解的新人准备技术面试需要快速刷题的求职者还有像我这样隔半年不碰复杂查询手生到连GROUP_CONCAT的分隔符都要查文档的“老手”。如果你现在打开MySQL客户端对着空白命令行发呆超过10秒这套题就是你的重启开关。2. 练习表设计背后的业务逻辑与数据陷阱很多人拿到练习题第一反应是跳过建表语句直接抄答案。这恰恰踩中了第一个大坑不理解表结构等于在迷雾中开枪打中什么全靠运气。这套题的三张核心练习表——users用户、orders订单、products商品——每个字段名、每个约束、每条测试数据都藏着业务规则的影子。我们拆开来看。2.1 users表不只是ID和姓名而是用户生命周期的切片CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100) UNIQUE, age TINYINT CHECK (age BETWEEN 0 AND 120), is_vip BOOLEAN DEFAULT FALSE, reg_source ENUM(web, ios, android, wechat) NOT NULL, last_login_time DATETIME NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );重点不在语法而在字段选择逻辑reg_source用ENUM而非VARCHAR不是为了省那几个字节而是强制约束数据来源渠道的合法性——业务系统里不可能出现reg_sourceqq这种脏数据枚举值就是产品埋点规范的数据库落地。last_login_time允许NULL是因为新注册用户可能从未登录而created_at用TIMESTAMP自动填充确保注册时间不可篡改。这两个时间字段的组合直接支撑“7日留存率”计算COUNT(DISTINCT CASE WHEN DATEDIFF(last_login_time, created_at) 6 THEN id END) / COUNT(*)。is_vip设为BOOLEAN但实际业务中VIP状态往往关联会员等级、到期时间、权益包这里简化成布尔值是为了聚焦练习目标——当你需要扩展时自然会意识到该加vip_level TINYINT和vip_expire_date DATE。提示练习题里插入的100条users数据中有12条reg_sourcewechat且is_vipTRUE但last_login_time全部为空。这是模拟微信小程序静默授权注册后用户未主动登录的典型场景。很多初学者写“活跃VIP用户”查询时会漏掉这个NULL判断直接用WHERE last_login_time IS NOT NULL AND is_vipTRUE结果漏掉这批用户。2.2 orders表状态机驱动的业务事实表CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, order_status ENUM(pending, paid, shipped, delivered, refunded, cancelled) NOT NULL DEFAULT pending, amount DECIMAL(10,2) NOT NULL, discount_amount DECIMAL(10,2) DEFAULT 0.00, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (product_id) REFERENCES products(id) );这张表是练习题的“心脏”所有复杂查询都绕不开它order_status的ENUM值顺序不是随意排的pending→paid→shipped→delivered是正向流转refunded和cancelled是终态。这意味着“已支付未发货订单”必须写成WHERE order_status IN (paid, shipped)而不是WHERE order_status ! pending——后者会错误包含已退款订单。amount和discount_amount分开存储是为了支持“满300减50”这类营销活动的独立核算。练习题第27题要求“统计各渠道首单优惠力度”就必须用SUM(discount_amount)/COUNT(*)而不是用AVG(amount)来倒推。updated_at的ON UPDATE机制让“最近24小时订单状态变更”查询变得简单WHERE updated_at DATE_SUB(NOW(), INTERVAL 1 DAY)。但要注意如果订单状态从shipped变deliveredupdated_at会更新而created_at不变——这是区分“新订单”和“订单更新”的关键。注意练习题数据中orders表有237条记录其中order_statusrefunded的有19条但discount_amount全部为0。这是因为退款发生在支付后优惠已核销退款金额只退实付部分。很多人在写“退款订单平均优惠”时会误用AVG(discount_amount)得到0而正确逻辑是关联payments表本题未提供需自行补充思路。2.3 products表分类体系与库存策略的映射CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_path VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL, stock_quantity INT NOT NULL DEFAULT 0, on_sale_flag BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );category_path字段是点睛之笔它存储的是/electronics/smartphones/iphone这样的完整路径而非简单的category_id。这直接支持“查看智能手机品类下所有iPhone型号的销量TOP10”这类需求WHERE category_path LIKE /electronics/smartphones/iphone%。但这也带来陷阱LIKE /electronics/%会匹配到/electronics/smartphones和/electronics/accessories而LIKE /electronics/smartphones%才精准。练习题第41题“统计各一级品类销售额”就要求你用SUBSTRING_INDEX(category_path, /, 3)提取/electronics部分再GROUP BY。stock_quantity设为INT而非TINYINT是因为库存可能上万如图书而on_sale_flag是布尔值但实际业务中促销可能有多个维度限时、限量、满减这里简化处理只为聚焦“是否在售”这一核心判断。这三张表的关联不是教科书式的“用户-订单-商品”三角而是带着业务伤疤的真实关系orders.user_id外键指向users.id但users表里没有total_spent字段——因为这个值应该实时计算避免冗余更新失败products表没有sales_count因为销量要按时间维度聚合不能静态存储。练习题的答案SQL里所有SUM()、COUNT()、AVG()都不是为了凑数而是对这种“不冗余设计”的必然响应。3. 题目分层从语法校验到业务推演的四阶跃迁这套题的100道题不是线性排列的而是按认知负荷严格分层。我把它拆成四个阶段每个阶段解决一类典型问题对应不同的思维模式切换。跳着做题可以但跳过某个阶段后面就会卡住。3.1 第一阶单表CRUD的“肌肉反射”训练题号1-25目标不是学会语法而是让基础操作变成条件反射。比如题3“查询年龄大于25岁的用户邮箱按注册时间倒序排列”SELECT email FROM users WHERE age 25 ORDER BY created_at DESC;看似简单但隐藏三个易错点ORDER BY created_at DESC不是last_login_time——题目明确要求“按注册时间”新手常凭直觉选活跃时间WHERE age 25不是25——题目说“大于”边界必须精确结果只取email字段不加*——练习题强调“只查需要的字段”这是性能意识的起点。这一阶的题答案SQL长度通常不超过3行但每道题都强迫你检查WHERE条件是否精准ORDER BY字段是否存在SELECT是否最小化做完这25道你会发现自己写简单查询时手指已经自动敲出SELECT ... FROM ... WHERE ... ORDER BY的节奏不用再想语法树。3.2 第二阶多表关联的“关系破译”实战题号26-50进入这里你开始面对真实世界的混乱。题28“查询所有已支付订单的用户名、商品名、订单金额按金额从高到低排序”SELECT u.username, p.name, o.amount FROM orders o JOIN users u ON o.user_id u.id JOIN products p ON o.product_id p.id WHERE o.order_status paid ORDER BY o.amount DESC;关键在JOIN顺序和条件位置必须用JOIN users和JOIN products不能用LEFT JOIN——因为题目要求“已支付订单”意味着订单必须关联有效用户和商品LEFT JOIN会引入NULL污染结果WHERE o.order_status paid必须放在JOIN之后如果写成JOIN users u ON o.user_id u.id AND o.order_status paid逻辑就错了这是在关联时过滤订单但o.order_status属于orders表关联条件只能用关联字段如o.user_id u.id状态过滤必须在WHEREORDER BY o.amount DESC明确指定表别名o.避免多表同名字段如users和products都有created_at的歧义。这一阶的题答案SQL开始出现表别名、显式JOIN、WHERE条件分层。做完后你看到业务需求里的“用户订单商品”组合第一反应不再是“我要连三张表”而是“哪张是主表关联字段是什么过滤条件该放ON还是WHERE”。3.3 第三阶聚合分析的“业务指标”构建题号51-75这里开始烧脑。题53“统计每个用户的订单总数、总金额、平均客单价并筛选出总金额超过1000元的用户”SELECT u.id, u.username, COUNT(o.id) AS order_count, SUM(o.amount) AS total_amount, ROUND(AVG(o.amount), 2) AS avg_order_amount FROM users u LEFT JOIN orders o ON u.id o.user_id GROUP BY u.id, u.username HAVING SUM(o.amount) 1000;核心是理解GROUP BY和HAVING的协作用LEFT JOIN而非INNER JOIN因为要包含“注册但从未下单的用户”他们的order_count0total_amountNULLGROUP BY u.id, u.username必须包含SELECT中所有非聚合字段这是SQL92标准强制要求HAVING SUM(o.amount) 1000不能用WHERE——因为SUM()是聚合函数WHERE在分组前执行无法访问聚合结果HAVING在分组后执行专为此设计ROUND(AVG(o.amount), 2)AVG对NULL自动忽略所以未下单用户的avg_order_amount是NULL符合业务逻辑。这一阶的题答案SQL里必然出现GROUP BY、聚合函数、HAVING。做完后你读到“复购率”“LTV”“GMV”这些指标词脑子里自动浮现对应的SQL骨架先JOIN关联再GROUP BY主体然后SUM/COUNT/AVG计算最后HAVING筛选。3.4 第四阶高级特性的“业务推演”挑战题号76-100这是拉开差距的地方。题89“查询每个用户最近一次下单的时间、商品名、订单状态并标记是否为VIP用户”SELECT u.id, u.username, u.is_vip, latest_orders.last_order_time, p.name AS last_product_name, o.order_status AS last_order_status FROM users u JOIN ( SELECT user_id, MAX(created_at) AS last_order_time FROM orders GROUP BY user_id ) AS latest_orders ON u.id latest_orders.user_id JOIN orders o ON u.id o.user_id AND o.created_at latest_orders.last_order_time JOIN products p ON o.product_id p.id;这里用了三层嵌套子查询latest_orders先算出每个用户的最大created_at这是“最近一次”的定义主查询用这个结果JOIN orders但必须加AND o.created_at latest_orders.last_order_time因为一个用户可能同秒下多单只取时间戳匹配的订单最后JOIN products获取商品名。更难的是题97“使用窗口函数为每个用户的订单按时间排序标记首次下单、末次下单、以及是否为当月首单”SELECT user_id, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_seq, FIRST_VALUE(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS first_order_time, LAST_VALUE(created_at) OVER (PARTITION BY user_id ORDER BY created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_order_time, CASE WHEN ROW_NUMBER() OVER (PARTITION BY user_id, YEAR(created_at), MONTH(created_at) ORDER BY created_at) 1 THEN yes ELSE no END AS is_monthly_first_order FROM orders;窗口函数的难点在于理解PARTITION BY分组和ORDER BY组内排序的双重作用以及ROWS BETWEEN对LAST_VALUE的影响。这些题不是考你会不会写而是考你能不能把业务语言“当月首单”精准翻译成SQL逻辑。做完这一阶你再看业务方的需求文档能直接脑内编译成SQL而不是先画流程图再翻译。4. 答案解析的“防坑指南”为什么标准答案长这样很多人对照答案时只看SQL是否跑通却忽略了答案里那些看似多余的细节。这些细节正是资深开发者踩坑后留下的路标。我们以题66为例“查询订单金额排名前10的用户ID、用户名、总金额并显示其排名”SELECT u.id, u.username, SUM(o.amount) AS total_amount, ROW_NUMBER() OVER (ORDER BY SUM(o.amount) DESC) AS rank_num FROM users u JOIN orders o ON u.id o.user_id GROUP BY u.id, u.username ORDER BY total_amount DESC LIMIT 10;标准答案里藏着五个关键决策点每个都对应一个常见误区4.1 聚合函数的位置为什么SUM()在SELECT里不在WHERE里新手常写WHERE SUM(o.amount) 100这会报错。因为WHERE在分组前执行此时o.amount还是单条记录没有“总金额”概念。SUM()必须出现在SELECT或HAVING中这是SQL执行顺序决定的铁律FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT。答案把SUM()放在SELECT既计算了总金额又为窗口函数ROW_NUMBER()提供了排序依据。4.2 排名函数的选择为什么用ROW_NUMBER()而不是RANK()或DENSE_RANK()ROW_NUMBER()1,2,3,4,5无并列严格递增RANK()1,2,2,4,5并列后跳过如两个第2名则下一个为第4名DENSE_RANK()1,2,2,3,4并列后不跳过 题干说“排名前10”未说明并列如何处理默认采用最严格的ROW_NUMBER()确保结果恰好10条。如果业务要求“销售额并列第5名的用户都算TOP10”才用RANK()。4.3 LIMIT的位置为什么放在最后而不是子查询里有人会写子查询SELECT * FROM (SELECT ... GROUP BY ... ORDER BY SUM() DESC) t LIMIT 10。这在逻辑上等价但效率更低子查询要先算出所有用户的总金额再排序再取10条而主查询的LIMIT 10在最终结果集上截断MySQL优化器可能提前终止。标准答案的写法更符合执行引擎的优化路径。4.4 字段别名的强制使用为什么AS rank_num不能省略在应用层如Python的pandas读取结果时列名是rank_num而不是ROW_NUMBER() OVER (...)这种长字符串。省略AS会导致列名不可读增加后续处理成本。练习题所有答案都强制使用有意义的别名这是生产环境的必备习惯。4.5 JOIN类型的选择为什么用INNER JOIN而不是LEFT JOIN题干是“订单金额排名”隐含前提是有订单的用户。如果用LEFT JOIN会把未下单用户SUM(o.amount)NULL也纳入ROW_NUMBER()会给NULL排一个序号污染TOP10结果。INNER JOIN是业务语义的显式声明我们只关心有交易行为的用户。提示题66的答案SQL在MySQL 8.0可直接运行但在5.7及以下版本不支持窗口函数。此时需用变量模拟rank : rank 1 AS rank_num。练习题答案页专门标注了各题的MySQL版本兼容性这是很多免费题库忽略的关键信息。5. 实战复盘我在某电商项目中如何用这套题救火去年帮某电商团队优化一个慢查询背景是运营每天要导出“近7天高价值用户清单”原SQL跑了12分钟-- 原始SQL已脱敏 SELECT DISTINCT u.id, u.username, u.email FROM users u WHERE u.id IN ( SELECT user_id FROM orders WHERE created_at 2023-01-01 ) AND u.id IN ( SELECT user_id FROM orders GROUP BY user_id HAVING SUM(amount) 5000 );这个SQL的问题正是练习题第33、57、62题的综合体。我打开练习题文档快速定位到对应解法5.1 问题定位三重嵌套重复扫描外层WHERE u.id IN (子查询1)和u.id IN (子查询2)导致users表被扫描两次子查询1SELECT user_id FROM orders WHERE created_at ...没有索引全表扫描orders子查询2GROUP BY user_id HAVING SUM(amount) 5000同样全表扫描且GROUP BY开销大。5.2 重构思路用练习题第68题的“单次聚合条件标记”解法练习题第68题是“查询近30天下单且总金额超2000的用户返回ID、用户名、总金额、订单数”。它的答案是SELECT u.id, u.username, COALESCE(SUM(o.amount), 0) AS total_amount, COALESCE(COUNT(o.id), 0) AS order_count FROM users u LEFT JOIN orders o ON u.id o.user_id AND o.created_at DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY u.id, u.username HAVING total_amount 2000;关键创新点LEFT JOIN ... AND o.created_at ...把时间过滤条件从WHERE移到JOIN的ON子句这样即使用户无近30天订单也会保留用户记录SUM()返回NULLCOALESCE转为0单次JOINGROUP BY避免多次子查询HAVING直接过滤聚合结果。我依此重构生产SQLSELECT u.id, u.username, u.email, COALESCE(SUM(o.amount), 0) AS total_amount, COALESCE(COUNT(o.id), 0) AS order_count FROM users u LEFT JOIN orders o ON u.id o.user_id AND o.created_at DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY u.id, u.username, u.email HAVING total_amount 5000;执行时间从12分钟降到3.2秒。更关键的是我顺手给orders.created_at和orders.user_id加了联合索引这是练习题第15题“为高频查询字段添加索引”的实践延伸。5.3 后续优化用练习题第92题的“物化视图”思路虽然快了但每天凌晨跑还是占资源。我参考练习题第92题“创建月度销售汇总表”建议团队建一张user_weekly_value表每天凌晨用上述SQL刷新业务查询直接读这张轻量表。这比临时计算快10倍且数据一致性更好。这套题的价值从来不是让你记住100个答案而是当你面对一个陌生的、带着业务气味的SQL需求时能迅速在脑中调出对应的练习题编号知道“哦这题我做过解法是XXX要注意YYY”。它把零散的知识点织成了一张可检索、可复用的网。6. 进阶建议如何把练习题变成你的SQL能力加速器做完100道题只是起点。真正的高手会用这套题做三件事建立自己的错题本、构造业务变体题、反向设计练习表。这才是让它真正长在你身上的方法。6.1 动手建一个“错题-原因-修正”三栏笔记不要只抄答案要记录你卡住的瞬间。比如题44“查询所有未下单用户的用户名和邮箱”你可能写了-- 你的错误答案 SELECT username, email FROM users WHERE id NOT IN (SELECT user_id FROM orders);运行发现结果为空但明明有用户没下单。原因SELECT user_id FROM orders返回了NULL值比如某条订单user_id被误设为NULL而NOT IN (..., NULL)永远为FALSE。这是SQL里经典的NULL陷阱。正确答案是SELECT username, email FROM users WHERE id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL); -- 或更优雅的LEFT JOIN SELECT u.username, u.email FROM users u LEFT JOIN orders o ON u.id o.user_id WHERE o.user_id IS NULL;在你的错题本里记下错题编号44卡点描述NOT IN遇到NULL时逻辑失效根本原因SQL中任何与NULL的比较, !, IN, NOT IN都返回UNKNOWNWHERE只接受TRUE修正方案过滤NULL或改用LEFT JOIN关联练习题题12NULL安全的COUNT、题31IS NULL/IS NOT NULL用法每周回顾错题本你会发现很多“新问题”只是旧陷阱换个马甲。6.2 给每道题加一个“业务变体”自己出题题22是“查询订单金额大于平均订单金额的订单ID、用户ID、金额”。这是基础版。你可以给自己加难度变体1时间维度“查询2023年Q4订单金额大于该季度平均订单金额的订单”——需要加WHERE时间过滤再用子查询算季度均值变体2关联维度“查询iPhone品类下订单金额大于该品类平均订单金额的订单”——需要JOIN products表用category_path筛选再算品类均值变体3动态阈值“查询订单金额大于其用户历史平均订单金额的订单”——要用窗口函数AVG(amount) OVER (PARTITION BY user_id)。自己出题的过程就是把知识点从“识别”升级到“生成”的过程。当你能设计出变体题说明你已经吃透了底层逻辑。6.3 反向设计基于业务需求倒推练习表结构假设你现在负责一个在线教育平台需要统计“课程完课率”。你会怎么设计练习表courses表course_id, name, total_lessons总课时users表user_id, usernameuser_course_progress表user_id, course_id, completed_lessons已完成课时last_access_time然后设计题“查询完课率completed_lessons/total_lessons大于80%的课程名称、完课率、学习用户数”。这个过程会让你深刻理解表结构设计不是技术问题而是业务抽象能力的体现。练习题的users/orders/products是电商范式你得学会把它迁移到教育、医疗、物流等任何领域。最后分享一个小技巧把练习题答案SQL里的所有关键字SELECT, FROM, WHERE, JOIN, GROUP BY, HAVING, ORDER BY, LIMIT用不同颜色高亮打印出来贴在显示器边框。每次写SQL前扫一眼这个“语法罗盘”肌肉记忆会越来越强。SQL不是靠背出来的是靠一遍遍写、一遍遍错、一遍遍修正刻进手指和大脑的。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表