/query-performance-safety
当代码涉及循环内查询、批量 ID 查询、IN 子句、BFS/递归遍历、嵌套 service 调用时触发。防止 N+1 查询、IN 子句过长、递归内存炸裂等性能陷阱。
$ npx -y skills add doccker/cc-use-exp --skill query-performance-safety --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/query-performance-safety
Context preview
The summary Claude sees to decide when to auto-load this skill.
当代码涉及循环内查询、批量 ID 查询、IN 子句、BFS/递归遍历、嵌套 service 调用时触发。防止 N+1 查询、IN 子句过长、递归内存炸裂等性能陷阱。
SKILL.md
query-performance-safety.SKILL.mdname: query-performance-safety
description: 当代码涉及循环内查询、批量 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 必须提供的批量方法
| 单条方法 | 必须配套 | 用途 | |---------|---------|------| | `findById` | `findAllById` / `findByIdIn` | 已知主键集合 | | `findByTenantIdAndId` | `findByTenantIdAndIdIn` | 多租户场景批量 | | `findByFooId` | `findByFooIdIn` | 已知外键集合 |
新增带 IN 的方法时**必须**和单条方法成对出现,避免上层不得不写 N+1。
隐式 N+1:循环内调 DTO 转换函数
最坑的一种 N+1 ——`convertToDTO()` / `toResponseDto()` / `enrichEntity()` 这类辅助方法看起来"一个函数搞定一切",但内部可能藏着 4-5 次 `findById`,循环里调一次就是 N×K 次 SQL。
// ❌ 看起来无害,实际触发 24000 次 SQL(6000 商品 × 4 次内部查询)
public Map<String, List<ProductDTO>> findDuplicates() {
List<Product> products = productRepository.findByTenantId(tenantId);
Map<String, List<ProductDTO>> groups = new HashMap<>();
for (Product p : products) {
String key = normalize(p.getProductName() + p.getSpecification());
groups.computeIfAbsent(key, k -> new ArrayList<>())
.add(convertToDTO(p)); // ❌ 内部每次都查 category/supplier/image/price
}
return groups;
}
private ProductDTO convertToDTO(Product p) {
return ProductDTO.builder()
.categoryName(categoryRepository.findById(p.getCategoryId())...) // +1
.supplier(productSupplierRepository.findByProductId(p.getId())...) // +1
.imageUrl(productImageRepository.findByProductId(p.getId())...) // +1
.priceTiers(priceTierRepository.findByProductId(p.getId())...) // +1
.build();
}修复策略
**策略 A:两阶段处理**(推荐,适合"先筛选再展示")
// ✅ 阶段 1:用 entity 做筛选/分组,零额外查询
Map<String, List<Product>> grouped = new HashMap<>();
for (Product p : products) {
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(p); // entity,不转 DTO
}
// 只对真正要展示的(如重复组 ≥ 2)做后续处理
grouped.entrySet().removeIf(e -> e.getValue().size() < 2);
// 阶段 2:批量预加载关联数据
Set<Long> categoryIds = grouped.values().stream()
.flatMap(List::stream)
.map(Product::getCategoryId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, String> categoryNameMap = categoryRepository.findAllById(categoryIds).stream()
.collect(Collectors.toMap(ProductCategory::getId, ProductCategory::getCategoryName));
// 阶段 3:用 Map 转 DTO,零额外查询
Map<String, List<ProductDTO>> result = new HashMap<>();
grouped.forEach((k, list) -> {
result.put(k, list.stream()
.map(p -> toLightweightDTO(p, categoryNameMap))
.toList());
});**策略 B:场景化轻量 DTO 转换器**(推荐,结合策略 A)
不同场景用不同 DTO 转换函数。完整版给详情接口,轻量版给列表/查重接口:
// ✅ 列表/查重:只填实际需要的字段
private ProductDTO toLightweightDTO(Product p, Map<Long, String> categoryNameMap) {
return ProductDTO.builder()
.id(p.getId())
.skuCode(p.getSkuCode())
.productName(p.getProductName())
.specification(p.getSpecification())
.categoryId(p.getCategoryId())
.categoryName(p.getCategoryId() == null ? null : categoryNameMap.get(p.getCategoryId()))
.status(p.getStatus())
.build();
// 不填 supplier/image/priceTiers,列表场景用不到
}
// 详情:用完整 convertToDTO
public ProductDTO getDetail(Long id) {
return convertToDTO(productRepository.findById(id).orElseThrow());
}**策略 C:批量版 convertToDTO**(适合必须返回完整 DTO 的列表接口)
public List<ProductDTO> convertToDTOs(List<Product> products) {
// 一次性预加载全部关联
Set<Long> categoryIds = products.stream().map(Product::getCategoryId).filter(Objects::nonNull).collect(Collectors.toSet());
Set<Long> productIds = products.stream().map(Product::getId).collect(Collectors.toSet());
Map<Long, ProductCategory> categoryMap = categoryRepository.findAllById(categoryIds).stream()
.collect(Collectors.toMap(ProductCategory::getId, c -> c));
Map<Long, List<ProductImage>> imageMap = productImageRepository.findByProductIdIn(productIds).stream()
.collect(Collectors.groupingBy(ProductImage::getProductId));
// ... 其他关联
// 转换时只查 Map
return products.stream()
.map(p -> toDTOWithPreloaded(p, categoryMap, imageMap, ...))
.toList();
}嗅探信号
代码评审时只要看到以下模式立即怀疑:
- 任意循环(for/stream/forEach)里调用 `convertToDTO(...)` / `toResponseDto(...)` / `to...DTO(...)` / `enrich...(...)`
- 看似"一行搞定"的辅助方法,其实跨了多个 Repository
- 接口响应时间随数据量线性增长,但单条数据看不到明显慢点
- 数据库 SQL 数 = 列表数 × 某个常数
检查清单(陷阱 #1 全部场景)
- [ ] service 方法里搜 `findById(` / `getOne(` / `getReferenceById(` / `findOne(`,是否在循环或 stream 链中
- [ ] 每个 list-aware 方法的 SQL 次数与列表大小是否解耦(理想是 O(1) 或 O(log N) 而非 O(N))
- [ ] enrichment 辅助方法是否被循环调用(隐式 N+1)
- [ ] 跨 service 调用(A.getDetail() → B.findX())是否在循环里
- [ ] 查重 / 列表 / 统计接口:是否在循环里调用 DTO 转换
- [ ] DTO 转换函数内部 `findXxx` 调用次数 × 列表大小是否能接受
- [ ] 是否区分了"列表轻量 DTO
Read more
name: query-performance-safety description: 当代码涉及循环内查询、批量 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 必须提供的批量方法
| 单条方法 | 必须配套 | 用途 | |---------|---------|------| | `findById` | `findAllById` / `findByIdIn` | 已知主键集合 | | `findByTenantIdAndId` | `findByTenantIdAndIdIn` | 多租户场景批量 | | `findByFooId` | `findByFooIdIn` | 已知外键集合 |
新增带 IN 的方法时**必须**和单条方法成对出现,避免上层不得不写 N+1。
隐式 N+1:循环内调 DTO 转换函数
最坑的一种 N+1 ——`convertToDTO()` / `toResponseDto()` / `enrichEntity()` 这类辅助方法看起来"一个函数搞定一切",但内部可能藏着 4-5 次 `findById`,循环里调一次就是 N×K 次 SQL。
// ❌ 看起来无害,实际触发 24000 次 SQL(6000 商品 × 4 次内部查询)
public Map<String, List<ProductDTO>> findDuplicates() {
List<Product> products = productRepository.findByTenantId(tenantId);
Map<String, List<ProductDTO>> groups = new HashMap<>();
for (Product p : products) {
String key = normalize(p.getProductName() + p.getSpecification());
groups.computeIfAbsent(key, k -> new ArrayList<>())
.add(convertToDTO(p)); // ❌ 内部每次都查 category/supplier/image/price
}
return groups;
}
private ProductDTO convertToDTO(Product p) {
return ProductDTO.builder()
.categoryName(categoryRepository.findById(p.getCategoryId())...) // +1
.supplier(productSupplierRepository.findByProductId(p.getId())...) // +1
.imageUrl(productImageRepository.findByProductId(p.getId())...) // +1
.priceTiers(priceTierRepository.findByProductId(p.getId())...) // +1
.build();
}修复策略
**策略 A:两阶段处理**(推荐,适合"先筛选再展示")
// ✅ 阶段 1:用 entity 做筛选/分组,零额外查询
Map<String, List<Product>> grouped = new HashMap<>();
for (Product p : products) {
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(p); // entity,不转 DTO
}
// 只对真正要展示的(如重复组 ≥ 2)做后续处理
grouped.entrySet().removeIf(e -> e.getValue().size() < 2);
// 阶段 2:批量预加载关联数据
Set<Long> categoryIds = grouped.values().stream()
.flatMap(List::stream)
.map(Product::getCategoryId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, String> categoryNameMap = categoryRepository.findAllById(categoryIds).stream()
.collect(Collectors.toMap(ProductCategory::getId, ProductCategory::getCategoryName));
// 阶段 3:用 Map 转 DTO,零额外查询
Map<String, List<ProductDTO>> result = new HashMap<>();
grouped.forEach((k, list) -> {
result.put(k, list.stream()
.map(p -> toLightweightDTO(p, categoryNameMap))
.toList());
});**策略 B:场景化轻量 DTO 转换器**(推荐,结合策略 A)
不同场景用不同 DTO 转换函数。完整版给详情接口,轻量版给列表/查重接口:
// ✅ 列表/查重:只填实际需要的字段
private ProductDTO toLightweightDTO(Product p, Map<Long, String> categoryNameMap) {
return ProductDTO.builder()
.id(p.getId())
.skuCode(p.getSkuCode())
.productName(p.getProductName())
.specification(p.getSpecification())
.categoryId(p.getCategoryId())
.categoryName(p.getCategoryId() == null ? null : categoryNameMap.get(p.getCategoryId()))
.status(p.getStatus())
.build();
// 不填 supplier/image/priceTiers,列表场景用不到
}
// 详情:用完整 convertToDTO
public ProductDTO getDetail(Long id) {
return convertToDTO(productRepository.findById(id).orElseThrow());
}**策略 C:批量版 convertToDTO**(适合必须返回完整 DTO 的列表接口)
public List<ProductDTO> convertToDTOs(List<Product> products) {
// 一次性预加载全部关联
Set<Long> categoryIds = products.stream().map(Product::getCategoryId).filter(Objects::nonNull).collect(Collectors.toSet());
Set<Long> productIds = products.stream().map(Product::getId).collect(Collectors.toSet());
Map<Long, ProductCategory> categoryMap = categoryRepository.findAllById(categoryIds).stream()
.collect(Collectors.toMap(ProductCategory::getId, c -> c));
Map<Long, List<ProductImage>> imageMap = productImageRepository.findByProductIdIn(productIds).stream()
.collect(Collectors.groupingBy(ProductImage::getProductId));
// ... 其他关联
// 转换时只查 Map
return products.stream()
.map(p -> toDTOWithPreloaded(p, categoryMap, imageMap, ...))
.toList();
}嗅探信号
代码评审时只要看到以下模式立即怀疑:
- 任意循环(for/stream/forEach)里调用 `convertToDTO(...)` / `toResponseDto(...)` / `to...DTO(...)` / `enrich...(...)`
- 看似"一行搞定"的辅助方法,其实跨了多个 Repository
- 接口响应时间随数据量线性增长,但单条数据看不到明显慢点
- 数据库 SQL 数 = 列表数 × 某个常数
检查清单(陷阱 #1 全部场景)
- [ ] service 方法里搜 `findById(` / `getOne(` / `getReferenceById(` / `findOne(`,是否在循环或 stream 链中
- [ ] 每个 list-aware 方法的 SQL 次数与列表大小是否解耦(理想是 O(1) 或 O(log N) 而非 O(N))
- [ ] enrichment 辅助方法是否被循环调用(隐式 N+1)
- [ ] 跨 service 调用(A.getDetail() → B.findX())是否在循环里
- [ ] 查重 / 列表 / 统计接口:是否在循环里调用 DTO 转换
- [ ] DTO 转换函数内部 `findXxx` 调用次数 × 列表大小是否能接受
- [ ] 是否区分了"列表轻量 DTO
保留你熟悉的 CLI/IDE,让 Claude Code、Gemini CLI、Codex、Cursor、GitHub Copilot 开箱即用 按费力度从低到高,用最少操作获得最大帮助 不是提示词集合,而是一套可维护的 AI 协作配置系统。
Repo: doccker/cc-use-exp
Other skills on cc-use-exp.
- /api-design-safety
当设计或修改 REST API 响应结构、处理 API 返回值,或生成 Excel/CSV/PDF/对账文件等下游产物时触发。防止 API 设计缺陷导致的字段错位、类型歧义,以及生成产物时关键字段缺失但静默成功的问题。
Open skill - /api-proxy-safety
网关/代理/WAF/CDN 中间件的安全关键词匹配实现规范,防止纯子串匹配误判正常响应内容中的技术术语(如 Cloudflare、502、error)
Open skill - /async-task-pattern
当 API/任务可能执行超过 10 秒(批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合)时触发。防止同步接口被网关 30s 超时切断、用户重复点击触发并发、状态缓存内存泄漏等问题。提供异步任务状态机标准模板。
Open skill - /bash-style
当用户操作 .sh、Dockerfile、Makefile、.yml、.yaml 文件,或在 Markdown 中编写 bash 代码块时触发。提供 Bash 编写规范。
Open skill - /code-quality-principles
当编写新模块、设计接口、重构代码或代码审查时触发。提供经典模块化六原则检查清单(大小适中/调用深度/扇入扇出/边界清晰/作用域内聚/可预测性),适用于 PR/Review/新模块设计场景。
Open skill - /external-system-debugging
涉及浏览器、编辑器、CDN/WAF、IM 平台、操作系统剪贴板、第三方 SaaS 等"外部黑盒系统"的代码编写或 bug 调试时触发。强制先抓真实环境数据再推理,避免连续 2 轮"凭代码推理"的修复 no-op。关键词:粘贴/复制异常、跨平台显示不一致、第三方 API 怪结果、CDN/WAF 拦截、本地复现失败、HTML→MD 转换丢属性。
Open skill

