MySQL查询优化之高效获取每个分组的最新记录
引言
在数据分析 或业务查询中,我们经常需要 “按某个字段分组,并取每组中时间最新的记录”。例如:
- 按 data_time 分组,取每组 insert_time 最大的记录。
- 按用户 ID 分组,取每个用户的最新订单。
但 MySQL 的 GROUP BY 默认行为可能无法直接满足需求,尤其是当查询依赖 ORDER BY 时。本文将深入探讨 一种常见的 hack 方案(依赖 LIMIT),并分析其问题,最终给出 更高效、更标准的解决方案。
1. 常见问题:如何获取每个分组的最新记录?
假设我们有一个表 spider.nbs_data,结构如下:
CREATE TABLE spider.nbs_data (
id BIGINT PRIMARY KEY,
index_id VARCHAR(20),
data_time DATE,
insert_time DATETIME,
-- 其他字段...
);
需求:查询 index_id = '0000120460' 且 data_time >= '2020-12-31' 的数据,按 data_time 分组,并取每组 insert_time 最大的记录。
2. 一种常见的 hack 方案:依赖 LIMIT 的 GROUP BY
2.1 原始 SQL
SELECT * FROM (
SELECT *
FROM spider.nbs_data
WHERE index_id = '0000120460' AND data_time >= '2020-12-31'
ORDER BY `insert_time` DESC
LIMIT 10000000000 -- 超大 LIMIT 值
) t
GROUP BY t.data_time -- 按 data_time 分组
ORDER BY `data_time`;
2.2 为什么它能工作?
内层查询:
- 先筛选符合条件的数据。
- 按 insert_time DESC 排序,确保最新记录排在前面。
- LIMIT 10000000000 强制 MySQL 先执行 ORDER BY 再 GROUP BY,避免优化器打乱顺序。
外层 GROUP BY:
- MySQL 默认取每组的第一条记录(即 insert_time 最大的那条)。
- 最终按 data_time 排序输出。
2.3 问题与风险
依赖 MySQL 的 GROUP BY 行为:
- 如果 SELECT * 包含非分组列,可能触发 ONLY_FULL_GROUP_BY 错误(MySQL 5.7+ 默认启用)。
- 不同 MySQL 版本或 SQL 模式可能导致结果不一致。
性能问题:
- LIMIT 10000000000 相当于 全表扫描,数据量大时性能极差。
- 如果数据量超过 LIMIT 值,可能丢失数据。
代码可读性差:
LIMIT 10000000000 是一个 hack 行为,容易让后续维护者困惑。
3. 更优方案:标准 SQL 实现
方案 1:JOIN + 子查询(兼容所有 MySQL 版本)
SELECT t1.*
FROM spider.nbs_data t1
JOIN (
SELECT data_time, MAX(insert_time) AS max_insert_time
FROM spider.nbs_data
WHERE index_id = '0000120460' AND data_time >= '2020-12-31'
GROUP BY data_time
) t2 ON t1.data_time = t2.data_time AND t1.insert_time = t2.max_insert_time
WHERE t1.index_id = '0000120460'
ORDER BY t1.data_time;
优点:
- 清晰、标准,不依赖 GROUP BY 行为。
- 性能较好(子查询先聚合,再 JOIN)。
- 兼容所有 MySQL 版本。
方案 2:ROW_NUMBER()(MySQL 8.0+)
WITH ranked_data AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY data_time ORDER BY insert_time DESC) AS rn
FROM spider.nbs_data
WHERE index_id = '0000120460' AND data_time >= '2020-12-31'
)
SELECT * FROM ranked_data WHERE rn = 1 ORDER BY data_time;
优点:
- 语法简洁,逻辑清晰。
- 性能优秀(窗口函数优化较好)。
- 适用于复杂分组排序场景。
缺点:
仅 MySQL 8.0+ 支持。
4. 性能对比
| 方案 | 适用场景 | 性能 | 兼容性 |
|---|---|---|---|
LIMIT hack | 快速验证、临时查询 | 差(全表扫描) | 所有版本 |
JOIN + 子查询 | 所有 MySQL 版本 | 优 | 所有版本 |
ROW_NUMBER() | MySQL 8.0+ | 优 | 8.0+ |
5. 总结
- 避免依赖
LIMIT的 hack 方案:虽然能工作,但性能差、可读性低、存在风险。 - 推荐
JOIN+ 子查询:兼容性好,性能稳定,适合大多数场景。 - MySQL 8.0+ 优先用
ROW_NUMBER():语法简洁,性能更优。
最终推荐 SQL:
-- MySQL 5.7 及以下版本
SELECT t1.*
FROM spider.nbs_data t1
JOIN (
SELECT data_time, MAX(insert_time) AS max_insert_time
FROM spider.nbs_data
WHERE index_id = '0000120460' AND data_time >= '2020-12-31'
GROUP BY data_time
) t2 ON t1.data_time = t2.data_time AND t1.insert_time = t2.max_insert_time
WHERE t1.index_id = '0000120460'
ORDER BY t1.data_time;
-- MySQL 8.0+
WITH ranked_data AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY data_time ORDER BY insert_time DESC) AS rn
FROM spider.nbs_data
WHERE index_id = '0000120460' AND data_time >= '2020-12-31'
)
SELECT * FROM ranked_data WHERE rn = 1 ORDER BY data_time;
结语
在 SQL 查询中,清晰、标准、高效 是最重要的原则。依赖 LIMIT 的 hack 方案虽然能解决一时之需,但长期来看,使用 JOIN 或窗口函数才是更可靠的选择。希望本文能帮助你优化查询,写出更健壮的 SQL
到此这篇关于MySQL查询优化之高效获取每个分组的最新记录的文章就介绍到这了,更多相关MySQL查询优化内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解MySQL中DELETE NOT IN删除的常见问题与解决方案
在数据库操作中,DELETE 语句用于从表中删除数据,当需要根据某些条件进行删除时,NOT IN 子句是一个常用的条件表达式,本文将探讨如何在 MySQL 中使用 DELETE ... NOT IN 语句,并讨论一些常见的问题及其解决方案,需要的可以参考下2025-10-10


最新评论