第01章:AI写的查询没有问题,但数据量大了就慢成狗

第01章:AI写的查询没有问题,但数据量大了就慢成狗

“本地开发,users 表1000行,AI 写的查询 10ms。上线后 100 万行,同样的查询 5 秒——因为你缺少索引,或者查询里有隐式的全表扫描。”


ℹ️ 版本说明:本章基于 PostgreSQL 18.4,运行环境 Ubuntu 26.04 LTS。

1.1 AI默认会生成什么

你让 AI 帮你查询"过去30天内注册且有过至少一笔订单的用户":

SELECT u.id, u.email, u.created_at, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= NOW() - INTERVAL '30 days'
GROUP BY u.id, u.email, u.created_at
HAVING COUNT(o.id) > 0
ORDER BY u.created_at DESC;

逻辑上完全正确,测试数据里也跑得很快。但是:

  • users 表100万行 + orders 表500万行时,这个查询可能需要 30 秒
  • 没有索引提示,AI 不知道 users.created_atorders.user_id 是否有索引
  • LEFT JOIN ... HAVING COUNT > 0 等价于 INNER JOIN,但语义不直观,优化器可能生成低效执行计划

1.2 AI通常遗漏的4个坑

⚠️ 坑1:没有在 WHERE 和 JOIN 字段上建索引

-- 这个查询需要以下索引才能高效执行:
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_orders_user_id ON orders(user_id);

AI 在写查询时通常不主动提索引——它假设你已经有了。但如果你的表是新建的,这两个索引就不存在,查询会退化为顺序扫描(Seq Scan),扫描全表。


⚠️ 坑2:N+1 查询——循环里执行 SQL

AI 经常生成这种 Python/Node.js 代码:

users = db.execute("SELECT id, email FROM users WHERE active = true").fetchall()
for user in users:
    # 对每个用户再查一次数据库 ← 这是 N+1 问题
    orders = db.execute(
        "SELECT * FROM orders WHERE user_id = %s", (user.id,)
    ).fetchall()
    user.orders = orders

10000 个用户 = 10001 次数据库查询。正确做法:一次 JOIN 查询。


⚠️ 坑3:在 WHERE 中对索引列使用函数

-- 这个查询无法用 created_at 的索引!
WHERE DATE(created_at) = '2026-01-01'

-- 正确写法(可以用到索引)
WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'

对索引列套函数(DATE()LOWER()YEAR()),PostgreSQL 就无法直接用 B-tree 索引做范围查询,只能全表扫描。


⚠️ 坑4:SELECT * 带来的额外 I/O

SELECT * FROM orders WHERE user_id = 123;  -- 可能返回30个字段

如果你只需要 id、amount、status 三个字段,SELECT * 会额外读取所有其他字段的数据——包括 JSONB、TEXT 这类大字段,增加大量不必要的 I/O。


1.3 更好的提示词

提示词 P01:生成查询的同时给出索引建议

使用时机:让 AI 写任何涉及大表的查询

比默认多了什么

  • 同时给出查询和所需索引
  • 预估查询性能
  • 提示 N+1 风险
帮我写一个 PostgreSQL 18.4 查询,同时给出性能优化建议。

需求:查询过去30天内注册、且有过至少1笔订单、且订单总金额超过100元的用户列表。

表结构:
- users(id bigserial PK, email text, created_at timestamptz, status text)
- orders(id bigserial PK, user_id bigint FK, amount decimal, status text, created_at timestamptz)

给我:
1. 最优的 SQL 查询(考虑执行计划效率)
2. 这个查询需要哪些索引才能高效运行?
   - 列出所有需要的 CREATE INDEX 语句
   - 解释每个索引的作用
3. 如果在 Python/SQLAlchemy 里使用,如何避免 N+1 问题?
4. 这个查询的大概时间复杂度?users 100万行 + orders 500万行时预期耗时?

基于 PostgreSQL 18.4。

提示词 P02:检查现有查询是否有性能问题

使用时机:怀疑某个 AI 生成的查询在大数据量下有问题

比默认多了什么

  • 主动识别全表扫描风险
  • 检查函数包裹索引列
  • 建议重写方案
帮我审查以下 PostgreSQL 查询的性能问题,假设表数据量为:users 100万行,orders 500万行。

查询:
[粘贴你的 SQL]

表中已有索引(如果知道的话):
[列出或者写"不清楚"]

请检查:
1. 是否有全表扫描风险(WHERE 条件缺少索引)?
2. JOIN 条件的字段是否有索引?
3. WHERE 条件中是否有对索引列使用函数(导致索引失效)?
4. 是否有隐式类型转换(如 int 和 bigint 比较)?
5. GROUP BY 和 ORDER BY 能利用索引吗?
6. 这个查询在高并发下的风险(锁竞争)?

对每个问题:给出问题描述 + 修复方案 + 修改后的 SQL。

基于 PostgreSQL 18.4。

提示词 P03:消除 N+1 查询

使用时机:发现代码里有循环执行 SQL 的模式

比默认多了什么

  • 把多次查询合并为一次
  • 支持 SQLAlchemy / Prisma / 原生 SQL 的不同写法
帮我把以下 N+1 查询改写为单次高效查询。

当前代码(Python + SQLAlchemy):
[粘贴你的代码]

或者描述:[描述你的查询模式]

要求:
1. 用一次 SQL 查询完成所有数据获取(消除循环查询)
2. 使用 JOIN 或 subquery 或 LATERAL 合并查询
3. 如果用 SQLAlchemy ORM:
   - 用 selectinload 或 joinedload 替代懒加载
   - 解释 selectinload(额外1次查询)和 joinedload(JOIN)的区别,以及何时用哪个
4. 如果用原生 SQL:给出完整的 SQL + 所需索引
5. 对比改造前后的数据库请求次数

基于 PostgreSQL 18.4 + SQLAlchemy 2.x。

1.4 验收清单

检查项 验证方法 AI辅助
关键查询有对应索引 \d tablename 查看索引 让 AI 列出所需 CREATE INDEX
无全表扫描(大表) EXPLAIN (ANALYZE, BUFFERS) [your query] 无 Seq Scan 让 AI 解读 EXPLAIN 输出
无 N+1 问题 代码审查:循环里无 SQL 调用 用 P03 提示词重写
WHERE 条件不包裹函数 代码审查:DATE()、LOWER() 等包裹索引列 让 AI 重写查询条件
SELECT 指定字段 代码审查:无 SELECT * 让 AI 替换 SELECT *
数据量测试 用生产级数据量(≥10万行)测试查询耗时 让 AI 生成测试数据 SQL

1.5 本章小结

如果你只记一件事:每次让 AI 写查询时,同时问它"这个查询需要哪些索引"。AI 写的 SQL 逻辑是对的,但它不知道你的表有没有建索引——这是你的责任,而不是 AI 的默认输出。

慢查询的三大元凶

  1. 缺索引:WHERE 和 JOIN 条件上的字段没有索引,大表查询必然全表扫描
  2. N+1:在应用层循环执行查询,1000 个用户 = 1001 次查询,比全表扫描更慢
  3. 函数包裹索引列WHERE LOWER(email) = ? 让已有的 email 索引完全失效

→ 第2章:AI帮我建了索引,但 EXPLAIN 说没有用到