审 AI 产生的 SQL,不能只看语法成功与结果像不像。至少要核对资料 grain、JOIN cardinality、filter、NULL、时间边界、聚合分母,以及 LIMIT/抽样;每一项都应搭配小型验证查询。SQL 能跑只证明引擎接受它,不证明它回答了业务问题。
这个落差在企业情境尤其明显。2026 年 EntSQL benchmark 收录五个领域、1,066 组中英对照题目,多数需要问题与 schema 之外的内部指标、报表惯例或组织规则;在该研究提供长篇企业文件的英文设定中,最佳受测系统只有 15.9%。这是特定 benchmark、版本与评估方式的结果,不能当成所有 Text-to-SQL 的通用准确率,但它说明「有 schema」不等于「懂口径」。
以下用「计算 2026 年 6 月完成订单的客户平均消费」为例。假设 orders 一列一张订单,order_items 一列一个品项。先别急着看平均数,依序审七点。
1. Grain:结果的一列代表什么?
先用一句话写出每张来源表与结果的 grain:
orders:一列一张订单
order_items:一列一个订单品项
结果:一列一位客户
再检查 key 是否符合假设:
SELECT
count(*) AS rows,
count(DISTINCT order_id) AS distinct_orders
FROM orders;
若两者不同,order_id 不是唯一,或资料已有重复。没有 grain,后面的平均、distinct 与 JOIN 都只能靠运气。
2. JOIN cardinality:有没有 fan-out?
一张订单 JOIN 多个品项后,orders.total_amount 会被复制。以下 SQL 语法正确,总额却可能被放大:
SELECT sum(o.total_amount)
FROM orders o
JOIN order_items i USING (order_id);
在 JOIN 前后比较 row count 与 distinct key:
SELECT
count(*) AS joined_rows,
count(DISTINCT o.order_id) AS distinct_orders
FROM orders o
JOIN order_items i USING (order_id);
如果只需要确认存在某种品项,可用 EXISTS;如果要算品项金额,先按 order_id 聚合 items,再 JOIN。不要用 SELECT DISTINCT 隐藏尚未理解的 fan-out。
3. Filter:包含与排除条件完整吗?
「完成订单」可能是 paid、fulfilled,是否排除退款、测试帐号与内部订单?逐项把自然语言对到 WHERE:
WHERE status IN ('paid', 'fulfilled')
AND is_test = false
AND refunded_at IS NULL
接着看被排除的分布,而不是只看保留资料:
SELECT status, count(*) AS n
FROM orders
GROUP BY status
ORDER BY n DESC;
状态定义来自业务文件或 owner,不应由模型根据栏名自行推断。
4. NULL:三值逻辑会排掉谁?
WHERE country <> 'TW' 不会保留 country IS NULL 的列;sum(amount) 会忽略 NULL;count(column) 与 count(*) 也不同。先量化:
SELECT
count(*) AS rows,
count(customer_id) AS rows_with_customer,
count(*) FILTER (WHERE customer_id IS NULL) AS missing_customer
FROM orders;
然后明确决定 NULL 是未知、无此值,还是资料错误。COALESCE 是业务处理,不是清理魔法。
5. 时间边界:用半开区间,先说清时区
月份筛选建议使用 [start, end),避免漏掉有时间的小数秒:
WHERE created_at >= TIMESTAMPTZ '2026-06-01 00:00:00+08:00'
AND created_at < TIMESTAMPTZ '2026-07-01 00:00:00+08:00'
但这只在 created_at 的型别与来源时区已确认时才成立。还要问:以建单、付款还是完成时间归月?跨日完成算哪一个 cohort?日期写对了,事件选错仍会答非所问。
6. Aggregation 与 denominator:平均的是订单还是客户?
AVG(total_amount) 是平均订单金额,不是「每位客户平均消费」。后者应先按客户聚合,再平均:
WITH customer_spend AS (
SELECT customer_id, sum(total_amount) AS spend
FROM orders
WHERE status IN ('paid', 'fulfilled')
AND created_at >= TIMESTAMPTZ '2026-06-01 00:00:00+08:00'
AND created_at < TIMESTAMPTZ '2026-07-01 00:00:00+08:00'
GROUP BY customer_id
)
SELECT
count(*) AS customers,
avg(spend) AS avg_spend_per_customer
FROM customer_spend;
同时输出 denominator count,Report 不要只留 rate 或 average。COUNT(DISTINCT customer_id) 也不能修复上游定义不清。
7. LIMIT、抽样与排序:结果是全量还是预览?
AI 在探索时常用 LIMIT 100。如果 LIMIT 被留在 CTE 或聚合前,最终结果可能只代表一小部分资料。没有 ORDER BY 的 LIMIT 也不保证是「最新」或「随机」样本。
审查时搜寻 LIMIT、TABLESAMPLE 与取样条件,确认它们只服务 preview,或在 Report 明确揭露抽样设计。若要 top 10,必须确认排序指标与 tie 处理。
一组最小验证查询,不要只跑主查询
正式 SQL 至少配三种 checkpoint:
- 体量:来源与每个主要 filter 前后的 row/distinct key。
- 分布:状态、日期、NULL 与极端值分布。
- 对帐:选一小段已知资料,手算或与可信报表比较。
例如:
-- Checkpoint:每个 filter 后剩多少订单与客户
SELECT
count(*) AS orders,
count(DISTINCT customer_id) AS customers,
min(created_at) AS min_ts,
max(created_at) AS max_ts,
sum(total_amount) AS total_amount
FROM orders
WHERE status IN ('paid', 'fulfilled')
AND created_at >= TIMESTAMPTZ '2026-06-01 00:00:00+08:00'
AND created_at < TIMESTAMPTZ '2026-07-01 00:00:00+08:00';
Checkpoint 的期望不一定是固定数字,也可以是「orders 不少于 customers」「时间不超出 6 月」「NULL customer 为零」。把 expected condition 写出来,才能在资料更新后重跑验证。
Lantide Data 如何让 SQL Review 不脱离分析契约
Lantide Data 采 SQL-first:Plan 说明业务问题、grain、分母与 checkpoints;持久 SQL 分页保存取数逻辑;Report 保存结论与 limitations。Agent 可草拟 SQL,但正式 Project Analysis 由使用者审阅 Plan 后按 Approve & Execute,SQL 与 execution evidence 留在同一分析脉络。流程见 SQL-first 工作流 与 Plan → Execute → Report。
持久分页执行后的快取可右键 查看 SQL;上游分页可用 Source Run 依赖重跑与查看 lineage。这让 reviewer 能从 Report 数字回到查询,而不是翻聊天。但 Lantide 不保证 AI SQL 正确:grain、业务状态、时间与排除条件仍需资料 owner/分析师判断;快取也是暂存中间结果,不是永久数仓。
结语:把「能跑」改成「能解释、能验证」
下次审 AI SQL,先不要看最后一格数字。依序写出 grain,检查 JOIN、filter、NULL、时间、denominator 与 LIMIT,再加三类 checkpoint。只有当主查询与验证查询能共同支持同一个业务问题,这段 SQL 才值得进 Report。