自然语言也能写复杂SQL:用 Select AI 搞定多表联查与统计分析的实战笔记
上个月月底,老王又站在我工位旁边了。
老王是我们这边的业务分析负责人,手里永远攥着一杯凉透的美式,开口永远是"帮个忙"。那天他甩过来一张Excel截图,是老板要的月度经营分析报告:按大区拆、按月拆,还要看同比,数据源跨了客户表、订单表、订单明细、产品表,外加一张区域维度表。老王自己试着拼 SQL,写了两小时,JOIN 写串了,GROUP BY 少了一层,最后对着报错发呆。
"你们 DBA 不是天天写这个?"他说。
我本来想让他走工单系统,但那周正好在折腾 Oracle 26ai 里的 Select AI —— 一个能用自然语言直接生成 SQL 的东西。我忽然想,与其替他写,不如让他自己对着数据库说话。于是有了这篇笔记,记的是怎么用 Select AI 搞定那些让人头大的多表关联和统计分析,以及我踩过的坑。
先说清楚,我这里用的环境是 Oracle AI Database 26ai,Select AI 是内置能力,不用额外装组件。如果你手上是 Autonomous Database 的 26ai 版本,下面的操作基本能照搬;老一点的版本可能有些动作(比如对话记忆)支持不全,这个后面会提。
一、先搞懂它到底是怎么干活的
很多人第一次听说"用大白话让数据库出 SQL",脑子里冒出来的画面是:我把整张表的数据打包发给某个大模型,它帮我算。这个画面是错的,而且错得危险。
Select AI 真正的做法是反过来:它把数据库的 结构信息 ——也就是表名、列名、数据类型、注释、还有表与表之间的外键约束——整理成一份元数据说明,连同你的自然语言问题一起,发给大模型。大模型看到的只有"长什么样",看不到任何一行真实业务数据。生成出 SQL 之后,这条 SQL 是在 Oracle 数据库里面自己跑的,结果也只在库里待着。
这件事有两层好处。一层是安全,业务数据根本没出数据库,合规那关好过;另一层是准,大模型不需要凭空猜你的表叫什么、列叫什么,它手里就有现成的字典。我后来带老王做演示,第一句话就是告诉他:你放心问,客户手机号不会飞到云上的某个模型里去。
语法简单到有点不像话,就是一条 SELECT:
SELECT AI [动作] 你的自然语言问题;
不写动作的时候,默认是 runsql ,也就是"生成 SQL 并直接执行,把结果给我"。除此之外还有几个动作,我后面全都会用到:
showsql :只把生成出来的 SQL 显示出来, 不执行 。我调试的时候几乎离不开它。
explainsql :用自然语言解释这条 SQL 在干什么,相当于让模型给 SQL 写白话注释。
narrate :把查询结果用一段自然语言描述出来,做报表自动生成文字时特别香。
chat :不走 NL2SQL,直接跟模型聊天,配合对话记忆用。
showprompt :把最终拼好、准备发给模型的完整提示词亮给你看,想搞清楚"它到底拿到了什么信息"时用它。
我习惯的顺序是:先用 showsql 看它写了什么,确认没问题,再去掉动作让它真跑。毕竟让一个模型直接动生产库,咱得有点敬畏心。
二、地基不牢,后面全歪:先把 AI Profile 配明白
Select AI 不是你连上就能问的,它得先有个"AI Profile",相当于告诉它:用哪家的大模型、凭证在哪、你能看哪些表。创建这个 profile 用的是 DBMS_CLOUD_AI.CREATE_PROFILE 。
我第一次配的时候,随手写了个最朴素的版本:
BEGIN
DBMS_CLOUD_AI.CREATE_PROFILE(
profile_name => 'MYAI',
attributes => '{"provider": "oci",
"credential_name": "OCI_CRED",
"object_list": [{"owner": "SH", "name": "customers"},
{"owner": "SH", "name": "orders"},
{"owner": "SH", "name": "order_items"},
{"owner": "SH", "name": "products"},
{"owner": "SH", "name": "countries"}]}');
END;
/
EXEC DBMS_CLOUD_AI.SET_PROFILE('MYAI');
这里面几个东西得说清楚。 provider 是模型供应商,Oracle 自己这套默认走 OCI,背后是 meta 的 llama-3.3-70b-instruct,你也可以换成 Azure OpenAI、OpenAI、Google Gemini、Anthropic Claude、Cohere 这些,配置里改个 provider 和对应凭证就行。 credential_name 是之前在数据库里建好的访问凭证,这个每个云账号不一样,我就不展开了。 object_list 是重头戏 ——它用 JSON 数组指明"大模型这回能看见哪几张表"。
为什么这个 object_list 这么关键?两个原因。一是安全,你总不希望模型顺手把 HR 的薪资表也当成了可查询对象;二是准确率,库里几百张表,你让它从里面猜你要哪几张,它八成猜错。把范围钉死在五张表,它就没那么多发挥空间了。
还有个开关叫 enforce_object_list ,设成 "true" 之后,模型被硬性约束只能碰 object_list 里的表,多一张都不行;设成 "false" ,模型还能凭自己的"常识"去引用别的表。我给老王用的环境,一律 true ,宁可它说"这表我看不到",也不能让它瞎 JOIN。
配完 SET_PROFILE 之后,就可以开口问了。我让老王先试个最简单的:
SELECT AI showsql how many customers in San Francisco are married;
出来的 SQL 是这样的:
SELECT COUNT(DISTINCT c."CUST_ID")
FROM "SH"."CUSTOMERS" c
JOIN "SH"."COUNTRIES" co ON c."COUNTRY_ID" = co."COUNTRY_ID"
WHERE c."CUST_CITY" = 'San Francisco'
AND c."CUST_MARITAL_STATUS" = 'married';
老王盯着这行 JOIN ... ON c."COUNTRY_ID" = co."COUNTRY_ID" ,半信半疑: “它怎么知道要连 countries 表?”——这就是 object_list 的功劳,只有五张表可选,城市在 customers 里、国家维度在 countries 里,模型顺着字段名就把关系找出来了。那一刻他眼睛亮了,但我知道,真正的麻烦在后面。
三、第一次翻车:四表联查,它把 JOIN 写丢了
老王真正的需求是经营分析,得把 orders、order_items、products、customers 四张表串起来,按产品线统计销售额。我让他试着问:
统计每个产品类别的销售额总和,按销售额从高到低排
我先用 showsql 看它写的 SQL,结果一出我就有点头皮发麻:
SELECT p."CATEGORY", SUM(oi."AMOUNT") AS total_sales
FROM "SH"."PRODUCTS" p
JOIN "SH"."ORDER_ITEMS" oi ON p."PROD_ID" = oi."PROD_ID"
GROUP BY p."CATEGORY"
ORDER BY total_sales DESC;
它漏了 orders 表。真实数据里,金额字段 AMOUNT 其实在 order_items,但 order_items 只存了订单行,真正的"是否有效订单""下单时间"在 orders 表里。模型凭列名直接把 products 和 order_items 连了,压根没去碰 orders。单看这条 SQL 能跑、也能出数,但出的数是错的——它把一些本该被过滤掉的无效订单也算进去了。
这就是纯靠字段名推断的软肋:当两张表能"连上",但业务上还需要第三张表做过滤时,模型容易偷懒。我当时的处理是,先不让它跑,去 profile 里加一个开关:
BEGIN
DBMS_CLOUD_AI.CREATE_PROFILE(
profile_name => 'MYAI',
attributes => '{"provider": "oci",
"credential_name": "OCI_CRED",
"object_list": [{"owner": "SH", "name": "customers"},
{"owner": "SH", "name": "orders"},
{"owner": "SH", "name": "order_items"},
{"owner": "SH", "name": "products"},
{"owner": "SH", "name": "countries"}],
"constraints": "true"}');
END;
/
EXEC DBMS_CLOUD_AI.SET_PROFILE('MYAI');
这个 constraints": "true" 是转折点。开启之后,Select AI 会把表与表之间的 外键约束 也一并喂给模型。注意,前提是你的库里这些外键真的建了 ——我们这套 SH 样例库里 orders 和 order_items 是有外键的,order_items 指向 orders,orders 指向 customers,products 独立。模型一旦拿到了"谁引用谁"这张关系网,再去写 JOIN 就不再是瞎蒙,而是顺着约束走。
重新问一遍同一句话,这次 showsql 出来的是:
SELECT p."CATEGORY", SUM(oi."AMOUNT") AS total_sales
FROM "SH"."ORDERS" o
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
JOIN "SH"."PRODUCTS" p ON oi."PROD_ID" = p."PROD_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY p."CATEGORY"
ORDER BY total_sales DESC;
看到 WHERE o."ORDER_STATUS" = 'COMPLETE' 那行了吗?它把 orders 表拉进来做了有效订单过滤,三表 JOIN 全齐。我把这条跑出来,跟老王之前手写的、用 BI 工具导出的对照表一对,数字对上了。老王沉默了三秒,说了句:“早知道有这玩意,我前年就不该自学 SQL。”
——这就是我第一次被 Select AI 惊到的瞬间。
四、注释才是隐藏的功臣
四表联查那次之后,我以为问题都解决了。直到老王又扔来一个问题: “按部门算一下工资高于平均的员工占比。“这次它出的 SQL 字段是对的,但算出来的"平均"和财务系统对不上。我追进去一看,库里跟工资相关的列有两个: salary (本币记账额)和 salary_conv (换算成美元的值),模型随手抓了 salary ,而财务那边口径是美元。这种事光靠外键约束救不了,因为两张列都在同一张表里,约束管的是"表与表之间”,管不了"列与列之间谁更对”。
解法在 comments 。创建 profile 时加一个 "comments": "true" ,Select AI 会把你在表、列上写的 COMMENT 也一并塞给模型。我先给库补了点人话:
COMMENT ON TABLE employees IS '核心员工表,包含在职与历史员工';
COMMENT ON COLUMN employees.salary IS '本币记账工资,单位见币种字段';
COMMENT ON COLUMN employees.salary_conv IS '已折算为美元的工资金额,对外报表统一用此列';
BEGIN
DBMS_CLOUD_AI.CREATE_PROFILE(
profile_name => 'MYAI',
attributes => '{"provider": "oci",
"credential_name": "OCI_CRED",
"object_list": [{"owner": "SH", "name": "employees"},
{"owner": "SH", "name": "departments"},
{"owner": "SH", "name": "orders"},
{"owner": "SH", "name": "order_items"},
{"owner": "SH", "name": "products"}],
"constraints": "true",
"comments": "true"}');
END;
/
EXEC DBMS_CLOUD_AI.SET_PROFILE('MYAI');
加了注释重问,模型老老实实用了 salary_conv 。这事儿让我意识到,注释不是写给人的文档,它是直接喂给模型的"业务口径"。后来我去翻 Oracle 那份 AI Enrichment(AI 赋能)的指南,发现官方把这件事系统化了一套方法:它可以 不改数据库结构和数据 ,纯粹加一层业务元数据,帮模型消歧义。
指南里给的注释标签很实用,我摘几个老王最常用的:
DESCRIPTION :说明这个对象到底是干嘛的。比如 T_EMP 写一句"这张表同时存了在职和离职员工"。
ALIASES :列的同义词。模型不懂你的黑话, EMP_ID 标上"employee number / worker id / person number",它就能听懂各种问法。
UNITS :数值单位。 salary_conv 标 Expressed in USD ,前面那个坑就从源头堵死了。
JOIN COLUMN :首选的 JOIN 伙伴。 EMPLOYEES.department_id 标上"常 JOIN DEPARTMENTS.department_id",模型就不会去错连别的表。
VALUES :枚举样本值。 status_code 标 A=在职, I=离职, T=已 termination ,模型就不会拿 A 当别的含义。
指南里那个 HR 的例子跟我们踩的坑几乎一模一样:扩展后的 EMPLOYEES 表有 salary_conv (美元)和 hire_dt (系统时间戳)这类容易混淆的列,问"2020 年后入职员工的平均美元薪资、按部门排名",没加注释时模型误用了 salary (本币)和 hire_dt (系统时间),还拿 manager_id 去错连 DEPARTMENTS;加上 UNITS: USD 、 JOIN COLUMN: EMPLOYEES.department_id 、 DESCRIPTION: 官方入职日 之后,模型精准选了 salary_conv 、 department_id 正确 JOIN、 hire_date 过滤。还有一个医疗的例子更极端:Practitioner、Event、EventDetail 三张表,问"晚上做的手术有多少",没注释时模型漏 JOIN EventDetail,拿预约时间当手术时间,把"下午约、晚九点才手术"的case全算漏了;给 act_desc 标了 DESCRIPTION 加 SAMPLE VALUES: surgery, consultation 之后,模型才正确地用 act_desc='Surgery' 和 act_time 去过滤。
老王听完直拍大腿:"敢情这玩意不是开箱即用,得调教。"对,它像头聪明但没常识的牛,你把业务口径喂到位,它才不乱撞。
五、统计分析的深水区
到了真正的经营分析,光是"连上表、加过滤"还不够,得会聚合、会分组、还会算同比。这部分是 Select AI 最让我意外的地方——它能写的不只是简单 COUNT ,GROUP BY 多层分组、窗口函数它都兜得住。
先来个分组聚合。老王要的是"按大区和月份,看每类产品线的销售额"。我让他问:
SELECT AI showsql 按大区和月份统计每个产品类别的销售额,只算已完成订单;
出来的 SQL:
SELECT c."REGION",
TRUNC(o."ORDER_DATE", 'MM') AS month,
p."CATEGORY",
SUM(oi."AMOUNT") AS total_sales
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
JOIN "SH"."PRODUCTS" p ON oi."PROD_ID" = p."PROD_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY c."REGION", TRUNC(o."ORDER_DATE", 'MM'), p."CATEGORY"
ORDER BY c."REGION", month, total_sales DESC;
四表 JOIN、三层 GROUP BY、 TRUNC 取月,一步到位。我没有因为这是模型写的就放松,照样用 showsql 先核了一遍逻辑,确认 TRUNC(..., 'MM') 的取月方式和我们报表口径一致,才让它 runsql 真跑。
真正刺激的是同比。老王老板最爱问"今年这个月比去年同月涨了多少"。这种需求用 SQL 写,标准做法是窗口函数 LAG 取去年同期再算比值。我把问题抛给 Select AI:
SELECT AI showsql 各大区本月销售额,以及相比去年同期的增长率;
它给的 SQL 是这样的:
SELECT region,
month,
total_sales,
LAG(total_sales, 12) OVER (PARTITION BY region ORDER BY month) AS last_year_sales,
ROUND((total_sales - LAG(total_sales, 12)
OVER (PARTITION BY region ORDER BY month))
/ LAG(total_sales, 12)
OVER (PARTITION BY region ORDER BY month) * 100, 2) AS yoy_pct
FROM (
SELECT c."REGION" AS region,
TRUNC(o."ORDER_DATE", 'MM') AS month,
SUM(oi."AMOUNT") AS total_sales
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY c."REGION", TRUNC(o."ORDER_DATE", 'MM')
)
ORDER BY region, month;
LAG(..., 12) 取往前第 12 个月的销售额,按大区分区、按月排序——这逻辑是对的。这里我得如实说一句:官方文档里没给窗口函数这种复杂统计的现成样例,但 Select AI 底层是靠大模型生成 SQL 的,标准窗口函数它本就能写。我的做法是, 凡是它写出了我拿不准的语法,一律先 showsql 看、再小范围抽样核对 ,确认无误才上全量。靠这一条,老王那张同比表一次跑通,没返工。
SQL 跑出来是一堆数字,老板要看的是话。这就要用 narrate 。我把刚才的结果接上:
SELECT AI narrate 用一段话总结各大区本月销售额和同比增长情况;
模型吐出来一段人话:"华东大区本月销售额 1,240 万元,同比增长 18.3%,增速领跑全国;华北大区 980 万元,同比增长 4.1%,增速放缓需关注……"老王当场把这段贴进了汇报 PPT,省了他两小时写文字。后来我们把 narrate 接在定时任务后面,每天自动出一段经营简报,这事现在都不用他操心了。
还有个 explainsql 也值得提。老王偶尔不服模型写的 SQL,我就让他 SELECT AI explainsql 上面那句问题 ,模型会用大白话说清楚"这条 SQL 先连了哪几张表、按什么分组、算的是什么"。对新手而言,这比看执行计划友好太多——它解释的是"意图",不是"算法"。
六、让分析连成一条线:对话记忆
前面那些都是一问一答,彼此独立。但真实分析是连续的:我先看各区域销售额,再问"那同比增长呢",再问"增长最快的区域里,哪个品类贡献最大"。这种上下文连贯,靠的是 conversation 开关。
创建 profile 时加 "conversation": "true" , chat 动作就会带上之前的对话历史。我给老王演示过一段:
SELECT AI chat 各区域的总销售额是多少;
-- 返回:华东 1.2亿,华北 0.9亿……
SELECT AI chat 那同比增长最快的是哪个;
-- 模型基于上一条的"各区域"上下文,直接给出同比增长最快的区域
它记住了前一句里"各区域"这个前提,第二句不用重复说。文档里提过,这个记忆默认最多保留 10 条历史,超出就丢最早的。所以真做长链路分析,别指望它无限记;要么把关键前提在每句话里带一点,要么把复杂问题拆成"一段问一块",每段重新把范围说清。老王有回连问了二十多句,到后面模型开始"失忆",还以为他在问别的表,就是踩了这个 10 条的线。
showprompt 在这时候是排查神器。当你觉得模型"理解偏了",就 SELECT AI showprompt 你的问题 ,它会把最终拼好、准备发给大模型的完整提示词亮出来 ——里面有哪些表、哪些列、哪些注释、哪些约束,一目了然。我曾靠它发现,某次模型把"销售额"理解成了"订单数",原因是我们对 AMOUNT 列的注释写得太含糊,补了一句"本列是订单金额,非订单笔数"之后就好了。
七、复盘与几句实在话
写到这,我把这段日子带老王用 Select AI 做复杂查询的经验,浓缩成几条,算给后来人避坑:
准确率三件套,缺一不可。 object_list 把模型能看的表钉死在范围内,既是安全闸也是准星; constraints: true 把外键关系喂给它,JOIN 才不会再漏表; comments: true 加上 AI Enrichment 的业务注释,是消歧义的最后一道关。这三样都配上,复杂联表查询的命中率肉眼可见地上去了。
永远先用 showsql 看,再 runsql 跑。 模型再聪明,也是概率生成,不是上帝。聚合口径、取数范围、时间粒度,这些业务语义它不一定一次猜对。先看 SQL、抽样核对、再上全量,这套动作我从未省略过。
注释是给模型看的,不是给人看的。 别写"本表存储员工信息"这种废话,要写"本表含在职与离职、薪资以美元列为准、入职日看 hire_date"这种能改变模型行为的硬信息。标签化的 UNITS 、 JOIN COLUMN 、 VALUES 比散文管用。
它替代不了 DBA,但能解放业务人员。 老王现在自己建了 profile,连四表经营分析都能自己出,不再半夜给我发微信。但遇到模型反复写不对、或者要动生产库结构的事,还是得我们上。它的定位是"让会提需求的人自己拿到数据",不是"让提需求的人替代工程师"。
最后说个细节,挺能说明问题。前两天老王跑来问我:"为啥我直接问它’销售额最高的产品’它答得飞快,一问’按区域分月看同比’就偶尔抽风?"我看了眼他的 profile,只有几张表、没开 constraints、列上一个注释没有。我让他花一下午把注释补了、外键确认了,再问,稳了。他后来跟我说了句实在话:“这东西不是不聪明,是得有人教它我们这摊生意到底怎么算账。”
这话比任何技术文档都到位。Select AI 把写 SQL 的门槛砍到了地板上,但"算得对"这件事,终究还是业务知识和工程规范在兜底。工具替你拿笔,账怎么算,得你自己清楚。
八、把一套能用的环境端到端搭起来
前面那些片段,都是我已经跑通之后的叙述。有人问我:"听着简单,我自己照着搭要踩多少坑?"我把当时从零到能用的步骤捋一遍,后来人照抄能省不少事。
第一步,建访问大模型的凭证。Select AI 要调外面的模型服务,得先在库里存好访问密钥,用的是 DBMS_CLOUD.CREATE_CREDENTIAL 。OCI 走的是资源主体加密钥,Azure OpenAI 走的是 endpoint 加 key,各家字段不同,但都是这一条命令,填错一个字母后面全白搭。我第一次就是把 key 末尾多带了个换行,报了一下午"凭证无效",最后用十六进制编辑器才看明白。
第二步,建 AI Profile,就是前面反复出现的那段 CREATE_PROFILE 。这里有个细节:JSON 里的 object_list 里 owner 的大小写要和数据库里 schema 名完全一致。Oracle 默认对象名是大写存储的,但如果你建 schema 时用了引号小写,那 object_list 里也得小写,否则模型"看"不到那张表,还不会明说,只会在生成 SQL 时默默忽略它——这种沉默的错误最坑人。
第三步,授权。AI Profile 是 DBA 级别的对象,业务用户要用,得让他能 SET_PROFILE 到对应的 profile,并且对 object_list 里的表有 SELECT 权限。老王那个账号,我就是这么配的:给他建了个独立的 profile,只能看经营分析那几张表,想越界都越不了。这也是为什么我始终建议,业务用的 profile 和 DBA 自己调试的 profile 分开建,权限边界清清楚楚。
第四步,加注释和 Enrichment。凭证和 profile 管的是"能不能用",注释管的是"用得准不准"。我一般先把表里那些容易混的列过一遍,把 UNITS 、 JOIN COLUMN 、 VALUES 标上,再让老王试着问。Enrichment 那个功能有个坑得提醒:不能在 SYS / SYSTEM 这种系统 schema 上启用,会直接失败,得在业务 schema 上操作。我们当时在业务用户下建,一次就成。
第五步,验证。别急着让业务上手,自己先用 showsql 把常见问题的 SQL 跑出来核一遍,尤其是聚合口径和时间粒度。确认三五个典型问题都对了,再交出去。我们那套环境,从建凭证到老王独立出第一张报表,前后不到一个下午,主要时间花在补注释上,而不是调工具。
九、不同模型供应商的实测差异
Select AI 好玩的地方在于,同一个自然语言问题,换不同的模型供应商,出来的 SQL 质量能差出不少。文档里列的支持方挺全:OCI 默认是 meta 的 llama-3.3-70b-instruct,另外能接 Azure OpenAI(GPT-4o 那系)、OpenAI(GPT-4o 等)、Google(Gemini)、Anthropic(Claude)、Cohere,还有 Hugging Face、AWS 这些。换供应商基本就是改 profile 里的 provider 和对应凭证,SQL 语法层面不用动。
我们实测下来,结论很朴素:模型越强,复杂多表查询越稳。像老王那种"四表联查加同比"的问题,GPT-4o 和 Claude 基本一次写对,llama-3.3-70b 偶尔会在外键约束多的时候漏 JOIN,得靠 constraints: true 兜底,或者干脆切到强模型。但强模型有强模型的代价:OCI 自家的 llama 延迟最低、调用成本也低,而且数据全程不出 OCI 边界,合规上最省心;GPT-4o、Claude 这些第三方,准确率高,但数据要出网到对应云服务,金融、医疗这类强监管行业得先过合规那一关。
我的做法是分档用:日常经营分析、报表这类高频但结构固定的需求,跑默认 OCI 模型,又快又便宜;遇到逻辑特别绕的临时探查,临时把 profile 切到 GPT-4o 或 Claude,跑完切回来。切换也就是一条 SET_PROFILE ,秒级生效,不耽误事。老王后来也学会这手了,他说: “简单问题别浪费好钢,难的问题才请大佛。”
还有个容易被忽视的点:模型供应商对中文自然语言的理解也有差异。我们的问题基本都是中文提的,实测下来 Claude 和 GPT-4o 对中文业务口语的消化更好,llama 偶尔会把"涨了多少"理解成"增加了多少笔订单"而不是"金额增长百分比"。所以如果你也用中文提问,选模型时这块也得算进去,不能光看英文 benchmark。
十、踩坑清单:那些它"自信地错"的时刻
写到这里,我把这段时间攒下的坑集中列一遍,省得你们再交一遍学费。Select AI 最危险的地方不是"它不会",而是"它错了还特别自信",生成的 SQL 能跑、能出数,但数是错的——这种比直接报错更难发现。
坑一:列名同义歧义。 一个表里 salary 和 salary_conv 并存,模型随手抓错那个。解法是 comments 加 UNITS 标注,把业务口径钉死。
坑二:JOIN 漏表。 模型凭字段名能连上两张表就懒得拉第三张,结果漏了过滤条件。解法是 constraints: true ,把外键关系喂给它。
坑三:时间粒度跑偏。 我问"按月统计",它有时用 TRUNC(..., 'MM') ,有时用 TO_CHAR(..., 'YYYY-MM') ,还有一次直接按天没聚合。这类必须 showsql 核对,别想当然。
坑四:聚合函数混淆 NULL。 SUM 忽略 NULL, COUNT 不忽略,模型有时把"有效订单金额合计"写成 COUNT(amount) ,数字看着合理其实错得离谱。这种尤其要抽样对账。
坑五:引号与大小写。 Oracle 默认对象名大写,模型有时自作主张加小写引号,导致"表或视图不存在"。我们后来统一在 object_list 里用大写、注释里也强调,基本没了。
坑六:对话超限失忆。 conversation 记忆默认最多 10 条,超出就丢最早的,长链路分析到后面模型开始答非所问。解法要么每句带点前提,要么拆成小段。
坑七:跨源联邦的"想当然"。 文档里那个案例 ——把本地客户收入表跟远程 PostgreSQL 视图 JOIN 算"哪些高营收客户有严重工单、各区平均解决时长"——模型能写,但对远程视图的字段类型、编码差异特别敏感,第一次跑出来全是类型转换错误。这种跨源场景,我建议先把远程视图的字段类型在注释里写清楚,别让它猜。
坑八:showprompt 是最后的照妖镜。 当模型怎么问都不对,就 showprompt 看它实际拿到的提示词:表列齐不齐、注释有没有进去、约束在不在。我有一半的坑是靠它定位的 ——问题往往不在模型笨,而在我们喂的信息缺了哪块。
说到底,这些坑没有一个是因为 Select AI"不行",全是因为我们默认它"该懂"。它懂的是 SQL 和语言,不懂的是你这家公司"销售额"到底算不算退货、"入职日"到底看哪个字段。把业务语义补齐,坑就自己填平了。
十一、拿来就能用的问答样例集
前面几节是拆开讲原理,这节我把老王那阵子真正问过、且跑通的问题集中收一下。每个都标了自然语言问法、我用 showsql 核出来的 SQL、以及一句点评。复杂联表加统计分析的"长相",看这一节最直观。
样例 1:多表 JOIN + 分组聚合 问题:各产品类别的销售额总和,按销售额从高到低排。
SELECT p."CATEGORY", SUM(oi."AMOUNT") AS total_sales
FROM "SH"."ORDERS" o
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
JOIN "SH"."PRODUCTS" p ON oi."PROD_ID" = p."PROD_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY p."CATEGORY"
ORDER BY total_sales DESC;
点评: constraints: true 之后它不再漏 orders 表,有效订单过滤稳了。
样例 2:区域 × 月份 多维统计 问题:按大区和月份统计每个产品类别的销售额,只算已完成订单。
SELECT c."REGION",
TRUNC(o."ORDER_DATE", 'MM') AS month,
p."CATEGORY",
SUM(oi."AMOUNT") AS total_sales
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
JOIN "SH"."PRODUCTS" p ON oi."PROD_ID" = p."PROD_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY c."REGION", TRUNC(o."ORDER_DATE", 'MM'), p."CATEGORY"
ORDER BY c."REGION", month, total_sales DESC;
点评:四表 JOIN 加三层 GROUP BY, TRUNC 取月这步它自己想到了,记得核对取月粒度。
样例 3:窗口函数算同比 问题:各大区本月销售额,以及相比去年同期的增长率。
SELECT region, month, total_sales,
LAG(total_sales, 12) OVER (PARTITION BY region ORDER BY month) AS last_year,
ROUND((total_sales - LAG(total_sales, 12)
OVER (PARTITION BY region ORDER BY month))
/ LAG(total_sales, 12)
OVER (PARTITION BY region ORDER BY month) * 100, 2) AS yoy_pct
FROM (SELECT c."REGION" AS region,
TRUNC(o."ORDER_DATE", 'MM') AS month,
SUM(oi."AMOUNT") AS total_sales
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY c."REGION", TRUNC(o."ORDER_DATE", 'MM'))
ORDER BY region, month;
点评: LAG(..., 12) 取去年同期,按大区分区。这种写法模型能出,但务必 showsql 核对逻辑再上全量。
样例 4:区域内 Top N 排名 问题:每个大区里销售额最高的前三个产品。
SELECT region, category, total_sales
FROM (SELECT c."REGION" AS region,
p."CATEGORY" AS category,
SUM(oi."AMOUNT") AS total_sales,
ROW_NUMBER() OVER (
PARTITION BY c."REGION" ORDER BY SUM(oi."AMOUNT") DESC) AS rn
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
JOIN "SH"."PRODUCTS" p ON oi."PROD_ID" = p."PROD_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY c."REGION", p."CATEGORY")
WHERE rn <= 3
ORDER BY region, rn;
点评: ROW_NUMBER + PARTITION BY 做分组内排名,它一次写对。老王拿这个直接做了"区域销冠"榜单。
样例 5:占比分析 问题:每个产品类别的销售额占总销售额的比例。
SELECT p."CATEGORY",
SUM(oi."AMOUNT") AS cat_sales,
ROUND(SUM(oi."AMOUNT") /
SUM(SUM(oi."AMOUNT")) OVER (), 4) AS share
FROM "SH"."ORDERS" o
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
JOIN "SH"."PRODUCTS" p ON oi."PROD_ID" = p."PROD_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
GROUP BY p."CATEGORY"
ORDER BY share DESC;
点评:嵌套聚合 SUM(SUM(...)) OVER () 算全局分母,这种"组内聚合再除以总计"的写法它也想得通,挺让人意外。
样例 6:日期区间 + 趋势 问题:上季度华东区已完成订单的逐月销售趋势。
SELECT TRUNC(o."ORDER_DATE", 'MM') AS month,
SUM(oi."AMOUNT") AS total_sales
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
JOIN "SH"."ORDER_ITEMS" oi ON o."ORDER_ID" = oi."ORDER_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
AND c."REGION" = '华东'
AND o."ORDER_DATE" >= ADD_MONTHS(TRUNC(SYSDATE, 'Q'), -3)
AND o."ORDER_DATE" < TRUNC(SYSDATE, 'Q')
GROUP BY TRUNC(o."ORDER_DATE", 'MM')
ORDER BY month;
点评:"上季度"被它翻译成了 ADD_MONTHS(TRUNC(SYSDATE,'Q'),-3) 到本季初的区间,逻辑对,但这种相对日期最好再确认一下边界含不含当天。
样例 7:去重计数 问题:第一季度里有多少不同的客户下过已完成订单。
SELECT COUNT(DISTINCT c."CUST_ID") AS active_customers
FROM "SH"."CUSTOMERS" c
JOIN "SH"."ORDERS" o ON c."CUST_ID" = o."CUST_ID"
WHERE o."ORDER_STATUS" = 'COMPLETE'
AND o."ORDER_DATE" >= DATE '2026-01-01'
AND o."ORDER_DATE" < DATE '2026-04-01';
点评: COUNT(DISTINCT) 用得没问题,区间边界用开区间 < 4-01 避免跨天重复,这点它比很多人手写还讲究。
样例 8:跨源联邦查询 问题:哪些上季度营收超百万美元的客户,有严重级别的工单,各区平均解决时长是多少。
SELECT c."CUSTOMER_NAME",
r."REGION",
AVG(t."RESOLUTION_HOURS") AS avg_res_hours
FROM "SH"."CUSTOMER_REVENUE" c
JOIN "REMOTE_SUPPORT"."SUPPORT_TICKET_METRICS" t
ON c."CUSTOMER_ID" = t."CUSTOMER_ID"
JOIN "SH"."REGION_MAP" r ON c."REGION_ID" = r."REGION_ID"
WHERE c."LAST_QTR_REVENUE_USD" > 1000000
AND t."SEVERITY" = 'CRITICAL'
GROUP BY c."CUSTOMER_NAME", r."REGION"
ORDER BY avg_res_hours DESC;
点评:本地表和远程视图混 JOIN,这种场景对字段类型极其敏感,第一次跑报了一堆类型转换错,后来把远程视图字段类型补进注释才过。跨源能写,但别裸奔。
这一组样例看下来,你大概能感觉到 Select AI 在复杂查询上的"天花板"在哪:标准的多表 JOIN、分组聚合、窗口函数、排名、占比、去重计数,它都能稳定产出;真正需要小心的不是它"写不出",而是它"写得像那么回事但口径不对"。所以我一直强调, showsql 是它给你留的安全绳,别嫌麻烦,每次复杂问题都先拉出来看一眼。等你对它的套路熟悉了,简单问题可以放心 runsql ,复杂问题永远 showsql 先行 ——这条纪律,老王现在比我还严。
十二、它背后到底拼了什么提示词
前面一直在讲"怎么用",这节我想聊聊"它怎么做到的",因为这关系到你对它输出质量的判断——你得知道它手里有什么牌,才能猜到它什么时候会打错。
核心机制叫 Prompt Augmentation,中文叫提示词扩充。简单说,当你扔一句自然语言过去,Select AI 不会直接把这句话丢给大模型,而是先去数据库里把相关对象的 结构信息 捞出来,拼成一段详细的"背景说明",再连同你的问题一起发过去。捞的是什么?表名、列名、每列的数据类型、你写的注释、表与表之间的外键约束,有时候还有示例值。
这一步是整套能力的命门。它解释了两个绕不开的问题:为什么数据安全,以及为什么它比"裸问"准。
安全那面很好懂:发出去的只有"结构",一行真实业务数据都没离库。客户手机号、工资额、订单明细,全留在 Oracle 里,大模型根本没见过。这也意味着它生成的 SQL 是在库内执行的,结果也在库内,整个链路数据不出境。我们合规那边的同事就吃这一套,否则任何把数据外传的方案都过不了审。
准那面更有意思。大模型本身不懂你的库,你光说"查一下各区域销售额",它连你有几张表、销售额叫 AMOUNT 还是 SALES 都不知道,只能瞎编。但 Select AI 把字典递到它手边:告诉你"有张 ORDERS 表,里面 ORDER_STATUS 是状态, ORDER_DATE 是日期;有张 ORDER_ITEMS 表里 AMOUNT 是金额,通过 ORDER_ID 连 ORDERS "。有了这份字典,它就不用猜了,直接照着写。列的数据类型尤其关键——模型看到 AMOUNT 是 NUMBER ,才知道能 SUM ;看到 ORDER_DATE 是 DATE ,才知道能用 TRUNC 取月。我后来特意用 showprompt 看过一次拼好的提示词,里面把每张表的列、类型、注释、外键挨个列得清清楚楚,活像一份自动生成的数据字典。那一刻我才真正信它:它不是在表演聪明,是在拿着说明书干活。
注释和外键在这份"说明书"里是重头戏。注释给的是业务语义,比如"本列是美元工资,非本币";外键给的是连接路径,比如" ORDER_ITEMS.ORDER_ID 引用 ORDERS.ORDER_ID "。模型拿着这两者,既能选对列,也能写对 JOIN。回头看前面那些坑——漏 JOIN、抓错列——本质上就是这份说明书里少了对应的外键或注释,模型被迫靠猜。所以你每次 showprompt 看到提示词里某张表的注释是空的,就该警觉:这块语义它大概率会错。
还有个常被忽略的点:表越多, object_list 越该收着用。因为每张表都会往提示词里塞一段元数据,表一多,提示词就膨胀,噪声也跟着涨,模型反而容易在十几张表里迷路。把范围钉在业务真正用到的那几张,既是安全,也是让模型聚焦。我给老王配的 profile 常年只挂五六张表,他问啥都在这范围内,反而比"全库开放"准得多。
至于模型供应商对提示词长度的处理,也有差别。强模型(GPT-4o、Claude)能吃下更长的元数据、在更多表里保持不乱;默认 llama 在表少、约束清晰时表现不差,表一多或约束缺失就容易飘。这也呼应了前面"分档用模型"的建议:复杂查询别省那点调用费,切强模型,把长提示词交给更能消化它的脑子。
理解到这一层,你对 Select AI 的心态会变了:它不是一个"许愿机",而是一个"拿着你给的说明书干活的学徒"。说明书越清楚(注释全、外键齐、范围准),它干得越漂亮;说明书含糊,它再聪明也救不了。老王听完这节,把那句口头禅改了——以前他说"这玩意还行",后来他说"这玩意,得教"。
十三、把它接进日常:从临时问答到自动化
前面说的都是"有人问、它答"的临时模式。真正让这套东西在团队里扎根的,是我们把它从"对话"变成了"流程"。
第一步是把高频问题固化。老王每天早上的第一件事是看昨日经营概况,以前是我写好的存储过程出数,现在换成一条 Select AI 的 narrate 定时跑:凌晨把昨日各区域、各类目的销售额算出来,直接用 narrate 生成一段白话简报,推到部门群里。那段时间我琢磨出一个小技巧 ——固化问题比临时提问更讲究措辞,因为定时任务没有"我再解释一句"的余地,自然语言必须一次写准。比如"昨日各区域已完成订单的销售额,用一段话总结,指出增长和下滑最明显的区域",这句话我调了好几版才稳定,关键在把"昨日"“已完成”"增长下滑"这些口径钉死,别留模糊空间。
第二步是让业务自己维护注释。工具跑顺之后,瓶颈从"能不能出数"变成了"口径对不对",而口径藏在注释里。我拉着老王搞了个简单的注释规范:每张被纳入 object_list 的表,必须写明业务目的;每个容易混的列,必须标 UNITS 或 VALUES ;每个外键关系,确保库里真建了约束。老王后来把这套规范写进了他们分析团队的 onboarding 文档,新来的同事第一天就被要求给自己的表加注释。这件事让我挺感慨——技术落地到最后,拼的不是模型多强,而是业务方愿不愿意把"算账的规矩"显性化。
第三步是教业务怎么"问"。Select AI 对自然语言的容错比想象中好,但也不是 都行 。我给老王他们总结了三句口诀:说清范围( “华东区”“已完成订单”)、说清口径(“按美元”“按月”)、说清动作(“排名”“占比”“同比”)。含糊的"帮我看看销售情况"它也能答,但答出来的粒度未必是你想要的。老王有回问我:"为啥我问’卖得最好的’它给了销售额第一,我问’最火的’它给了订单笔数第一?"我看了眼,这就是 VALUES 和 UNITS 没标清楚导致的 ——"卖得好"在业务里是金额,"火"在口语里是销量,模型按字面猜了。补了注释之后,两个问法都稳定指向金额。这种"问法即口径"的训练,是业务团队用顺这套工具的隐形门槛。
最有意思的是角色反转。以前是老王拿着需求来找我写 SQL,现在他经常发来一句"你这个提示词写得不严谨,模型会理解偏",然后给我改自然语言。上星期他发了条消息:"我在给模型提问前,先自己把口径在脑子里过一遍,发现好多坑其实是咱们业务自己没定义清楚。"这句话点醒了我——Select AI 真正倒逼出来的,是团队把模糊的业务语言翻译成精确的数据语言。工具只是镜子,照出的是我们平时懒得说清的那些规矩。
到这一步,Select AI 在我这里的定位已经很清楚了:它不是来抢 DBA 饭碗的,它是把"提需求的人"和"数据"之间的那道墙拆了。老王们能自己拿到准确的数,我们这些搞数据库的,反而能从"人肉 SQL 生成器"里解脱出来,去管真正要紧的事——性能、安全、建模、治理。工具用得好,DBA 不是失业,是升维。
老王那杯美式,这回终于没凉在 my 工位上。他端着杯子回自己座位,顺手给我发了条消息:"下个月经营会,我打算直接用 narrate 出开场白。“我回了个"好”,然后默默给他那几张表又补了两条注释——毕竟,能让业务自己把活干漂亮,DBA 才腾得出手去干真正难的事。