Shop 系统分级练习
本篇围绕 Shop 示例数据库,分 6 大题,每题场景说明、关键 SQL、简要点评
一、★ 创建数据库与表,基础 CRUD(20m)
场景:新建库
shop,创建users与products两表,并演练基本增删查改。
-- 1. 创建 shop 库并切换
create database shop charset=utf8mb4;
use shop;
-- 2. 创建 users 表:user_id 主键自增,email 唯一
create table users (
user_id int primary key auto_increment,
username varchar(50) not null,
email varchar(50) unique
) engine=innodb default charset=utf8mb4;
-- 3. 创建 products 表:product_id 主键自增
create table products (
product_id int primary key auto_increment,
name varchar(50) not null,
price decimal(10,2) not null
) engine=innodb default charset=utf8mb4;
点评
primary key auto_increment:定义自增主键。unique:保证邮箱/商品名不重复。- 推荐:建表后先
show tables; desc users; desc products;验证结构。
基础 CRUD 演练
-- 插入多条 users
insert into users (username,email)
values ('tets1','3540@qq.com'),
('test2','test@gmail.com'),
('jjt','jjt@qq.com');
-- 插入多条 products
insert into products (name,price)
values ('牛奶',5.00),
('香蕉',2.00),
('苹果',6.00);
-- 查询全表
select * from users;
select * from products;
-- 更新:修改 user_id=1 的 email
update users
set email = 'test@qq.com'
where user_id = 1;
-- 删除:移除 product_id=3 的商品
delete from products
where product_id = 3;
二、★★ 增删改查进阶:条件、排序、分页(20m)
场景:基于
products表,练习多种查询、排序、分页与批量操作。
-- 1. 条件过滤
select * from products
where price > 3.00; -- 价格大于 3
select * from products
where name like '%牛%'; -- 名称包含“牛”
-- 2. 多字段排序
select * from products
order by price desc, -- 先按价格降序
name asc; -- 再按名称升序
-- 3. 分页:第 2 页,每页 2 条
select * from products
order by product_id asc
limit 2 offset 2;
-- 4. 批量更新:5–10 元之间打 5 折
update products
set price = price * 0.5
where price between 5.00 and 10.00;
-- 5. 批量删除:删除价格为 0 或 null
delete from products
where price = 0.00
or price is null;
点评
limit … offset …模拟分页;offset从 0 起。- 批量更新/删除前,建议先
select确认影响行数。
三、★★ 主键与外键:关联表设计(20m)
场景:在
shop库创建orders表,演练外键约束与参照完整性。
-- 1. 查看父表结构
desc users;
desc products;
-- 2. 创建 orders 表
create table orders (
order_id int primary key auto_increment,
user_id int not null,
product_id int not null,
order_date date not null,
foreign key (user_id)
references users(user_id)
on delete cascade, -- 删除用户时级联删除订单
foreign key (product_id)
references products(product_id)
on delete restrict -- 禁止删除有订单的商品
) engine=innodb default charset=utf8mb4;
-- 3. 插入合法记录
insert into orders (user_id,product_id,order_date)
values (1,1,'2025-05-16'),
(2,2,'2025-05-16'),
(3,1,'2025-05-15');
-- 4. 插入非法外键(观察错误)
insert into orders (user_id,product_id,order_date)
values (1,99,'2025-05-17');
点评
- 外键依赖:父表记录必须先插入,否则子表会报
1452错误。 on delete cascade/restrict控制删除行为,满足业务需求。
四、★★★ 索引实战:普通、唯一、复合索引(20m)
场景:为三表添加多种索引,并用
explain analyze验证效果。
-- 普通索引
create index idx_users_username on users(username);
create index idx_products_price on products(price);
-- 唯一索引
create unique index ux_products_name on products(name);
-- 复合索引:orders(user_id,order_date)
create index idx_orders_user_date on orders(user_id,order_date);
-- 对比:有索引 vs 无索引
explain analyze
select * from orders
where user_id = 1
and order_date = '2025-05-16';
explain analyze
select * from orders
where order_date = '2025-05-16';
点评
- 最左前缀:复合索引
(user_id,order_date)只加速同时包含两列或仅user_id的查询;仅按order_date仍全表扫描。 - 覆盖索引(只查询索引列)可避免回表,
extra中显示using index。
索引对查询性能的影响对比
对比两条查询在 orders 表上的执行计划与实际耗时,重点观察索引命中及全表扫描的差异。
| 查询条件 | 执行计划类型 | 命中索引 | cost | rows | actual time (ms) | 备注 |
|---|---|---|---|---|---|---|
where user_id = 1 and order_date = '2025-05-16' |
index lookup | fh_userid_orderdate |
0.35 | 1 | 19.3 – 20.6 | 复合索引精准匹配,直接定位到单行,最快 |
where order_date = '2025-05-16' |
table scan + filter | 无 | 0.65 | 4 | 21.1 – 25.8 | 索引未命中,全表扫描后再过滤,扫描行数更多,略慢 |
关键要点
-
索引命中 vs 全表扫描
- 当查询条件完全匹配复合索引列顺序(最左前缀)时,MySQL 利用
index lookup,只读取必要的索引页,无需扫描全表。 - 单列查询非最左索引(仅按
order_date)会触发全表扫描,再进行filter,效率较低。
- 当查询条件完全匹配复合索引列顺序(最左前缀)时,MySQL 利用
-
cost 与实际耗时
cost只是优化器估算值,实际actual time更能反映真实性能。- 尽管表很小(仅 4 行),全表扫描与索引查找的耗时差距在 2–6 ms 之间;在大表场景下,这个差距会被放大数十倍以上。
-
最佳实践
- 根据最常用查询组合设计复合索引,保证“最左前缀”原则。
- 对于单独按
order_date频繁查询的场景,可再额外建单列索引:create index idx_orders_date on orders(order_date); - 使用
explain analyze查看真实行数与耗时,帮助评估索引是否有效。
五、★★★ 多表联查:inner/left join、group by、聚合(20m)
场景:在
shop库中,编写多种联表统计查询。
-- 1. inner join:查询每位用户及购买过的商品名
select
u.user_id,
u.username,
p.name as product_name
from users u
inner join orders o on u.user_id = o.user_id
inner join products p on o.product_id = p.product_id;
-- 2. left join + 子查询:查询每位用户最近一次下单日期
select
u.user_id,
u.username,
o.last_order_date
from users u
left join (
select user_id, max(order_date) as last_order_date
from orders
group by user_id
) o on u.user_id = o.user_id;
-- 3. group by + count:按用户统计订单数,过滤 >1
select
u.user_id,
u.username,
count(o.order_id) as order_count
from users u
join orders o on u.user_id = o.user_id
group by u.user_id,u.username
having order_count > 1;
-- 4. group by + sum:按商品统计销售总额
select
p.product_id,
p.name,
sum(p.price) as total_sales
from products p
join orders o on p.product_id = o.product_id
group by p.product_id,p.name;
点评
inner join只保留两表都有匹配的行;left join保留左表所有行并补null。having作用于聚合结果,where只能过滤原始行。
六、★★★ DDL 进阶:alter table 深入(20m)
场景:对已建三表做在线结构调整,并验证新结构。
-- 1. 增加列:users.signup_date
alter table users
add column signup_date date not null default current_date;
-- 2. 修改列类型:products.price → decimal(10,2)
alter table products
modify column price decimal(10,2);
-- 3. 重命名列:orders.order_date → purchase_date
alter table orders
rename column order_date to purchase_date;
-- 4. 删除列:orders.metrics
alter table orders
drop column metrics;
-- 5. 重建复合索引:调整列顺序
alter table orders
drop index idx_orders_user_date,
add index idx_orders_date_user (purchase_date,user_id);
-- 验证:插入、更新、查询
insert into orders (user_id,product_id,purchase_date)
values (3,2,current_date());
update orders
set purchase_date = '2025-05-17'
where order_id = 3;
select * from orders
where user_id = 3
and purchase_date between '2025-05-13' and '2025-05-20';
点评
rename column(MySQL 8+ 支持)无需指定类型;老版本用change column。- 每次
alter table后,务必desc或select验证。
作于2025年5月21日