
为了更直观,我们先准备四张表:标签表、分类表、文章表、文章-标签关系表。
id | tag_name | create_time |
|---|---|---|
1 | Java | 2025-08-01 10:00:00 |
2 | Python | 2025-08-01 10:00:00 |
3 | C++ | 2025-08-01 10:00:00 |
4 | Spring | 2025-08-01 10:00:00 |
5 | MyBatis | 2025-08-01 10:00:00 |
id | category_name | create_time |
|---|---|---|
1 | 后端开发 | 2025-08-01 10:00:00 |
2 | 数据分析 | 2025-08-01 10:00:00 |
3 | 系统编程 | 2025-08-01 10:00:00 |
id | title | content | category_id | create_time |
|---|---|---|---|---|
1 | Java 基础教程 | ... | 1 | 2025-08-01 10:05:00 |
2 | Python 爬虫实战 | ... | 2 | 2025-08-01 10:06:00 |
3 | C++ STL 详解 | ... | 3 | 2025-08-01 10:07:00 |
4 | Spring 入门指南 | ... | 1 | 2025-08-01 10:08:00 |
5 | MyBatis 使用技巧 | ... | 1 | 2025-08-01 10:09:00 |
id | article_id | tag_id |
|---|---|---|
1 | 1 | 1 |
2 | 2 | 2 |
3 | 3 | 3 |
4 | 4 | 4 |
5 | 4 | 1 |
6 | 5 | 5 |
7 | 5 | 1 |
假设我们要获取所有 标签,并统计每个标签下的 文章数量。最直观的写法可能是:
-- 第 1 次查询:获取所有标签
SELECT * FROM t_tag;
-- 接下来每个标签再执行一次查询
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 1;
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 2;
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 3;
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 4;
...如果有 100 个标签,就会执行 1 + 100 = 101 条 SQL。 这样随着数据量的增加,数据库压力会 指数级膨胀,页面加载也会变得非常缓慢。
N+1 查询问题:当获取一组数据时,额外对每个数据再次执行查询,导致总共执行 1 + N 条 SQL 语句。
这种问题在 ORM 框架(如 MyBatis、Hibernate、JPA)中非常常见。
发现比解决更重要。 最直接的方法就是:打开 SQL 日志 或 查看 后端应用程序 日志。
当你访问一个列表页面时,如果日志中出现了以下模式:
SELECT * FROM t_tag;
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 1;
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 2;
SELECT COUNT(*) FROM t_article_tag WHERE tag_id = 3;
...恭喜你,十有八九遇到的就是 N+1 查询问题。
📌 小技巧
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl)。
解决的核心思想是:
尽量减少 SQL 执行次数,把多次查询合并成少数几次查询。
常见有两种方式:
适合做 统计、计数 这类操作。
// 1. 查询所有标签
List<Tag> tags = tagMapper.selectList(...);
// 2. 一次性批量查询所有标签的文章数量
@Select("SELECT tag_id, COUNT(article_id) AS article_count
FROM t_article_tag GROUP BY tag_id")
@MapKey("tag_id")
Map<Long, Long> selectArticleCountByTagId();
// 3. 内存组装,避免循环查库
Map<Long, Long> articleCountMap = selectArticleCountByTagId();
List<TagVO> result = tags.stream().map(tag -> {
TagVO vo = new TagVO();
vo.setId(tag.getId());
vo.setName(tag.getTagName()); // 注意这里是 tagName
vo.setArticleCount(articleCountMap.getOrDefault(tag.getId(), 0L));
return vo;
}).toList(); // toList() 返回一个 List<TagVO>
//查询结果
[
{ "id": 1, "name": "Java", "articleCount": 2 },
{ "id": 2, "name": "Spring", "articleCount": 1 },
{ "id": 3, "name": "MySQL", "articleCount": 1 },
{ "id": 4, "name": "Redis", "articleCount": 1 },
{ "id": 5, "name": "消息队列", "articleCount": 1 }
]📊 优化效果: 无论有多少个标签,最终都只需要 2 条 SQL。
适合做 关联查询。
SELECT
a.*,
c.category_name
FROM
t_article a
LEFT JOIN
t_category c ON a.category_id = c.id;这样,数据库直接返回文章和分类信息,避免了 “先查文章再查分类” 的 N+1 查询。
SQL 优化解决了 N+1 问题后,还可以引入缓存进一步加速。
Spring Cache + Redis 是常见选择:
@Cacheable(cacheNames = "tag", key = "'list'")
public List<TagVO> listAllTag() {
// 返回优化后的查询结果
}这样:
📌 经验法则:
N+1 查询问题
│
├── 定义与现象
│ └── 1+N 次查询,性能低下
│
├── 如何发现
│ └── SQL 日志 → 重复查询模式
│
├── 解决方案
│ ├── 批量查询 + 内存组装
│ └── JOIN 查询
│
└── 进一步优化
└── 缓存(Spring Cache + Redis)💡 写在最后: N+1 查询问题是后端开发的常见性能杀手。 掌握它,不仅能让你的系统更高效,也能在面试中体现出你对性能优化的思考深度。