文章梳理 AI 生成 SQL 时常见的字段臆造、方言混用、业务口径偏差及关联重复计数问题。建议提供数据库结构、目标方言和执行计划,并明确业务定义及数据特征。
AI 工具很擅长写 SQL。向 ChatGPT、Claude、Cursor 或 Claude Code 提出“按套餐统计月收入”的需求,几秒钟就能拿到一条查询,而且它很可能可以运行。
问题恰恰就在这里。错误的 SQL 很少会直接报错。它会返回一个数字,而且这个数字看起来还挺合理。下面是我在 AI 生成的 SQL 中最常见的错误、它们出现的原因,以及能解决其中大多数问题的三项输入:schema、dialect 和执行计划。
模型并不是粗心,而是缺少信息:
这些信息缺口,每一个都会变成某种具体、可辨认的 bug。
-- "Revenue per plan from subscription invoices"
SELECT i.plan_id, SUM(i.amount_cents) AS revenue_cents
FROM invoices i
JOIN invoice_lines l ON l.invoice_id = i.id
WHERE l.kind = 'subscription'
GROUP BY i.plan_id;
如果一张发票有三条订阅明细,它的总金额就会被累加三次。查询可以运行,数字却是错的,而且没有任何提醒。修复方法是避免在错误的数据粒度上进行关联:
SELECT i.plan_id, SUM(i.amount_cents) AS revenue_cents
FROM invoices i
WHERE EXISTS (
SELECT 1 FROM invoice_lines l
WHERE l.invoice_id = i.id AND l.kind = 'subscription'
)
GROUP BY i.plan_id;
只有知道 invoices → invoice_lines 是一对多关系,模型才有可能发现这个问题。这意味着,你需要提供包含键信息的 schema。
-- "Customers who have never ordered"
SELECT id FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);
只要 orders.customer_id 中有一个值是 NULL,NOT IN 就不会为真,这条查询便会返回零行。NOT EXISTS 没有这个问题:
SELECT c.id FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
类似的陷阱还有:status <> 'cancelled' 也会过滤掉 status 为 NULL 的行;COUNT(col) 会跳过 NULL,而 COUNT(*) 不会;SUM 在没有任何输入行时返回 NULL,而不是 0。模型需要知道哪些列允许为 NULL,所以请给它 DDL,而不是一份概述。
有些差异会直接报错,比如 MySQL 中不存在 date_trunc,也不支持 RETURNING。危险的是那些可以正常运行、结果却不同的情况:
告诉模型数据库引擎和版本。只说“MySQL”还不够:窗口函数和 CTE 是 MySQL 8.0 才引入的,EXCEPT 则到 8.0.31 才加入。
整理一份上下文,之后反复使用。直接从数据库获取 DDL:
保留列是否允许为 NULL、外键、唯一约束,以及复合索引的列顺序。补充大致行数和项目约定,比如软删除列、租户列,以及时间戳使用的时区。在 Cursor、Claude Code 或 Codex 中,可以将这些内容保存为仓库里的一个文件,例如 docs/db-context.md,再从规则文件中引用它,确保每次都会加载。
大多数报表出错,是因为某个定义没有说清楚,而不是语法有问题。下面是我通常使用的起始 prompt:
You are a senior SQL developer. Write ONE query for the exact engine and version below.
Engine: [ENGINE AND VERSION]
Schema (use only these tables and columns; if something is missing, say so):
[DDL]
Question: [QUESTION]
Definitions: [e.g. "month = calendar month in UTC", "exclude deleted_at IS NOT NULL"]
Before writing SQL:
1. State the grain (one output row per what), filters, and the time window as
>= start AND < end.
2. List your assumptions. If one would change the answer materially, ask me.
3. Name each join and its cardinality (1:1, 1:N, N:M) and how you avoid
double counting in aggregates.
Then write the query, and give me 2 sanity-check queries (a total that should
reconcile, and a spot check for one entity).
Rules: bind parameters for user values, SELECT only, syntax valid for this engine.
“关联基数”这一步能发现 fan-out。“列出假设”这一步能发现定义上的问题。合理性检查则让你有查询可以实际运行验证,而不必直接相信生成的结果。
如果模型没有执行计划可看,你得到的建议往往就是“给 WHERE 子句里的每一列都加上索引”。把执行计划提供给它:
有一点需要注意:在 PostgreSQL 和 MySQL 中,EXPLAIN ANALYZE 会实际执行语句。对于 UPDATE 或 DELETE,请将它放在事务中执行,然后回滚;条件允许时,尽量在数据副本上操作。
接着,让模型逐个节点解读执行计划,并为每一项判断指出对应的节点。对于 PostgreSQL 的执行计划,要告诉它 actual time 和 rows 都是每次循环的数值,因此,一个看起来开销很小、但 loops=50000 的内层节点,可能才是耗时所在。让它标出预估行数与实际行数不符的地方——这往往是统计信息的问题,而非索引问题——以及溢出到磁盘的排序。另外,执行计划要来自接近生产环境的数据:面对一千行和一千万行,查询规划器选择的执行计划会不同。
AI 生成的只读 SQL 风险较低。对于 AI 生成的 DDL 和 DML,则应该立下一条规矩:没有逐条读过所有语句,任何人都不能在生产环境运行。具体来说:
提供 schema,明确 dialect,写清定义,附上执行计划,再验证结果。模型还是同一个模型,改变答案的是输入。
我把自己使用的 32 个 prompt,加上一份验证清单和一份 PDF 指南,整理成了 AI SQL & Database Prompt Pack。这些 prompt 涵盖:结合 schema 上下文将自然语言转成 SQL;检查查询中的 NULL、fan-out、N+1 和索引使用问题;解读 PostgreSQL、MySQL 和 SQLite 的 EXPLAIN;审查 schema 和索引;零停机迁移与回滚;ORM 和注入检查;种子数据和数据库测试;以及使用窗口函数生成报表。上面介绍的所有方法,不购买这个合集也一样能用。
— Pluto · @Mars_Peakul
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。