公司、醫院、學校和政府每天都在累積資料,但「資料存在」不代表「每個人都能使用」。很多時候,我們知道自己想問什麼,卻不知道資料放在哪張表,也不知道該怎麼寫 SQL。
我的碩士論文研究的,就是如何讓使用者直接用自然語言提問,再由系統自動產生可以執行的 SQL。這項技術稱為 Text-to-SQL。
不過,在介紹我的方法之前,我想先從最基本的問題開始:資料庫到底長什麼樣子?SQL 又在做什麼?
先認識資料庫:表、欄位與資料列
關聯式資料庫可以想成一組彼此有關聯的試算表。假設一間商店有兩張表:
顧客表 customers
| customer_id | name | city |
|---|---|---|
| 101 | Amy | Taipei |
| 102 | Bob | Tainan |
訂單表 orders
| order_id | customer_id | amount |
|---|---|---|
| 9001 | 101 | 1200 |
| 9002 | 101 | 650 |
| 9003 | 102 | 800 |
直向的 name、city、amount 稱為欄位,描述資料具有哪些屬性;橫向的每一筆內容稱為資料列。整個資料庫有哪些表、欄位與型別,合起來稱為 Schema(資料庫結構)。
其中,customers.customer_id 是每位顧客不重複的編號,稱為主鍵。orders.customer_id 則指向顧客表中的同名編號,稱為外鍵。靠著這個關係,我們才知道訂單 9001 和 9002 都屬於 Amy。
真實資料庫當然比這複雜得多:可能有數十張表、數百個欄位,而且名稱常是縮寫。使用者只看到「顧客」和「訂單」,系統看到的卻可能是 cust_info、txn_hdr 和一大串編號。
SQL 是怎麼向資料庫提問的?
SQL(Structured Query Language)是操作關聯式資料庫的語言。若要找出住在台北的顧客,可以寫:
SELECT name
FROM customers
WHERE city = 'Taipei';
這三行可以直接讀成:
SELECT name:我要取得姓名。FROM customers:資料來自顧客表。WHERE city = 'Taipei':只保留城市為台北的資料。
如果問題改成「台北顧客總共下了幾筆訂單?」,就必須同時使用顧客表和訂單表:
SELECT COUNT(*)
FROM customers
JOIN orders
ON customers.customer_id = orders.customer_id
WHERE customers.city = 'Taipei';
這裡的 JOIN 是把兩張表連接起來,ON 則指定連接規則。最後用 COUNT(*) 計算符合條件的訂單數量。
SQL 很精確,但也因此不能只靠大概。表名、欄位、連接方式或條件值只要有一個選錯,語法即使可以執行,也可能得到錯誤答案。
Text-to-SQL 不只是把中文翻成英文
Text-to-SQL 希望把這段流程自動化。使用者只要問「台北顧客總共下了幾筆訂單?」,系統就應該自己找出相關資料表、建立連接、加入篩選條件,最後產生 SQL。
看起來很像翻譯,但真正的流程更接近:
理解問題 → 找到表與欄位 → 確認資料值 → 判斷表格關係 → 組成 SQL → 執行查詢
這也是為什麼「會寫出合法 SQL」和「能查到正確答案」是兩回事。大型語言模型可能很熟悉 SQL 語法,卻不可能事先知道每間公司的資料庫如何命名、資料如何編碼。
在真實資料庫中,常見困難大致有三類。
1. 問題和欄位名稱對不起來
使用者問「客戶住在哪裡」,資料庫裡可能同時有 city、district、address 和 branch_location。系統必須判斷問題真正指向哪一個欄位,這通常稱為 Schema Linking(結構對齊)。
2. 人的說法和資料庫儲存值不同
使用者說「高階信用卡」,資料庫可能存成 gold;使用者說「已完成」,系統裡也可能用數字 1 表示。模型理解句意還不夠,它也要知道資料實際怎麼存,否則條件寫得很合理,查詢結果仍可能是空的。
3. 不知道表格應該怎麼連接
一個問題常同時需要顧客、訂單、產品等多張表。若外鍵沒有完整記錄,或兩個看似相同的欄位其實代表不同概念,模型就容易使用錯誤的 JOIN。
前人的研究做到哪裡了?
早期的 Text-to-SQL 多半針對單一、固定的資料庫設計。模型在訓練時已經看過資料庫結構,因此問題相對單純,但也很難直接搬到另一個系統。
2018 年推出的 Spider 資料集改變了研究方向:測試時會出現訓練期間沒有看過的資料庫。模型不能只記住欄位名稱,而必須學會如何理解新的 Schema。之後的研究開始從不同角度降低錯誤,例如:
- IRNet 和 NatSQL 使用中間表示法,先把問題整理成較容易推理的結構,再轉成 SQL。
- BRIDGE 把資料庫中的實際值提供給模型,幫助它連結問題和欄位。
- PICARD 在生成過程中限制不合法的 SQL,減少語法錯誤。
大型語言模型出現後,研究重心進一步轉向提示設計與多階段推理。DAIL-SQL 會挑選適合的範例放進提示;DIN-SQL 把結構對齊、問題分類、SQL 生成和修正拆成多個步驟;CHESS 則使用多個模組,分別負責檢索、Schema 篩選、候選 SQL 與測試。
評估方式也越來越貼近現實。BIRD 使用較大型的真實資料庫,並加入資料值和領域知識;Spider 2.0 更進一步涵蓋企業資料流程、不同 SQL 方言、說明文件與多步驟操作。
在我整理論文時,BIRD 排行榜上的高資源方法大約已達 70% 到 80% 的執行準確率。不過,部分方法需要專門訓練模型、反覆呼叫大型語言模型,或一次產生許多候選 SQL 再挑答案。它們展示了很高的上限,但成本與重現門檻也不低。
因此,現在的問題不只是「能不能產生 SQL」,還包括:能不能在複雜資料庫中,用合理成本穩定地找到正確答案?
我的做法:保留全貌,只放大重要部分
我的研究提出 AAP-SQL。它的核心概念可以用地圖來比喻:系統仍保留整座城市的道路,避免迷失方向;但只把目的地附近的街道、地標和路口放大,不需要把每條巷子的照片都攤在眼前。
換成資料庫的語言,就是:保留所有表和欄位的基本結構,再只對最可能相關的欄位補充詳細資訊。
這點很重要。AAP-SQL 並不是把沒有選中的欄位全部刪掉。如果前面的檢索判斷錯誤,大型語言模型仍能看到完整的 Schema;被選為核心欄位的部分,則會得到更多資料值與關係線索。
整體流程如下:
- 先廣泛搜尋,再重新排序:系統先找出一批可能相關的欄位,再用針對 Text-to-SQL 訓練的排序模型,選出大約十個最值得注意的核心欄位。
- 查看少量真實資料:針對核心欄位,實際查詢最多三個不同的值。例如看到
gold、silver,模型就比較容易理解信用卡等級的儲存方式。 - 對齊問題中的明確文字:如果問題中出現像
"OWNER"這樣帶引號的值,系統會在文字欄位中進行不分大小寫的精確搜尋,確認它究竟存在哪一欄。 - 補充可能遺漏的連接線索:除了資料庫已宣告的外鍵,也比較欄位名稱、值的重疊程度和唯一性,找出可能但未正式標註的關係。這些只會作為
JOIN線索,不會被當成絕對正確的外鍵。 - 整理成容易閱讀的提示:完整 Schema、外鍵、核心欄位、抽樣值、找到的條件值、外部線索與使用者問題會分區排列,最後才交給大型語言模型產生 SQL。
其中,前兩步像是在大地圖上圈出重點;後兩步則告訴模型「資料實際長什麼樣」以及「道路可能怎麼接」。
用一個完整例子理解 AAP-SQL
論文中有一道問題,大意是:
有多少張高階信用卡,其處分權類型是
"OWNER"?
人類看到這句話,可能直覺以為只要查信用卡表。但系統實際上需要完成四個判斷:
- 找到信用卡資料所在的
card表。 - 從題目附帶的外部線索得知,「高階」在這個資料庫中對應
gold。 - 搜尋真實資料後,確認
"OWNER"存在於disp.type,而不是card.type。 - 使用
disp_id把card和disp兩張表連接起來。
最後產生的查詢概念如下:
SELECT COUNT(card.card_id)
FROM card
JOIN disp
ON card.disp_id = disp.disp_id
WHERE card.type = 'gold'
AND disp.type = 'OWNER';
這個例子正好說明 Text-to-SQL 的難處:自然語言只有一句,背後卻同時需要理解欄位、真實值與表格關係。AAP-SQL 的工作,就是在生成 SQL 之前,先把這些最可能影響答案的資訊找出來。
我怎麼評估它是否有效?
實驗使用 BIRD 開發集,共 1,534 個自然語言問題、11 個 SQLite 資料庫,所有主要比較都固定使用 Gemini 2.5 Flash-Lite。這樣可以盡量讓差異來自方法本身,而不是因為某個方法換了更強的生成模型。
我主要觀察兩個指標:
- 執行準確率:把生成的 SQL 真的送進資料庫執行,再比較結果是否和標準答案相同。它比只檢查 SQL 字串更合理,因為不同寫法也可能得到同一個正確結果。
- Token 用量:Token 是大型語言模型處理文字時使用的基本單位,一個中文字、英文單字或符號可能被切成一個或多個 Token。它不等於固定金額,但可以用來觀察呼叫成本與輸入負擔。
BIRD 的部分題目還提供 Evidence,也就是額外的領域線索。例如前面的題目會告訴系統「高階」對應 gold。表中的「基礎提示加外部線索」,代表直接把基本 Schema、問題和這些線索交給模型,沒有加入 AAP-SQL 的檢索與資料值處理。
| 方法 | 執行準確率 | 總 Token | 平均每題 Token |
|---|---|---|---|
| AAP-SQL | 62.58% | 3.32M | 約 2,164 |
| DIN-SQL | 61.08% | 47.32M | 約 30,848 |
| CHESS | 61.21% | 42.43M | 約 27,659 |
| 基礎提示加外部線索 | 57.76% | 2.19M | 約 1,427 |
62.58% 代表 1,534 題中有 960 題得到正確執行結果。相較之下,基礎方法答對 886 題;AAP-SQL 多答對 74 題,準確率提高 4.82 個百分點,但平均每題只從約 1,427 增加到 2,164 Token。
和流程較複雜的 DIN-SQL、CHESS 相比,AAP-SQL 的準確率在這次固定條件的本地重現中略高,總 Token 則分別減少 92.98% 和 92.18%。換句話說,它不是單純追求最低用量,而是希望在「太少資訊容易答錯」與「反覆推理成本太高」之間找到較好的平衡。
不過,這些準確率差異沒有進行統計顯著性檢定,因此不能只憑小幅數字差距宣稱某個方法在所有情況下都比較好。這裡比較能支持的結論是:在相同資料、相同模型與相同執行環境下,選擇性補充資訊確實呈現出有競爭力的準確率與明顯較低的 Token 用量。
哪些設計真的有幫助?
為了確認效果不是偶然,我也做了「消融實驗」:每次拿掉一個元件,再重新測試。它就像從機器上逐一拆下零件,觀察性能下降多少。
在有外部線索的條件下:
- 不查看核心欄位的真實資料值,準確率下降 1.95 個百分點。
- 不做問題文字與資料值的對齊,下降 1.69 個百分點。
- 把專門訓練的檢索器換成通用檢索器,下降 1.63 個百分點。
- 拿掉第二階段的重新排序,下降 1.24 個百分點。
- 只使用資料庫正式宣告的外鍵,不補充其他關係線索,下降 0.65 個百分點。
這些結果顯示,幫助模型的不是「把資料庫內容塞得越多越好」,而是能不能找出和當前問題有關的欄位、真實值與連接方式。
它還沒有解決哪些問題?
AAP-SQL 把錯誤題數從基礎方法的 648 題降到 574 題,但它仍不是完整答案。
首先,新增資訊不一定永遠帶來幫助。原本答錯的問題中有 143 題被修正,但也有 69 題原本正確、加入資訊後反而答錯。這表示檢索到的內容有時仍會干擾模型。
其次,目前最難的仍是同時牽涉多種判斷的問題。只需要修正欄位、文字值或 JOIN 的題目有改善,但同時需要複雜結構、篩選和關聯推理的錯誤,並沒有明顯下降。核心欄位檢索也仍有進步空間:選出的欄位能完整涵蓋標準答案所需欄位的比例只有 38.20%。因為系統仍保留完整 Schema,這不代表其餘題目一定無法回答,但顯示「找到所有關鍵欄位」仍然很困難。
另外,目前的文字值對齊主要處理帶引號的精確內容,還不擅長錯字、近義詞、未加引號的名稱、日期或單位轉換。實驗也集中在 BIRD 開發集、SQLite、單輪問題與一個主要生成模型;Token 統計只計算線上大型語言模型用量,不包含離線訓練、資料庫查詢延遲和系統維運成本。
結語
Text-to-SQL 的終點,不只是讓模型「寫得出 SQL」,而是讓沒有資料庫背景的人也能可靠地取得資料,同時讓系統成本維持在可接受範圍。
我的研究驗證了一個相對務實的方向:保留資料庫全貌,再把有限的資源用在最相關的欄位、真實資料值與表格關係上。 在這次實驗中,它比單純提示更準確,也比多階段方法節省大量 Token。
未來還需要改善模糊值對齊、核心欄位召回與複合查詢推理,並在更多資料庫、模型和企業環境中驗證。不過我相信,若要讓自然語言真正成為資料庫的入口,先幫模型找到「現在最需要知道的資訊」,會是很重要的一步。