Use Cases (更新: 2026/6/1)

用 Claude Code 实战 SQL 优化:从慢查询到上线检查

用 Claude Code 做 SQL 优化:慢查询、EXPLAIN、索引、N+1、分页、聚合、测试与上线清单。

用 Claude Code 实战 SQL 优化:从慢查询到上线检查

SQL 优化到底优化什么

SQL 优化,是在不改变结果的前提下,调整查询、索引和数据访问方式,让数据库用更少的读取、更短的等待时间返回同样的数据。它不是把 SQL 写得更炫,而是把接口、ORM 调用、执行计划、真实数据量、回归测试和发布监控一起看。

Claude Code 的优势在于它可以同时阅读路由处理器、Prisma 或其他 ORM 模型、迁移文件、慢查询日志和测试。实际项目里,慢接口通常不是由一条“神秘 SQL”造成的,而是多个普通问题叠加:列表页 SELECT *、N+1 查询、很深的 OFFSET 分页、不匹配的复合索引,以及每次请求都同步计算的仪表盘汇总。

本文以 PostgreSQL 为主,兼顾 MySQL。阅读执行计划时建议同时查看官方文档:PostgreSQL Using EXPLAINPostgreSQL EXPLAINPostgreSQL IndexesPostgreSQL Multicolumn IndexesMySQL 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,不要随便用于有破坏性的 UPDATEDELETE

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_atid 做游标。

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 优化清单。

#Claude Code #SQL #数据库 #性能 #优化
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。