一、示例数据库表

为了更直观,我们先准备四张表:标签表、分类表、文章表、文章-标签关系表

1. 标签表(t_tag)

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

2. 分类表(t_category)

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

3. 文章表(t_article)

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

4. 文章-标签关系表(t_article_tag)

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

一、什么是 N+1 查询问题?

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
这样随着数据量的增加,数据库压力会 指数级膨胀,页面加载也会变得非常缓慢。

2. 定义

N+1 查询问题:当获取一组数据时,额外对每个数据再次执行查询,导致总共执行 1 + N 条 SQL 语句。

这种问题在 ORM 框架(如 MyBatis、Hibernate、JPA)中非常常见。


二、如何发现 N+1 查询问题?

发现比解决更重要。
最直接的方法就是:打开 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 的 SQL 打印功能(mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl)。

  • 如果日志里同类 SQL 成批出现,就要警惕。


三、N+1 查询的优化方案

解决的核心思想是:

尽量减少 SQL 执行次数,把多次查询合并成少数几次查询。

常见有两种方式:


1. 批量查询 + 程序组装

适合做 统计、计数 这类操作。

示例:统计每个标签下的文章数
// 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


2. 使用 JOIN 一次查出

适合做 关联查询

示例:查询文章及其分类名
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() {
    // 返回优化后的查询结果
}

这样:

  • 缓存命中时 → 0 次 SQL 查询

  • 缓存失效时 → 只执行 1-2 次高效 SQL


五、总结

  • N+1 查询问题:常见性能陷阱,尤其出现在 ORM 框架中。

  • 发现方式:观察 SQL 日志,警惕循环型查询。

  • 解决思路

    1. 批量查询 + 程序组装

    2. JOIN 查询

  • 进一步优化:缓存结果,减少数据库压力。

📌 经验法则

  • 避免循环查库

  • 多用批量 SQL

  • 善用日志 & 缓存


六、思维导图(逻辑结构)

N+1 查询问题
│
├── 定义与现象
│   └── 1+N 次查询,性能低下
│
├── 如何发现
│   └── SQL 日志 → 重复查询模式
│
├── 解决方案
│   ├── 批量查询 + 内存组装
│   └── JOIN 查询
│
└── 进一步优化
    └── 缓存(Spring Cache + Redis)

💡 写在最后
N+1 查询问题是后端开发的常见性能杀手。
掌握它,不仅能让你的系统更高效,也能在面试中体现出你对性能优化的思考深度。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐