第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_at和orders.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 的默认输出。
慢查询的三大元凶:
- 缺索引:WHERE 和 JOIN 条件上的字段没有索引,大表查询必然全表扫描
- N+1:在应用层循环执行查询,1000 个用户 = 1001 次查询,比全表扫描更慢
- 函数包裹索引列:
WHERE LOWER(email) = ?让已有的 email 索引完全失效
→ 第2章:AI帮我建了索引,但 EXPLAIN 说没有用到