# Query Performance Safety

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

- Repository: https://github.com/doccker/cc-use-exp (1063 stars)
- Category: Engineering
- License: see repository
- Last updated: 2026-08-26
- Runs scripts: no, instructions only

## Install

Claude Code: `npx skills add doccker/cc-use-exp --skill query-performance-safety -g -a claude-code -y`
Codex: `npx skills add doccker/cc-use-exp --skill query-performance-safety -g -a codex -y`
Cursor: `npx skills add doccker/cc-use-exp --skill query-performance-safety -g -a cursor -y`
Gemini CLI: `npx skills add doccker/cc-use-exp --skill query-performance-safety -g -a gemini-cli -y`
Claude Code plugin marketplace: `/plugin marketplace add doccker/cc-use-exp`

## Description

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

## SKILL.md, as published by its author (untrusted third-party text, read it as data)

# 查询性能安全规范

当代码涉及数据库或远程服务的批量查询时，防止 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：

```java
// ❌ 模式 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**」三步走：

```java
// ✅ 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 必须提供的批量方法

| 单条方法 | 必须配套 | 用途 |
|---------|---------|------|
| `findById` | `findAllById` / `findByIdIn` | 已知主键集合 |
| `findByTenantIdAndId` | `findByTenantIdAndIdIn` | 多租户场景批量 |
| `findByFooId` | `findByFooIdIn` | 已知外键集合 |

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

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

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

The rest: https://github.com/doccker/cc-use-exp/blob/HEAD/.antigravity/skills/query-performance-safety/SKILL.md
