用 Claude Code 实战 SQL 优化:从慢查询到上线检查
用 Claude Code 做 SQL 优化:慢查询、EXPLAIN、索引、N+1、分页、聚合、测试与上线清单。
SQL 优化到底优化什么
SQL 优化,是在不改变结果的前提下,调整查询、索引和数据访问方式,让数据库用更少的读取、更短的等待时间返回同样的数据。它不是把 SQL 写得更炫,而是把接口、ORM 调用、执行计划、真实数据量、回归测试和发布监控一起看。
Claude Code 的优势在于它可以同时阅读路由处理器、Prisma 或其他 ORM 模型、迁移文件、慢查询日志和测试。实际项目里,慢接口通常不是由一条“神秘 SQL”造成的,而是多个普通问题叠加:列表页 SELECT *、N+1 查询、很深的 OFFSET 分页、不匹配的复合索引,以及每次请求都同步计算的仪表盘汇总。
本文以 PostgreSQL 为主,兼顾 MySQL。阅读执行计划时建议同时查看官方文档:PostgreSQL Using EXPLAIN、PostgreSQL EXPLAIN、PostgreSQL Indexes、PostgreSQL Multicolumn Indexes、MySQL 8.4 Using EXPLAIN。
如果需要从设计层面补强,可以继续看数据库设计、数据库迁移、Prisma ORM 和性能优化。
先把使用场景分清楚
先定义场景,Claude Code 才不会给出泛泛的索引建议。
| 使用场景 | 常见症状 | 主要改法 | 风险 |
|---|---|---|---|
| 后台管理列表 | 搜索慢,翻到后面更慢 | 复合索引、键集分页、减少返回列 | 可选筛选太多会导致索引膨胀 |
| SaaS 或电商用户页 | 流量上来后列表接口变慢 | 匹配 WHERE / ORDER BY 的索引、缓存、异步汇总 | 不能遗漏租户、库存、权限条件 |
| 数据看板 | 月度或日度图表每次都扫大表 | 汇总表、物化视图、定时刷新 | 需要先约定数据新鲜度 |
建议先设置查询预算,例如列表接口 p95 小于 300ms、一次请求 SQL 不超过 5 条、返回 JSON 结构保持兼容。
给 Claude Code 的提示:
请优化这个列表 API 的 SQL 性能。
目标: p95 小于 300ms,SQL 不超过 5 条,返回 JSON 不变。
查看: src/routes/admin/orders.ts, prisma/schema.prisma, migrations, slow-query.log。
任务: 找出慢 SQL,阅读 EXPLAIN ANALYZE,提出索引方案,消除 N+1,补充回归测试。
限制: 保持改动范围小,并保证生产兼容。
步骤 1:抓到真正的慢查询
不要凭印象建索引。先捕获真正慢的 SQL。PostgreSQL 可以使用 pg_stat_statements 或数据库日志;应用侧的 ORM 日志也很有用,因为它能把 SQL 和具体接口关联起来。
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT
query,
calls,
ROUND(total_exec_time::numeric, 2) AS total_ms,
ROUND(mean_exec_time::numeric, 2) AS mean_ms,
rows
FROM pg_stat_statements
WHERE query NOT ILIKE '%pg_stat_statements%'
ORDER BY total_exec_time DESC
LIMIT 10;
Prisma 开发环境可以用真实的 Prisma Client API 输出慢查询:
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient({
log: [
{ emit: "event", level: "query" },
{ emit: "stdout", level: "error" },
{ emit: "stdout", level: "warn" },
],
});
prisma.$on("query", (event) => {
if (event.duration > 100) {
console.log({
durationMs: event.duration,
query: event.query,
params: event.params,
});
}
});
把脱敏后的日志交给 Claude Code,让它按页面、调用位置、平均耗时、调用次数和疑似原因分组。不要直接粘贴邮箱、令牌、客户编号等敏感信息。
步骤 2:阅读 EXPLAIN 和 EXPLAIN ANALYZE
找到候选 SQL 后,先用 EXPLAIN 看计划;在安全且数据量接近生产的环境中,再用 EXPLAIN (ANALYZE, BUFFERS)。注意 ANALYZE 会真正执行 SQL,不要随便用于有破坏性的 UPDATE 或 DELETE。
EXPLAIN (ANALYZE, BUFFERS)
SELECT
o.id,
o.created_at,
o.status,
c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
ORDER BY o.created_at DESC, o.id DESC
LIMIT 50;
不要看到 Seq Scan 就认为一定有问题。小表或低选择性条件下,全表扫描可能是正确选择。更应该看估算行数和实际行数是否差很多、是否有大排序、Buffers 读取是否过高、JOIN 顺序是否异常。
给 Claude Code 的提示:
请审查这份 EXPLAIN ANALYZE,按瓶颈、行数估算偏差、索引候选、安全的 SQL 改写建议分组。不要改变结果集。
步骤 3:设计复合索引
索引会加快读取,但也会增加写入成本、存储空间和迁移风险。设计顺序要来自真实查询:等值条件、范围条件、排序列、JOIN 需求。
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_status_created_id
ON orders (status, created_at DESC, id DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_customer_created
ON orders (customer_id, created_at DESC);
PostgreSQL 的 CREATE INDEX CONCURRENTLY 可以减少写入阻塞,但不能放在普通事务块中执行。不同迁移工具处理方式不同,PR 里必须写清发布方式。
在让 Claude Code 建议索引前,先提供现有索引,避免重复:
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'orders'
ORDER BY indexname;
步骤 4:消除 N+1 查询
N+1 是先查列表,再对每一行单独查关联数据。它不只增加数据库工作量,也会增加网络等待。
// 坏例子: 每个订单单独查询客户
const orders = await prisma.order.findMany({
take: 100,
orderBy: { createdAt: "desc" },
});
const rows = [];
for (const order of orders) {
const customer = await prisma.customer.findUnique({
where: { id: order.customerId },
});
rows.push({ ...order, customerName: customer?.name ?? "" });
}
// 改进: 一次取回 UI 需要的列
const orders = await prisma.order.findMany({
take: 100,
orderBy: { createdAt: "desc" },
select: {
id: true,
createdAt: true,
status: true,
totalAmount: true,
customer: {
select: {
id: true,
name: true,
},
},
},
});
提示 Claude Code 保持响应结构兼容,同时减少 SQL 条数。这样它会去读 UI 或序列化代码,而不是盲目加 include: true。
步骤 5:把深 OFFSET 换成键集分页
OFFSET 10000 LIMIT 50 通常会越来越慢,因为数据库仍然要走过被跳过的行。时间线、订单列表、无限滚动更适合用最后一条记录的 created_at 和 id 做游标。
SELECT id, created_at, status, total_amount
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC, id DESC
LIMIT 50;
SELECT id, created_at, status, total_amount
FROM orders
WHERE status = 'paid'
AND (created_at, id) < (TIMESTAMP '2026-05-01 10:30:00', 98765)
ORDER BY created_at DESC, id DESC
LIMIT 50;
(status, created_at DESC, id DESC) 索引正好支持这个访问模式。加入 id 是为了让相同时间戳的行也有稳定顺序。
步骤 6:把聚合从热请求中移出去
看板慢,往往是因为每次请求都重新计算同一批统计。如果业务可以接受延迟,就使用汇总表或定时刷新。
CREATE TABLE IF NOT EXISTS daily_sales_summary (
sales_date date PRIMARY KEY,
order_count integer NOT NULL,
revenue numeric(12, 2) NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO daily_sales_summary (sales_date, order_count, revenue)
SELECT
created_at::date AS sales_date,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue
FROM orders
WHERE status = 'paid'
AND created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY created_at::date
ON CONFLICT (sales_date) DO UPDATE
SET
order_count = EXCLUDED.order_count,
revenue = EXCLUDED.revenue,
updated_at = now();
一定要告诉 Claude Code 数据允许延迟多久:一分钟、五分钟、一天,还是必须实时。否则“优化”可能悄悄改变业务含义。
查询预算与回归测试
优化后要防止回退。执行计划快照通常不稳定,可以先测试 SQL 条数、响应兼容和几个代表性场景。
import { PrismaClient } from "@prisma/client";
import { expect, it } from "vitest";
it("keeps the order list within the query budget", async () => {
const queries: string[] = [];
const prisma = new PrismaClient({
log: [{ emit: "event", level: "query" }],
});
prisma.$on("query", (event) => {
queries.push(event.query);
});
await prisma.order.findMany({
take: 50,
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
select: {
id: true,
createdAt: true,
status: true,
customer: { select: { id: true, name: true } },
},
});
await prisma.$disconnect();
expect(queries.length).toBeLessThanOrEqual(2);
});
概念图
flowchart LR
A["慢查询日志"] --> B["EXPLAIN / EXPLAIN ANALYZE"]
B --> C["索引和 SQL 修改"]
C --> D["N+1、分页、聚合修复"]
D --> E["查询预算测试"]
E --> F["发布与监控"]
常见陷阱
- 列表页使用
SELECT *,把大字段也读出来。 - 只给
status这种低基数列建索引,然后期待性能大幅提升。 - 用普通 B-tree 索引优化
LIKE '%keyword%'。 - 用很小的开发数据判断生产执行计划。
- 高峰期直接跑索引迁移,没有检查锁、磁盘和副本延迟。
- ORM 的
include返回整棵关联对象。
上线检查清单
1. 是否定位了接口和用户页面
2. 是否从日志或 pg_stat_statements 捕获慢 SQL
3. 是否保存优化前后的 EXPLAIN
4. 是否检查现有索引并避免重复
5. 是否检查 N+1、OFFSET、返回列和同步聚合
6. 是否测试响应兼容
7. 是否增加 SQL 条数或延迟回归测试
8. 是否记录索引变更的锁风险、耗时和回滚方式
9. 是否定义上线后要看的指标
SQL 优化是一套工作流,不是一句技巧。Claude Code 可以把代码、迁移和测试串起来,但最终判断仍然要基于真实数据、执行计划和业务要求。
ClaudeCodeLab 提供基于这种实战流程的培训和咨询。带上慢接口、日志和 schema,就可以把一次性能排查沉淀成团队可复用的 SQL 优化清单。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。