在电商、零售等业务场景中,“查询每个用户最近购买的N件商品”是高频需求,比如用户中心展示“最近订单”、基于近期消费行为做个性化推荐等。
一、需求与表结构定义
在动手写SQL前,先统一需求和基础表结构,确保所有方案基于相同的数据模型,避免理解偏差。
1. 核心需求
- 关联用户表(
users
)、订单表(orders
)、商品表(products
); - 为每个用户筛选出最近3条购买记录(按订单创建时间倒序);
- 最终返回结果包含:用户名、用户邮箱、商品名称、订单时间、订单金额。
2. 表结构定义
-- 用户表:存储用户基础信息
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL COMMENT '用户名',
email VARCHAR(100) NOT NULL UNIQUE COMMENT '用户邮箱',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
);
-- 商品表:存储商品信息
CREATE TABLE products (
id INT PRIMARY KEY AUTO_INCREMENT,
product_name VARCHAR(100) NOT NULL COMMENT '商品名称',
price DECIMAL(10,2) NOT NULL COMMENT '商品单价',
stock INT DEFAULT 0 COMMENT '库存数量'
);
-- 订单表:存储用户购买记录(核心关联表)
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL COMMENT '关联用户ID',
product_id INT NOT NULL COMMENT '关联商品ID',
amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '订单创建时间',
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
二、方案一:LATERAL连接(直观高效的“用户级筛选”)
LATERAL
是MySQL 8.0及以上版本引入的关键字,核心优势是让子查询能引用外部表的字段,特别适合“为每个用户单独筛选数据”的场景,逻辑上更贴近“逐个用户查最近订单”的思维。
1. 实现代码
SELECT
u.username, -- 用户名
u.email, -- 用户邮箱
ro.product_name, -- 商品名称
ro.created_time, -- 订单时间(最近的优先)
ro.amount -- 订单金额
FROM users u
-- LATERAL关键:子查询可直接引用外部表u的字段(如u.id)
JOIN LATERAL (
-- 子查询:为当前用户(u.id)筛选最近3条订单
SELECT
p.product_name,
o.created_time,
o.amount
FROM orders o
-- 关联商品表,获取商品名称
JOIN products p ON o.product_id = p.id
-- 锁定当前用户的订单,避免跨用户查询
WHERE o.user_id = u.id
-- 按订单创建时间倒序,确保最近的订单排在前面
ORDER BY o.created_time DESC
-- 限制仅返回3条记录,满足“最近3件商品”需求
LIMIT 3
) AS ro -- ro是“recent orders(最近订单)”的缩写,便于理解
ON TRUE; -- LATERAL连接的固定写法,无需额外关联条件
2. 核心原理
- 逐用户遍历:MySQL会先遍历
users
表中的每一条记录(即每个用户); - 子查询精准筛选:针对每个用户,执行
LATERAL
后的子查询,通过o.user_id = u.id
锁定该用户的订单,再用ORDER BY + LIMIT 3
筛选出最近的3条; - 结果拼接:将用户的基础信息(用户名、邮箱)与该用户的最近3条订单信息拼接,形成最终结果。
3. 优缺点分析
优点 | 缺点 |
---|---|
语法直观,逻辑清晰,容易理解(直接对应“每个用户查最近3单”的业务逻辑) | 版本兼容性有限,仅支持MySQL 8.0及以上,旧版本(如5.6、5.7)无法使用 |
性能高效:子查询仅处理单个用户的订单,避免全表排序带来的性能损耗 | 若用户表数据量极大(如千万级),逐用户遍历的开销会增加 |
灵活性高:修改“最近N件”时,只需调整LIMIT 后的数值(如改LIMIT 5 获取最近5件) | 无法直接获取订单排名(如“第1近”“第2近”的标识) |
4. 旧版本兼容方案(MySQL 5.x)
如果使用的是MySQL 5.x等不支持LATERAL
的版本,可通过CROSS JOIN + 子查询计数
实现等价效果:
SELECT
u.username,
u.email,
p.product_name,
o.created_time,
o.amount
FROM users u
-- 交叉连接订单表和商品表,先关联基础数据
CROSS JOIN orders o
JOIN products p ON o.product_id = p.id
-- 筛选条件:确保当前订单是用户最近3条以内的
WHERE o.user_id = u.id
AND (
-- 子查询:统计当前用户中,比当前订单创建时间晚的订单数量
SELECT COUNT(*)
FROM orders
WHERE user_id = u.id
AND created_time >= o.created_time
) <= 3 -- 数量≤3,说明当前订单在“最近3条”范围内
-- 最终排序:按用户ID分组,同一用户内按订单时间倒序
ORDER BY u.id, o.created_time DESC;
三、方案二:窗口函数(灵活通用的“全表排序+筛选”)
窗口函数(如ROW_NUMBER()
)是MySQL 8.0+引入的高级功能,核心能力是在不破坏表结构的前提下,为数据添加“分组排序标识”,适合需要“先全表关联,再按组筛选Top-N”的场景,尤其在需要排名信息时优势明显。
1. 实现代码
SELECT
username,
email,
product_name,
created_time,
amount
FROM (
-- 子查询1:为每个用户的订单添加“排名标识”
SELECT
u.username,
u.email,
p.product_name,
o.created_time,
o.amount,
-- 窗口函数:按用户分组,按订单时间倒序排名
ROW_NUMBER() OVER (
PARTITION BY o.user_id -- 按用户ID分组,每个用户单独排名
ORDER BY o.created_time DESC -- 按订单创建时间倒序,最近的排第1
) AS order_rank -- order_rank:订单排名(1=最近,2=次近,以此类推)
FROM orders o
-- 关联用户表,获取用户名和邮箱
JOIN users u ON o.user_id = u.id
-- 关联商品表,获取商品名称
JOIN products p ON o.product_id = p.id
) AS ranked_orders
-- 筛选条件:只保留排名≤3的订单,即每个用户的最近3条
WHERE order_rank <= 3
-- 最终排序:按用户ID分组,同一用户内按订单时间倒序
ORDER BY u.id, created_time DESC;
2. 核心原理
- 全表关联:先将
orders
(订单表)与users
(用户表)、products
(商品表)全量关联,获取所有用户的所有订单数据; - 分组排名:通过
ROW_NUMBER()
窗口函数,按o.user_id
(用户ID)分组(PARTITION BY
),为每个用户的订单按created_time
(创建时间)倒序分配排名(order_rank
); - 筛选Top-N:外部查询通过
WHERE order_rank <= 3
,筛选出每个用户排名前3的订单,即最近3条购买记录。
3. 优缺点分析
优点 | 缺点 |
---|---|
功能灵活:可直接获取订单排名,支持复杂筛选(如“筛选排名为1或3的订单”) | 语法相对复杂,需要理解“窗口函数”“分组排序”等概念,新手入门门槛稍高 |
版本兼容性与LATERAL一致(支持MySQL 8.0+),无需额外适配 | 性能依赖索引:若订单表数据量大且无合适索引,全表排序会产生较高开销 |
适用场景广:除了Top-N筛选,还可用于计算用户订单总数、平均金额等统计需求 | 无法直接用LIMIT 控制数量,需通过排名字段(order_rank )筛选 |
4. 性能优化建议
为避免窗口函数全表排序的性能损耗,建议为orders
表添加联合索引,索引字段包含“分组字段”和“排序字段”:
-- 索引:user_id(分组字段)+ created_time(排序字段,倒序)
CREATE INDEX idx_orders_user_time ON orders(user_id, created_time DESC);
该索引能让MySQL直接按用户分组,并按时间倒序获取数据,无需额外排序,大幅提升查询效率。
四、两种方案的对比与选择建议
对比维度 | LATERAL连接方案 | 窗口函数方案 |
---|---|---|
核心逻辑 | 逐用户筛选(“先分后查”) | 全表排序后筛选(“先查后分”) |
版本要求 | MySQL 8.0+ | MySQL 8.0+ |
性能表现 | 小用户表+大订单表时更优 | 大用户表+需排名时更优 |
功能支持 | 仅支持Top-N筛选,无排名 | 支持Top-N筛选+排名,可复杂统计 |
语法复杂度 | 低(直观易懂) | 中(需理解窗口函数) |
适用场景 | 需求简单,仅需“最近N件商品”,无需排名 | 需排名信息,或有复杂筛选、统计需求 |
选择建议
-
优先选LATERAL连接:
- 你的MySQL版本是8.0+;
- 需求仅为“获取每个用户最近N条订单”,无需排名;
- 用户表数据量不大(如百万级以内),逐用户遍历开销可控。
-
优先选窗口函数:
- 需要显示订单排名(如“最近3单,第1单是XX,第2单是XX”);
- 有复杂筛选需求(如“筛选每个用户排名为奇数的订单”);
- 用户表数据量大,但已为订单表添加优化索引(如
idx_orders_user_time
)。
-
旧版本MySQL(5.x):
- 直接使用“方案一”中的兼容写法(
CROSS JOIN + 子查询计数
); - 长期来看,建议升级MySQL版本至8.0+,不仅支持
LATERAL
和窗口函数,还能获得更好的安全性和性能。
- 直接使用“方案一”中的兼容写法(
五、常见问题与注意事项
-
订单时间相同的情况: 若两个订单的
created_time
完全相同(如同一秒内下单),ROW_NUMBER()
会随机分配排名,可能导致每次查询结果不一致。解决方案是在ORDER BY
后添加“次要排序字段”,如订单ID(o.id
):-- 窗口函数中调整排序逻辑 ORDER BY o.created_time DESC, o.id DESC
这样即使时间相同,也会按订单ID倒序排名,保证结果稳定。
-
用户无订单的情况: 上述两种方案中,
JOIN
会只保留有订单记录的用户,无订单用户会被过滤。若需保留无订单用户(订单相关字段显示为NULL
),可将JOIN
改为LEFT JOIN
:-- LATERAL方案示例(窗口函数方案同理) FROM users u LEFT JOIN LATERAL (...) AS ro ON TRUE;
-
性能测试建议: 若数据量较大(如订单表千万级),建议用
EXPLAIN ANALYZE
分析执行计划,重点关注是否存在“全表扫描”“Using filesort”等低效操作,及时通过添加索引优化。
总结
查询“每个用户最近3件商品”的核心是“按用户分组后的Top-N筛选”。在MySQL 8.0+环境下,LATERAL
连接胜在直观高效,适合简单需求;窗口函数胜在灵活通用,适合需排名或复杂筛选的场景。实际应用中,需结合业务需求、数据量大小和MySQL版本,选择最适配的方案,同时通过合理索引优化性能,确保查询高效稳定。
注意:本文归作者所有,未经作者允许,不得转载