数据库从业者抱怨了多年:应用开发者过度依赖 ORM,生成的 SQL 性能堪忧,但没人愿意改——"我习惯了这种方式,我知道它能跑出正确结果,只是不够快。"这句话挡住了所有优化建议。
现在情况变了。越来越多开发者让 Claude Code 等 AI 助手直接生成代码。如果代码不再由人手敲出,那开发者的个人习惯就不再是瓶颈。真正的问题是:你能不能教会 AI 用更好的方式写 SQL?
ORM 惯性到底有多顽固
一个典型场景:查询某用户最近 100 条订单及其关联商品。ORM 的"方便"写法几乎必然产生 N+1 问题:
# Django ORM — 看起来简洁,实际灾难
orders = Order.objects.filter(user_id=42).order_by("-created_at")[:100]
for order in orders:
items = order.items.all() # 每条订单发一次查询
print(order.id, [i.name for i in items])
100 条订单 → 101 次数据库往返。ORM 也提供了 prefetch_related,但开发者往往懒得用,因为"默认写法已经能跑"。
手写 SQL 只需一次查询:
SELECT o.id AS order_id, i.name AS item_name
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN items i ON i.id = oi.item_id
WHERE o.user_id = 42
ORDER BY o.created_at DESC
LIMIT 100;
性能差距可以是几十倍。但说服开发者改习惯,难如登天。
AI 写代码时,惯性从何而来
AI 助手生成代码的倾向,取决于三个东西:
- 训练数据中的主流模式——ORM 用法远多于手写 SQL,所以 AI 默认输出 ORM 风格。
- 项目上下文——如果仓库里全是 ORM 调用,AI 会模仿现有风格。
- 你的指令——这是唯一你能主动控制的部分。
第三点就是突破口。开发者不愿改习惯,但改一句 prompt 没有任何心理负担。
实践:用指令让 AI 写出高性能 SQL
最直接的做法是在项目的 AI 配置文件中写入规则,让助手每次生成数据库相关代码时自动遵循。以 Claude Code 的 CLAUDE.md 为例:
# CLAUDE.md — 放在项目根目录
## 数据库规则
- 禁止生成 N+1 查询模式。涉及关联数据时,必须使用 JOIN 或子查询在单次查询中完成。
- 优先手写 SQL,而非依赖 ORM 的懒加载机制。
- 所有列表查询必须带 LIMIT;所有分页必须用 keyset pagination(基于排序键),而非 OFFSET。
- WHERE 条件中的列必须与索引定义匹配,避免隐式类型转换。
- 生成 SQL 后,附上对应的索引建议。
然后在对话中直接要求:
请查询用户 42 最近 100 条订单及每条订单的商品名称,
遵循 CLAUDE.md 中的数据库规则,用纯 SQL 实现,
返回 Python 执行代码。
AI 会输出完全不同的代码:
import psycopg2
conn = psycopg2.connect(dsn="postgresql://app:secret@db-host/mydb")
SQL = """
SELECT o.id AS order_id, o.created_at, i.name AS item_name
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN items i ON i.id = oi.item_id
WHERE o.user_id = 42
ORDER BY o.created_at DESC
LIMIT 100;
"""
with conn.cursor() as cur:
cur.execute(SQL)
rows = cur.fetchall()
# 按 order_id 分组展示
from collections import defaultdict
result = defaultdict(list)
for order_id, created_at, item_name in rows:
result[order_id].append(item_name)
for oid, items in result.items():
print(f"Order {oid}: {items}")
一次查询,零 N+1。对比之前 ORM 版本的 101 次往返,这是实质性的性能改善。
更进一步:教 AI 使用 NoRM
原文作者提到的 NoRM,核心理念是"不用 ORM,但保留便利性"——用轻量封装直接写 SQL,而非让框架替你生成。你可以把这个理念写进规则:
## NoRM 规则
- 数据访问层使用 raw SQL + 轻量结果映射,不使用 ORM Model 的懒加载。
- 示例模式:
```python
from dataclasses import dataclass
@dataclass
class OrderRow:
order_id: int
item_names: list[str]
def get_recent_orders(user_id: int, limit: int = 100) -> list[OrderRow]:
SQL = """...""" # 单次 JOIN 查询
rows = execute(SQL, {"user_id": user_id, "limit": limit})
return map_rows_to(OrderRow, rows)
当这类模式出现在项目上下文中,AI 会倾向于复用,而非回退到 ORM 默认路径。**你不需要说服开发者改变习惯——你只需要让 AI 看到更好的模式存在于这个项目里。**
## 边界与风险
这条路并非没有隐患:
- **AI 也会犯错**——生成的 SQL 可能遗漏索引提示、选错 JOIN 类型,必须人工 review 执行计划。
- **规则冲突**——如果团队规范强制使用 ORM,CLAUDE.md 的指令会与既有代码风格打架,AI 可能左右摇摆。
- **上下文窗口限制**——复杂查询涉及多表关联时,AI 可能简化甚至跳过规则,需要拆分任务逐步引导。
- **可持续性**——规则写在配置文件里,新成员不一定读;最好配合 CI 中的 SQL 审查工具(如 `sqlfluff`)做自动化校验。
一个务实的起步方式:
```bash
# 安装 sqlfluff,在 CI 中自动检查 AI 生成的 SQL
pip install sqlfluff
sqlfluff lint queries/ --dialect postgres
把 lint 规则和 AI 指令对齐,形成双重保障。
可以现在尝试的事
- 在当前项目的 AI 配置中加 3 条 SQL 规则——禁止 N+1、强制 LIMIT、优先 JOIN。观察一周,看生成代码的变化。
- 挑一个已知慢查询的接口,让 AI 用纯 SQL 重写,对比执行时间。
- 把重写后的模式作为示例写入项目规则,让后续生成自动复用。
开发者惯性是真实存在的心理障碍,但当代码由 AI 生成时,这个障碍被绕过了——你不需要改变人的习惯,只需要改变 AI 看到的上下文。这可能是多年来推动 SQL 性能改善最现实的机会。