1. MySQL SQL练习题详解:从入门到精通的实战指南
作为一名数据库工程师,我经常被问到如何系统提升SQL编写能力。市面上大多数教程要么过于基础,要么缺乏实战场景。今天我将通过一套精心设计的MySQL练习题,带大家从基础查询到复杂分析,逐步掌握SQL的核心应用技巧。这些题目全部来自我过去五年面试候选人和培训新人的真实案例,每个题目都经过精心打磨,覆盖了90%的实际工作场景。
2. 环境准备与数据建模
2.1 本地开发环境配置
我推荐使用Docker快速搭建MySQL实验环境,避免污染本地系统。以下是最新的MySQL 8.0容器启动命令:
bash复制docker run --name mysql-practice -e MYSQL_ROOT_PASSWORD=yourpassword -p 3306:3306 -d mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
注意:字符集一定要用utf8mb4而非utf8,否则会遇到emoji存储问题。这是我踩过的坑,MySQL的utf8实际只支持3字节字符。
2.2 练习数据库设计
我们模拟一个电商业务场景,包含五张核心表:
sql复制CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
register_date DATE NOT NULL,
vip_level ENUM('normal', 'gold', 'platinum') DEFAULT 'normal'
);
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
order_date DATETIME NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
CREATE TABLE order_details (
detail_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
CREATE TABLE user_logs (
log_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
action VARCHAR(20) NOT NULL,
log_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
实战经验:在订单明细表中冗余unit_price字段是必要的业务设计,因为商品价格可能变动,需要记录下单时的实际价格。
3. 基础查询进阶训练
3.1 多表连接查询实战
题目1:查询每个用户的订单总金额,显示用户名和消费总额
sql复制SELECT
u.username,
SUM(o.total_amount) AS total_spent
FROM
users u
LEFT JOIN
orders o ON u.user_id = o.user_id
GROUP BY
u.user_id, u.username;
关键点:一定要用LEFT JOIN而非INNER JOIN,否则会漏掉未下单用户。这是新人常犯的错误。
题目2:找出消费金额超过1000元的VIP客户
sql复制SELECT
u.user_id,
u.username,
SUM(o.total_amount) AS total_spent
FROM
users u
JOIN
orders o ON u.user_id = o.user_id
WHERE
u.vip_level != 'normal'
GROUP BY
u.user_id, u.username
HAVING
total_spent > 1000
ORDER BY
total_spent DESC;
注意:WHERE过滤行,HAVING过滤组,这个执行顺序不能搞混。我曾见过因为这个错误导致查询超时的生产事故。
3.2 子查询与派生表应用
题目3:查询从未下单的用户列表
sql复制SELECT
user_id,
username
FROM
users
WHERE
user_id NOT IN (
SELECT DISTINCT user_id
FROM orders
);
更高效的写法:
sql复制SELECT
u.user_id,
u.username
FROM
users u
LEFT JOIN
orders o ON u.user_id = o.user_id
WHERE
o.order_id IS NULL;
性能对比:当users表很大时,第二种写法效率更高。可以用EXPLAIN验证执行计划。
4. 窗口函数深度解析
4.1 排名与分页技巧
题目4:计算每个商品类别的销售额排名
sql复制SELECT
p.category,
p.product_name,
SUM(od.quantity * od.unit_price) AS sales_amount,
RANK() OVER (PARTITION BY p.category ORDER BY SUM(od.quantity * od.unit_price) DESC) AS sales_rank
FROM
products p
JOIN
order_details od ON p.product_id = od.product_id
GROUP BY
p.category, p.product_name;
题目5:查询每个用户最近3笔订单
sql复制WITH user_orders AS (
SELECT
o.*,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn
FROM
orders o
)
SELECT
u.username,
uo.order_id,
uo.order_date,
uo.total_amount
FROM
user_orders uo
JOIN
users u ON uo.user_id = u.user_id
WHERE
uo.rn <= 3;
窗口函数是SQL进阶的分水岭。我面试时发现,能熟练使用窗口函数的候选人通常有扎实的SQL功底。
5. 性能优化实战技巧
5.1 索引设计原则
针对我们的练习库,推荐创建以下索引:
sql复制CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_order_details_order ON order_details(order_id);
CREATE INDEX idx_order_details_product ON order_details(product_id);
CREATE INDEX idx_user_logs_user ON user_logs(user_id);
血泪教训:不要在枚举类型字段上建索引(如vip_level),基数太低反而影响性能。我曾在一个千万级用户表上错误创建这种索引,导致查询性能下降30%。
5.2 执行计划分析
题目6:分析慢查询的性能瓶颈
sql复制EXPLAIN ANALYZE
SELECT
u.username,
p.product_name,
COUNT(*) AS purchase_count
FROM
users u
JOIN
orders o ON u.user_id = o.user_id
JOIN
order_details od ON o.order_id = od.order_id
JOIN
products p ON od.product_id = p.product_id
WHERE
u.register_date > '2023-01-01'
GROUP BY
u.username, p.product_name
HAVING
COUNT(*) > 5;
关键指标解读:
- type列显示ALL表示全表扫描,需要优化
- rows列显示估算扫描行数
- Extra列出现"Using temporary"表示需要优化GROUP BY
6. 复杂业务场景解决方案
6.1 留存率计算
题目7:计算次日留存率
sql复制WITH first_login AS (
SELECT
user_id,
DATE(MIN(log_time)) AS first_day
FROM
user_logs
WHERE
action = 'login'
GROUP BY
user_id
),
retention_data AS (
SELECT
fl.first_day,
COUNT(DISTINCT fl.user_id) AS new_users,
COUNT(DISTINCT CASE WHEN DATE(l.log_time) = DATE_ADD(fl.first_day, INTERVAL 1 DAY)
THEN l.user_id END) AS retained_users
FROM
first_login fl
LEFT JOIN
user_logs l ON fl.user_id = l.user_id AND l.action = 'login'
GROUP BY
fl.first_day
)
SELECT
first_day,
new_users,
retained_users,
ROUND(retained_users/new_users*100, 2) AS retention_rate
FROM
retention_data
ORDER BY
first_day;
6.2 RFM用户分层模型
题目8:实现RFM用户价值分析
sql复制WITH user_rfm AS (
SELECT
u.user_id,
u.username,
DATEDIFF(CURRENT_DATE, MAX(o.order_date)) AS recency,
COUNT(o.order_id) AS frequency,
SUM(o.total_amount) AS monetary
FROM
users u
LEFT JOIN
orders o ON u.user_id = o.user_id
GROUP BY
u.user_id, u.username
)
SELECT
user_id,
username,
recency,
frequency,
monetary,
CASE
WHEN recency <= 30 AND frequency >= 5 AND monetary >= 1000 THEN '高价值客户'
WHEN recency <= 90 AND frequency >= 2 THEN '潜力客户'
WHEN monetary >= 500 THEN '高消费客户'
WHEN recency > 180 THEN '流失风险客户'
ELSE '一般客户'
END AS user_segment
FROM
user_rfm;
7. 常见错误与调试技巧
7.1 日期处理陷阱
错误示例:
sql复制SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31';
正确写法:
sql复制SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date < '2023-02-01';
原因:BETWEEN包含边界值,当order_date是datetime类型时,会漏掉1月31日23:59:59之后的数据。
7.2 GROUP BY注意事项
错误示例:
sql复制SELECT product_id, product_name, AVG(price)
FROM products
GROUP BY product_id;
正确写法:
sql复制SELECT product_id, product_name, AVG(price)
FROM products
GROUP BY product_id, product_name;
MySQL 5.7+默认开启ONLY_FULL_GROUP_BY模式,非聚合列必须出现在GROUP BY中。
8. 扩展练习与学习路径
建议按以下顺序逐步提升:
- 单表基础查询(SELECT, WHERE, ORDER BY)
- 多表连接与聚合(JOIN, GROUP BY, HAVING)
- 子查询与复杂条件(EXISTS, IN, CASE WHEN)
- 窗口函数(OVER, PARTITION BY, RANK)
- 性能优化(EXPLAIN, 索引设计)
- 业务场景实战(留存、漏斗、RFM)
每个阶段建议完成5-10个典型练习题。我整理了一份包含50个经典练习题的清单,涵盖从入门到高级的所有知识点,需要的读者可以私信我获取。