Skills

Query Performance Safety

当代码涉及循环内查询、批量 ID 查询、IN 子句、BFS/递归遍历、嵌套 service 调用时触发。防止 N+1 查询、IN 子句过长、递归内存炸裂等性能陷阱。

Install

npx skills add doccker/cc-use-exp --skill query-performance-safety -g -a claude-code -y
Repository
doccker/cc-use-exp1.1k
Category
Engineering
Updated
2mo ago
License
See repository
Context cost
Mediumabout 3,000 tokens
Runs scripts
No, instructions only

What it does

当代码涉及循环内查询、批量 ID 查询、IN 子句、BFS/递归遍历、嵌套 service 调用时触发。防止 N+1 查询、IN 子句过长、递归内存炸裂等性能陷阱。

查询性能安全规范

当代码涉及数据库或远程服务的批量查询时,防止 N+1、IN 子句过长、递归无界等高频性能陷阱。

与 multi-tenant-safety 配合:本 skill 关注「查询效率」,租户隔离遵循后者。


陷阱 #1: 循环内调用单条查询 → N+1

场景: for / stream / forEach 内部直接调用 repo.findById(...) 或同等单条远程调用

问题根因

每次循环触发一次 SQL/HTTP,N 个元素就是 N 次往返。100 元素 = 100 次 SQL,1000 元素就是 1000 次。本地开发数据少看不出来,生产环境直接拖垮接口。

嗅探信号

只要在 service / handler 方法里看到以下任一模式,立即怀疑 N+1:

// ❌ 模式 1:显式 for
for (Order o : orders) {
    User u = userRepository.findById(o.getUserId()).orElse(null);
}

// ❌ 模式 2:stream 链
orders.stream()
      .map(o -> userRepository.findById(o.getUserId()))
      .collect(...);

// ❌ 模式 3:辅助方法被循环调用
for (Item i : items) {
    enrichItem(i);  // 里面又 findById 一次
}

// ❌ 模式 4:嵌套调用
List<X> list = repoA.findByY(y);
for (X x : list) {
    x.setZ(repoB.findById(x.getZId()).orElse(null));
}

正确做法

「先批量拿 → 落到 Map → 循环里查 Map」三步走:

// ✅ 1. 收集所有外键 ID
List<Long> userIds = orders.stream()
        .map(Order::getUserId)
        .filter(Objects::nonNull)
        .distinct()
        .toList();

// ✅ 2. 一次 IN 查询,落到 Map
Map<Long, User> userMap = userRepository
        .findByTenantIdAndIdIn(tenantId, userIds)
        .stream()
        .collect(Collectors.toMap(User::getId, u -> u));

// ✅ 3. 循环内只查 Map,零额外 SQL
for (Order o : orders) {
    User u = userMap.get(o.getUserId());
}

Repository 必须提供的批量方法

单条方法必须配套用途
findByIdfindAllById / findByIdIn已知主键集合
findByTenantIdAndIdfindByTenantIdAndIdIn多租户场景批量
findByFooIdfindByFooIdIn已知外键集合

新增带 IN 的方法时必须和单条方法成对出现,避免上层不得不写 N+1。

隐式 N+1:循环内调 DTO 转换函数

最坑的一种 N+1, , convertToDTO() / toResponseDto() / enrichEntity() 这类辅助方法看起来"一个函数搞定一切",但内部可能藏着 4-5 次 findById,循环里调一次就是 N×K 次 SQL。

Read the whole SKILL.md on GitHub

More in Engineering