MySQL SQL 查询面试题与答案
这套题专门训练“给出表结构,现场编写 SQL”。题目统一使用电商、员工和登录行为数据模型,避免每道题的字段语义不一致。
建议练习方式:
- 先只看题目,限时 5~15 分钟手写。
- 自己准备边界数据运行,不要只凭肉眼判断。
- 再展开参考答案,比较结果语义而不是只比较 SQL 文本。
- 最后用
EXPLAIN或EXPLAIN ANALYZE检查执行方式,并说明需要什么索引。
参考答案主要使用 MySQL 8.0。使用窗口函数或递归 CTE 的题目会标注版本,并在部分高频题中给出 MySQL 5.7 思路。
统一数据模型
用户、商品和订单
create table users (
id bigint primary key comment '用户ID',
username varchar(64) not null comment '用户名',
city varchar(64) comment '所在城市,未填写时为NULL',
register_time datetime not null comment '注册时间',
unique key uk_username(username),
key idx_users_city(city)
) engine = InnoDB default charset = utf8mb4 comment = '用户表';
create table categories (
id bigint primary key comment '商品分类ID',
category_name varchar(64) not null comment '商品分类名称',
unique key uk_category_name(category_name)
) engine = InnoDB default charset = utf8mb4 comment = '商品分类表';
create table products (
id bigint primary key comment '商品ID',
product_name varchar(128) not null comment '商品名称',
category_id bigint not null comment '商品分类ID',
price decimal(18, 2) not null comment '商品当前单价',
status varchar(20) not null comment '商品状态:ON_SALE-在售,OFF_SHELF-下架',
key idx_products_category(category_id),
constraint fk_products_category
foreign key(category_id) references categories(id),
constraint chk_products_price check(price >= 0)
) engine = InnoDB default charset = utf8mb4 comment = '商品表';
create table orders (
id bigint primary key comment '订单ID',
user_id bigint not null comment '下单用户ID',
status varchar(20) not null comment '订单状态:PENDING-待支付,PAID-已支付,CLOSED-已关闭',
total_amount decimal(18, 2) not null comment '订单总金额',
created_at datetime not null comment '订单创建时间',
key idx_orders_user_created(user_id, created_at desc, id desc),
key idx_orders_status_created(status, created_at),
constraint fk_orders_user
foreign key(user_id) references users(id)
) engine = InnoDB default charset = utf8mb4 comment = '订单表';
create table order_items (
id bigint primary key comment '订单明细ID',
order_id bigint not null comment '订单ID',
product_id bigint not null comment '商品ID',
quantity int not null comment '购买数量',
unit_price decimal(18, 2) not null comment '下单时商品单价',
key idx_items_order(order_id),
key idx_items_product(product_id),
constraint fk_items_order
foreign key(order_id) references orders(id),
constraint fk_items_product
foreign key(product_id) references products(id),
constraint chk_items_quantity check(quantity > 0)
) engine = InnoDB default charset = utf8mb4 comment = '订单明细表';员工和登录记录
create table departments (
id bigint primary key comment '部门ID',
department_name varchar(64) not null comment '部门名称',
unique key uk_department_name(department_name)
) engine = InnoDB default charset = utf8mb4 comment = '部门表';
create table employees (
id bigint primary key comment '员工ID',
employee_name varchar(64) not null comment '员工姓名',
department_id bigint not null comment '所属部门ID',
manager_id bigint comment '直属上级员工ID,最高负责人为NULL',
salary decimal(18, 2) not null comment '当前薪资',
hire_date date not null comment '入职日期',
key idx_employees_department_salary(department_id, salary desc, id),
key idx_employees_manager(manager_id),
constraint fk_employees_department
foreign key(department_id) references departments(id),
constraint fk_employees_manager
foreign key(manager_id) references employees(id)
) engine = InnoDB default charset = utf8mb4 comment = '员工表';
create table login_records (
id bigint primary key comment '登录记录ID',
user_id bigint not null comment '登录用户ID',
login_date date not null comment '登录日期,同一用户每天最多一条',
unique key uk_login_user_date(user_id, login_date),
key idx_login_date_user(login_date, user_id),
constraint fk_login_user
foreign key(user_id) references users(id)
) engine = InnoDB default charset = utf8mb4 comment = '用户每日登录记录表';数据模型关系速查
| 业务域 | 表名 | 中文名称 | 核心关系 |
|---|---|---|---|
| 用户 | users | 用户表 | 一个用户可以有多张订单、多个登录日期 |
| 商品 | categories | 商品分类表 | 一个分类可以包含多个商品 |
| 商品 | products | 商品表 | 每个商品属于一个分类,可以出现在多条订单明细中 |
| 交易 | orders | 订单表 | 每张订单属于一个用户,可以包含多条订单明细 |
| 交易 | order_items | 订单明细表 | 连接订单和商品,保存购买数量及下单时单价 |
| 组织 | departments | 部门表 | 一个部门可以包含多个员工 |
| 组织 | employees | 员工表 | 通过 manager_id 自关联形成上下级关系 |
| 行为 | login_records | 用户每日登录记录表 | 一个用户同一天最多保留一条登录记录 |
字段中文速查
用户、分类和商品
| 表名 | 字段 | 类型 | 可空 | 中文含义与口径 |
|---|---|---|---|---|
users | id | bigint | 否 | 用户 ID,主键 |
users | username | varchar(64) | 否 | 用户名,全局唯一 |
users | city | varchar(64) | 是 | 所在城市,未填写时为 NULL |
users | register_time | datetime | 否 | 用户注册时间 |
categories | id | bigint | 否 | 商品分类 ID,主键 |
categories | category_name | varchar(64) | 否 | 商品分类名称,全局唯一 |
products | id | bigint | 否 | 商品 ID,主键 |
products | product_name | varchar(128) | 否 | 商品名称 |
products | category_id | bigint | 否 | 商品分类 ID,关联 categories.id |
products | price | decimal(18,2) | 否 | 商品当前单价,不代表历史下单价格 |
products | status | varchar(20) | 否 | 商品状态:ON_SALE 在售,OFF_SHELF 下架 |
订单和订单明细
| 表名 | 字段 | 类型 | 可空 | 中文含义与口径 |
|---|---|---|---|---|
orders | id | bigint | 否 | 订单 ID,主键 |
orders | user_id | bigint | 否 | 下单用户 ID,关联 users.id |
orders | status | varchar(20) | 否 | 订单状态:PENDING 待支付、PAID 已支付、CLOSED 已关闭 |
orders | total_amount | decimal(18,2) | 否 | 订单总金额 |
orders | created_at | datetime | 否 | 订单创建时间 |
order_items | id | bigint | 否 | 订单明细 ID,主键 |
order_items | order_id | bigint | 否 | 所属订单 ID,关联 orders.id |
order_items | product_id | bigint | 否 | 商品 ID,关联 products.id |
order_items | quantity | int | 否 | 购买数量,必须大于 0 |
order_items | unit_price | decimal(18,2) | 否 | 下单时商品单价,不随商品当前价格变化 |
部门、员工和登录记录
| 表名 | 字段 | 类型 | 可空 | 中文含义与口径 |
|---|---|---|---|---|
departments | id | bigint | 否 | 部门 ID,主键 |
departments | department_name | varchar(64) | 否 | 部门名称,全局唯一 |
employees | id | bigint | 否 | 员工 ID,主键 |
employees | employee_name | varchar(64) | 否 | 员工姓名 |
employees | department_id | bigint | 否 | 所属部门 ID,关联 departments.id |
employees | manager_id | bigint | 是 | 直属上级员工 ID,自关联 employees.id;最高负责人为 NULL |
employees | salary | decimal(18,2) | 否 | 员工当前薪资 |
employees | hire_date | date | 否 | 入职日期 |
login_records | id | bigint | 否 | 登录记录 ID,主键 |
login_records | user_id | bigint | 否 | 登录用户 ID,关联 users.id |
login_records | login_date | date | 否 | 登录日期;user_id + login_date 唯一 |
SQL 题通用约定
- 已支付订单统一使用
status = 'PAID'。 - 已关闭订单使用
status = 'CLOSED',待支付使用PENDING。 - 金额以
decimal保存,不使用浮点数。 - 日期区间统一使用左闭右开,例如
[2026-08-01, 2026-09-01)。 - “第二高工资”默认指第二个不同的工资值,而不是排序后的第二行。
- “最近一笔订单”在时间相同时使用
id desc作为稳定排序。 - 除非题目明确允许,否则不能依赖无
order by的返回顺序。
第一组:基础查询
题目 1:查询指定月份的已支付订单
查询 2026 年 8 月创建的已支付订单,返回订单 ID、用户 ID、金额和创建时间,按创建时间倒序;时间相同按订单 ID 倒序。
参考答案
select id,
user_id,
total_amount,
created_at
from orders
where status = 'PAID'
and created_at >= '2026-08-01 00:00:00'
and created_at < '2026-09-01 00:00:00'
order by created_at desc, id desc;不要写 date_format(created_at, '%Y-%m') = '2026-08',它可能让普通时间索引无法直接完成范围定位。左闭右开区间也不会遗漏毫秒或微秒。
题目 2:查询城市并正确处理 NULL
查询城市为北京或者城市信息未填写的用户。
参考答案
select id, username, city
from users
where city = '北京'
or city is null;不能使用 city = null。SQL 中 NULL 要使用 is null 或 is not null 判断。
题目 3:查询用户来自哪些城市
返回非空城市列表,去重并按城市名升序排列。
参考答案
select distinct city
from users
where city is not null
order by city;排序结果受列的字符集和 collation 影响。
题目 4:使用 CASE 对订单金额分级
金额大于等于 1000 为 HIGH,大于等于 100 且小于 1000 为 MEDIUM,其他为 LOW。
参考答案
select id,
total_amount,
case
when total_amount >= 1000 then 'HIGH'
when total_amount >= 100 then 'MEDIUM'
else 'LOW'
end as amount_level
from orders;case 按顺序匹配,所以应该先判断较高阈值。
题目 5:找出名称中包含 MySQL 的商品
不区分大小写的前提由列 collation 决定。
参考答案
select id, product_name, price
from products
where product_name like '%MySQL%';前导 % 通常无法使用普通 B+Tree 做前缀定位。数据量大、检索复杂时应评估全文索引或搜索引擎,而不是只增加普通索引。
题目 6:查询第 2 页订单
每页 20 条,按 created_at desc, id desc 排序。
参考答案
select id, user_id, status, total_amount, created_at
from orders
order by created_at desc, id desc
limit 20 offset 20;页码较深时 offset 分页需要扫描并丢弃大量记录。线上大表更推荐游标分页:
select id, user_id, status, total_amount, created_at
from orders
where created_at < ?
or (created_at = ? and id < ?)
order by created_at desc, id desc
limit 20;题目 7:查询从未设置城市的注册用户数
参考答案
select count(*) as user_count
from users
where city is null;这里使用 count(*) 是统计满足条件的行。count(city) 会忽略 NULL,结果反而为 0。
题目 8:查询价格处于分类平均价格之上的商品
返回商品 ID、名称、分类 ID、商品价格和所在分类平均价格。
参考答案
select p.id,
p.product_name,
p.category_id,
p.price,
a.avg_price
from products p
join (
select category_id, avg(price) as avg_price
from products
group by category_id
) a on a.category_id = p.category_id
where p.price > a.avg_price;MySQL 8 也可以使用窗口函数:
with priced as (
select p.*,
avg(price) over(partition by category_id) as avg_price
from products p
)
select id, product_name, category_id, price, avg_price
from priced
where price > avg_price;第二组:聚合、Join 与子查询
题目 9:统计每个用户的已支付订单
返回所有用户,包括没有已支付订单的用户;输出订单数和支付总金额,没有订单时显示 0。
参考答案
select u.id,
u.username,
count(o.id) as paid_order_count,
coalesce(sum(o.total_amount), 0) as paid_amount
from users u
left join orders o
on o.user_id = u.id
and o.status = 'PAID'
group by u.id, u.username;右表状态条件必须放在 on 中。如果放到 where o.status = 'PAID',没有订单的用户会被过滤,left join 实际退化为内连接语义。不能使用 count(*),否则无订单用户也会因为左表占位行被统计为 1。
题目 10:查询从未下单的用户
分别用 NOT EXISTS 和 LEFT JOIN 编写。
参考答案
not exists:
select u.id, u.username
from users u
where not exists (
select 1
from orders o
where o.user_id = u.id
);left join:
select u.id, u.username
from users u
left join orders o on o.user_id = u.id
where o.id is null;不要机械使用 not in。如果子查询结果包含 NULL,会受到三值逻辑影响。
题目 11:统计每个月的订单指标
统计每月订单总数、支付订单数、支付金额,按月份升序。
参考答案
select date_format(created_at, '%Y-%m') as order_month,
count(*) as order_count,
sum(case when status = 'PAID' then 1 else 0 end) as paid_count,
sum(case when status = 'PAID' then total_amount else 0 end) as paid_amount
from orders
group by date_format(created_at, '%Y-%m')
order by order_month;这是报表聚合写法。如果只统计一个固定时间范围,应同时在 where 使用原始时间列范围,先减少扫描数据:
where created_at >= ? and created_at < ?题目 12:查询支付金额超过 10000 的用户
只统计已支付订单,返回用户 ID、用户名和支付金额,按金额倒序。
参考答案
select u.id,
u.username,
sum(o.total_amount) as paid_amount
from users u
join orders o on o.user_id = u.id
where o.status = 'PAID'
group by u.id, u.username
having sum(o.total_amount) > 10000
order by paid_amount desc, u.id;where 过滤分组前的订单行,having 过滤聚合后的用户组。
题目 13:查询从未被购买的商品
“购买”只统计已支付订单中的订单明细。
参考答案
select p.id, p.product_name
from products p
where not exists (
select 1
from order_items oi
join orders o on o.id = oi.order_id
where oi.product_id = p.id
and o.status = 'PAID'
);如果只判断 order_items 是否存在,会把待支付和已关闭订单也当作已购买。
题目 14:查询第二高的不同工资
没有第二高工资时返回 NULL。
参考答案
MySQL 5.7 和 8.0 都可用:
select max(salary) as second_highest_salary
from employees
where salary < (
select max(salary)
from employees
);MySQL 8 窗口函数写法:
select max(case when salary_rank = 2 then salary end) as second_highest_salary
from (
select salary,
dense_rank() over(order by salary desc) as salary_rank
from employees
) ranked;题目要求的是第二个不同工资值,所以不能简单 order by salary desc limit 1 offset 1,重复最高工资会产生错误。
题目 15:查询工资高于本部门平均值的员工
返回部门名、员工名、工资和部门平均工资。
参考答案
select d.department_name,
e.employee_name,
e.salary,
a.avg_salary
from employees e
join departments d on d.id = e.department_id
join (
select department_id, avg(salary) as avg_salary
from employees
group by department_id
) a on a.department_id = e.department_id
where e.salary > a.avg_salary
order by d.id, e.salary desc, e.id;MySQL 8 也可以使用 avg(salary) over(partition by department_id),再在外层过滤。
题目 16:查询订单表头金额与明细金额不一致的订单
明细金额为 quantity * unit_price 之和,金额比较保留两位小数。
参考答案
select o.id,
o.total_amount,
coalesce(sum(oi.quantity * oi.unit_price), 0) as item_amount
from orders o
left join order_items oi on oi.order_id = o.id
group by o.id, o.total_amount
having round(o.total_amount, 2)
<> round(coalesce(sum(oi.quantity * oi.unit_price), 0), 2);金额字段应使用 decimal。真实业务还要明确优惠、运费、税费、退款和舍入发生在行级还是订单级,不能假定表头金额一定等于商品原价之和。
第三组:MySQL 8 窗口函数
题目 17:查询每个部门工资最高的三名员工
同工资共享名次,并允许同一部门返回超过三行。
参考答案
with ranked as (
select e.*,
dense_rank() over(
partition by department_id
order by salary desc
) as salary_rank
from employees e
)
select d.department_name,
r.employee_name,
r.salary,
r.salary_rank
from ranked r
join departments d on d.id = r.department_id
where r.salary_rank <= 3
order by d.id, r.salary_rank, r.id;如果题目要求每个部门严格返回三行,应使用 row_number 并增加稳定排序;如果同工资共享名次,应使用 dense_rank 或按题意使用 rank。
题目 18:查询每个用户最近一笔订单
时间相同选择 ID 最大的订单。
参考答案
MySQL 8:
with ranked as (
select o.*,
row_number() over(
partition by user_id
order by created_at desc, id desc
) as rn
from orders o
)
select id, user_id, status, total_amount, created_at
from ranked
where rn = 1;MySQL 5.7 可使用 not exists:
select o.*
from orders o
where not exists (
select 1
from orders newer
where newer.user_id = o.user_id
and (
newer.created_at > o.created_at
or (newer.created_at = o.created_at and newer.id > o.id)
)
);推荐索引:(user_id, created_at desc, id desc)。
题目 19:计算用户订单累计支付金额
只统计已支付订单,按时间和订单 ID 累计。
参考答案
select user_id,
id as order_id,
created_at,
total_amount,
sum(total_amount) over(
partition by user_id
order by created_at, id
rows between unbounded preceding and current row
) as running_paid_amount
from orders
where status = 'PAID'
order by user_id, created_at, id;显式使用 rows 框架,避免相同排序值下默认 range 框架把同值行一起纳入。
题目 20:计算每笔订单与上一笔订单的金额差
按同一用户的订单时间排序。
参考答案
with compared as (
select user_id,
id as order_id,
created_at,
total_amount,
lag(total_amount) over(
partition by user_id
order by created_at, id
) as previous_amount
from orders
)
select user_id,
order_id,
created_at,
total_amount,
previous_amount,
total_amount - previous_amount as amount_diff
from compared;用户第一笔订单没有上一笔,差值为 NULL。不要无条件转成零,除非业务明确这样定义。
题目 21:计算每个用户的支付金额占比
返回订单金额、用户支付总金额和订单占该用户支付金额的比例。
参考答案
select user_id,
id as order_id,
total_amount,
sum(total_amount) over(partition by user_id) as user_paid_amount,
total_amount
/ nullif(sum(total_amount) over(partition by user_id), 0)
as amount_ratio
from orders
where status = 'PAID';nullif(..., 0) 防止除零。展示为百分比时可以在应用层格式化,避免过早把数值转成字符串。
题目 22:计算月支付金额和环比增长率
环比为 (本月金额 - 上月金额) / 上月金额。
参考答案
with monthly as (
select date_format(created_at, '%Y-%m-01') as month_start,
sum(total_amount) as paid_amount
from orders
where status = 'PAID'
group by date_format(created_at, '%Y-%m-01')
), compared as (
select month_start,
paid_amount,
lag(paid_amount) over(order by month_start) as previous_amount
from monthly
)
select month_start,
paid_amount,
previous_amount,
(paid_amount - previous_amount) / nullif(previous_amount, 0)
as growth_rate
from compared
order by month_start;这个答案只显示有订单的月份。如果题目要求没有订单的月份也显示零,需要先生成完整月份日历,再左连接月度聚合。
题目 23:计算员工工资排名和与部门最高工资的差值
参考答案
select department_id,
id,
employee_name,
salary,
dense_rank() over(
partition by department_id
order by salary desc
) as salary_rank,
max(salary) over(partition by department_id) - salary
as gap_to_department_max
from employees
order by department_id, salary_rank, id;一个窗口负责排名,另一个窗口计算部门最高工资,不需要把员工表重复 Join 两次。
第四组:连续行为、留存与递归查询
题目 24:查询连续登录至少 3 天的用户
login_records 已通过唯一键保证同一用户一天只有一条记录。
参考答案
使用“日期减去行号得到同组固定值”的 Gaps and Islands 方法:
with numbered as (
select user_id,
login_date,
row_number() over(
partition by user_id
order by login_date
) as rn
from login_records
), grouped as (
select user_id,
login_date,
date_sub(login_date, interval rn day) as group_key
from numbered
), streaks as (
select user_id,
min(login_date) as start_date,
max(login_date) as end_date,
count(*) as consecutive_days
from grouped
group by user_id, group_key
)
select user_id, start_date, end_date, consecutive_days
from streaks
where consecutive_days >= 3
order by user_id, start_date;如果原表同一天可能登录多次,必须先 select distinct user_id, date(login_time) 去重,否则行号会把同一天重复记录误当成连续天数。
题目 25:查询每个用户最长连续登录天数
参考答案
with numbered as (
select user_id,
login_date,
row_number() over(
partition by user_id
order by login_date
) as rn
from login_records
), grouped as (
select user_id,
date_sub(login_date, interval rn day) as group_key
from numbered
), streaks as (
select user_id,
group_key,
count(*) as consecutive_days
from grouped
group by user_id, group_key
)
select user_id,
max(consecutive_days) as max_consecutive_days
from streaks
group by user_id;这是连续区间题的通用模型:先排序编号,再构造连续区间分组键,最后对每个区间聚合。
题目 26:计算次日留存率
以用户注册日期为基准,计算每个注册日的注册人数、次日登录人数和次日留存率。
参考答案
select date(u.register_time) as register_date,
count(*) as registered_users,
count(l.user_id) as next_day_login_users,
count(l.user_id) / nullif(count(*), 0) as next_day_retention
from users u
left join login_records l
on l.user_id = u.id
and l.login_date = date(u.register_time) + interval 1 day
group by date(u.register_time)
order by register_date;login_records 的唯一键保证每个用户每天最多一行,所以 count(l.user_id) 不会重复统计。如果没有唯一约束,应在关联前按用户和日期去重。
大表中 date(u.register_time) 主要用于分组;如果限制注册范围,仍应额外使用原始时间列区间过滤。
题目 27:查询复购用户
定义为至少有两个不同日期的已支付订单。
参考答案
select user_id,
count(*) as paid_order_count,
count(distinct date(created_at)) as paid_days
from orders
where status = 'PAID'
group by user_id
having count(distinct date(created_at)) >= 2;这里题目明确要求不同日期,所以不能只使用 count(*) >= 2。真实复购口径还可能排除退款、测试订单或要求间隔超过一定时间。
题目 28:查询买过分类下所有在售商品的用户
即“关系除法”问题:用户必须购买目标分类中每一个 status = 'ACTIVE' 的商品。
参考答案
假设目标分类 ID 为 ?:
select o.user_id
from orders o
join order_items oi on oi.order_id = o.id
join products p on p.id = oi.product_id
where o.status = 'PAID'
and p.category_id = ?
and p.status = 'ACTIVE'
group by o.user_id
having count(distinct p.id) = (
select count(*)
from products target
where target.category_id = ?
and target.status = 'ACTIVE'
);需要明确空集合语义:如果该分类当前没有在售商品,是否所有用户都算“买过全部商品”?上面的 SQL 不会返回用户。业务定义不同,SQL 也要调整。
题目 29:递归查询某经理的全部下属
返回直接和间接下属,并显示层级。要求 MySQL 8。
参考答案
with recursive subordinate_tree as (
select id,
employee_name,
manager_id,
1 as depth
from employees
where manager_id = ?
union all
select e.id,
e.employee_name,
e.manager_id,
t.depth + 1
from employees e
join subordinate_tree t on e.manager_id = t.id
where t.depth < 20
)
select id, employee_name, manager_id, depth
from subordinate_tree
order by depth, id;employees(manager_id) 需要索引。生产还应阻止组织数据形成环;这里只用深度限制避免异常数据无限递归,并不能识别所有环。
题目 30:找出每个分类销量最高的商品
只统计已支付订单,销量相同的商品全部返回。
参考答案
with product_sales as (
select p.category_id,
p.id as product_id,
p.product_name,
sum(oi.quantity) as sold_quantity
from orders o
join order_items oi on oi.order_id = o.id
join products p on p.id = oi.product_id
where o.status = 'PAID'
group by p.category_id, p.id, p.product_name
), ranked as (
select ps.*,
dense_rank() over(
partition by category_id
order by sold_quantity desc
) as sales_rank
from product_sales ps
)
select c.category_name,
r.product_id,
r.product_name,
r.sold_quantity
from ranked r
join categories c on c.id = r.category_id
where r.sales_rank = 1
order by c.id, r.product_id;必须先按商品聚合销量,再对聚合结果排名。不能在原始订单明细上直接排名,否则排名的是某一行明细数量,不是商品总销量。
第五组:加试题,只给思路不直接给完整答案
完成前 30 题后,再独立实现以下题目:
- 找出工资高于直属经理的员工。
- 查询每个部门入职时间最早的员工,时间相同全部返回。
- 查询每个用户第一次和第二次已支付订单的间隔天数。
- 计算每个注册月份用户在注册后 30 天内的付费转化率。
- 查询连续 3 个月都有已支付订单的用户。
- 查询每个商品连续无销量的最长天数,需要先生成日期日历。
- 找出购买商品集合完全相同的用户对。
- 查询同一用户 10 分钟内连续创建的疑似重复订单。
- 计算商品每 7 天移动平均销量。
- 查询订单金额中位数,分别考虑总行数为奇数和偶数。
这些题没有唯一写法。至少说明:结果粒度、去重规则、NULL、并列值、稳定排序、日期边界和建议索引。
面试写 SQL 的检查顺序
拿到题目后不要立刻写代码,先确认:
- 结果一行代表什么:用户、订单、商品还是用户月份。
- 关联是一对一还是一对多:Join 后是否放大行数。
- 统计口径:是否只算已支付、是否排除退款、是否按不同日期去重。
- 并列规则:Top 3 是严格三行还是同分共享名次。
- NULL 语义:没有订单要不要显示,金额要显示 NULL 还是 0。
- 时间边界:使用左闭右开,时区是什么。
- 稳定排序:时间相同用哪个唯一键打破平局。
- MySQL 版本:能否使用窗口函数和递归 CTE。
- 性能:过滤能否提前,Join Key 和高频条件是否有索引。
- 验证数据:空表、重复值、同一时间、NULL、并列第一、跨月和无明细订单。
常见错误
| 错误 | 典型后果 |
|---|---|
left join 右表条件写进 where | 无匹配左表行被过滤,语义变内连接 |
左连接后使用 count(*) | 无明细对象也被统计为 1 |
一对多 Join 后直接 sum 表头金额 | 同一订单金额被明细行重复累加 |
第二高工资直接 offset 1 | 最高工资重复时结果错误 |
| Top N 没有定义并列规则 | row_number、rank、dense_rank 选错 |
| 只按时间排序 | 时间相同结果不稳定,分页重复或漏数据 |
not in 子查询可能含 NULL | 结果因三值逻辑异常 |
| 日期列套函数过滤 | 普通时间索引可能无法范围定位 |
| 月环比不补月份 | 没数据月份消失,上一行不等于上个月 |
| 连续登录前不按天去重 | 一天多次登录被误算为多天 |
| 各分片平均值再次平均 | 全局平均值错误,应汇总总和与总数 |
没有 order by 却依赖返回顺序 | 数据量或执行计划变化后顺序改变 |
索引思考题
给每道 SQL 再回答一次“为什么需要这个索引”:
| 查询 | 候选索引 | 目标 |
|---|---|---|
| 用户最近订单 | orders(user_id, created_at desc, id desc) | 过滤、排序和 limit 连续完成 |
| 状态时间范围 | orders(status, created_at) | 等值状态后按时间范围扫描 |
| 订单明细汇总 | order_items(order_id) | 快速定位订单的全部明细 |
| 商品销量 | order_items(product_id) | 按商品关联明细 |
| 部门工资排名 | employees(department_id, salary desc, id) | 按部门访问并减少排序成本的可能性 |
| 组织树 | employees(manager_id) | 每轮递归定位直接下属 |
| 用户登录日 | login_records(user_id, login_date) | 用户内按日期顺序访问 |
窗口函数有索引也不一定完全避免排序,是否使用、扫描多少和是否产生临时表仍应通过执行计划验证。
自测评分
每题 5 分:
- 1 分:结果粒度和统计口径正确。
- 1 分:Join、NULL、去重和并列语义正确。
- 1 分:时间和排序边界正确。
- 1 分:SQL 能在目标 MySQL 版本运行。
- 1 分:能说明索引、执行计划和大数据量风险。
前 30 题总分 150:
- 120 分以上:具备常规面试 SQL 编写能力。
- 135 分以上:能较稳定应对追问和边界条件。
- 145 分以上:继续做加试题,并用百万级测试数据分析执行计划。
关联知识点
- SQL 基础:查询、条件、排序和分页。
- SQL 进阶:CTE、窗口函数、子查询和 NULL。
- MySQL 函数:日期、聚合、窗口与函数性能。
- 查询与 Join:驱动表、Nested Loop、绑定条件与 N+1。
- 复杂关联查询实战:多条一对多、时态关联、财务核对和父子分页。
- 索引:联合索引、覆盖索引和最左前缀。
- EXPLAIN:验证访问类型、扫描行数和排序。
- 商业场景训练营:事务、库存、锁、日志与复制实战。
本章小结
SQL 面试真正考的是把业务语言转换成确定的数据粒度、关联关系和统计口径。写出结果只是第一步,还要处理 NULL、重复、并列、时间边界和版本差异,并能说明数据量扩大后如何通过索引和执行计划验证。建议先完成前 30 题,再做 10 道无答案加试题,最后对慢查询使用真实数据做 EXPLAIN ANALYZE。
