# 用 GROUP BY ROLLUP 與純量子查詢消除 text-to-SQL 智能體的文字計算 — 禁止規則擋不住的加總與扇出誤差

> text-to-SQL 智能體違反了「不可手動加總」的規則。GROUP BY ROLLUP 與純量子查詢不是修規則，而是直接消除了這個步驟。

- Source: https://oharu121.com/zh-tw/blog/text-to-sql-rollup-scalar-subquery-fan-out/
- Published: 2026-08-28T07:57:42+09:00
- Tags: Text-to-SQL, LLM, AWS

---
**重點摘要**

- 禁止規則要求模型每一次執行都要遵守；而一種無法表達出錯誤的查詢形狀，則完全不需要遵守。
- 手動加總不是不服從命令。是兩條提示規則無法同時滿足，模型只是默默選了其中一條。
- `GROUP BY ROLLUP` 會在一個結果裡同時回傳小計與總計，兩層回答因此不再需要相加。
- 對於只涉及單一實體的比率，扇出的修正方式是乾脆不要 JOIN：兩支純量子查詢沒有任何列可以被重複相乘。
- 有兩個變更被實測否決：一個只會拉高基準分數，另一個則讓三個原本通過的問題失敗了。

## 引言

我們的 text-to-SQL 智能體回報的總計約為 71.2 萬，而正確數字約為 61.2 萬。它執行的 SQL 是正確的，回答的格式也沒問題，也沒有出現任何錯誤。它取得了十列資料，在回覆的文字中自行相加，結果在十萬位上錯了一位數。

這件事之所以值得深究，是因為系統提示早就禁止了這種行為。規則就寫在一個叫做「用 SQL 做計算」的段落裡，附有一個錯誤示範和一個正確示範。**一條附有範例的規則，還是被打破了**，而通常會有的反應，也就是把規則寫得更嚴，當初寫下這段規則的人早就試過了。

真正的修正並不是更好的禁止規則，而是改變了智能體所寫查詢的兩種形狀，讓錯誤從此無處發生：合計改用 `GROUP BY ROLLUP`，JOIN 則改用純量子查詢取代。本文涵蓋這兩個失敗、兩個修正，以及另外兩個被實測否決的變更。

## 禁止規則早就存在

提示中相關段落的內容大意是：絕不在回答文字裡進行計算。比率、百分比、單位換算，以及**小計**，全都應該在 `SELECT` 子句中完成，寫下的數字必須就是查詢回傳的數字。

裡面附了兩個範例。一個示範了錯誤的做法：從兩個分別取得的數字，在文字中計算出單位平均值。另一個示範了另一種錯誤：把明細用手動方式相加，*1,951 + 278 = 2,229*。

**而第二個範例，幾乎就是後來實際發生的那個失敗。**

這排除了所有討喜的解釋：規則不是不存在，不是含糊不清，也不是埋在一大段沒有範例的文字裡。模型明明被告知了規則，也看過錯誤版本和正確版本，卻還是做出了錯誤的版本。

## 失敗一：在文字中把三列相加

問題要求的是全國總計。提示中另一處的慣例規定，這種總計必須分兩層回報：先是合併子公司的合計，接著是納入權益法適用公司的數字。

智能體執行了一支查詢：

```sql
SELECT company_group, consolidation, stock_count
FROM stock
WHERE fiscal_year = 2026
```

結果返回了十列。它在回覆中把這些數字相加，結果錯了一位數。

智能體同時做到了兩件事：完全符合兩層報告的規定，也完全違反了計算禁止規則。原因是它寫的查詢無法同時滿足這兩者。

*Figure — RuleConflict: 兩條規則本身都沒有錯。但兩者合在一起，描述的是一支智能體根本無法寫出的查詢，於是它違反了不會報錯的那一條。*

**回傳明細的查詢，回傳的是部分，不是整體。** 要回報第二層數字，智能體需要一個沒有任何儲存格裝著的數值，它的選項只有兩個：再執行一支查詢，或是自己把列相加。它選了自己相加。

把這件事讀成「不服從」，會指向錯誤的修法。**規則不是被忽略，而是在那個位置上根本無法同時滿足**，不管把規則重寫幾次，那個位置本身都不會改變。

## GROUP BY ROLLUP：總計以一個儲存格的形式返回

`GROUP BY ROLLUP` 是智能體提出的，我採用了這個做法。Presto 與 Trino 都支援它，所以在 Athena 上不用修改就能執行：

```sql
SELECT COALESCE(consolidation, 'total') AS tier,
       SUM(stock_count) AS stock_count
FROM stock
WHERE fiscal_year = 2026
GROUP BY ROLLUP(consolidation)
ORDER BY 1
```

```text
tier             stock_count
consolidated         524,000
equity_method         88,000
total                612,000
```

兩層回答所需的兩個數字，現在都成了現成的儲存格。**相加不是被勸阻，而是這個步驟本身已經不存在了。** 智能體只要引用這兩個數值就能完成回答。

提示端的變更，是在描述兩層回答的段落裡加入一個附範例的樣板，再加上一句話把限制收窄：你可以用兩支個別的查詢分別取得小計與總計，唯一被禁止的是在回覆文字中把明細的列自行相加。與其強制規定一支「唯一正確」的查詢，不如只點名一個禁止的動作。這樣既留給智能體空間，也堵住了這個失敗。

變更之後，測試套件中需要兩層回答的四個問題各執行了三次。**十二次全部通過**，原本失敗的那一題，三次都採用了 `ROLLUP` 樣板，每次只需一次工具呼叫，比原本的四次少了許多。

## 失敗二：一列 JOIN 到三列，營收變成三倍

第二個失敗來自一個人均比率的問題：某個事業部門一年的營收，除以某個時間點該集團的業務人力。營收存放在按月更新的 `financials` 資料表，人力則存放在按月更新的 `headcount` 快照中。

智能體正確地把營收彙總成一列，接著把它 JOIN 到人力資料表：

```sql
SELECT SUM(r.revenue) / NULLIF(MAX(p.sales_staff), 0)
FROM (
  SELECT company_group, SUM(revenue) AS revenue
  FROM financials
  WHERE fiscal_year = 2025 AND segment = 'construction'
  GROUP BY 1
) r
JOIN headcount p ON r.company_group = p.company_group
WHERE p.fiscal_year = 2025 AND p.month = 3
```

這個集團底下有三家公司，所以 `headcount` 回傳了三列。原本只有一列的營收，對應到這三列時各自被複製了一份，`SUM` 因此把它算了三次：原本約 21,000 的數字，變成了約 63,000，比率也隨之被放大成約三倍。

*Figure — FanOut: 人力那一欄無論怎麼看都是正確的。錯的只有被複製的那一側，而這正是這個問題難以被發現的原因。*

有兩個細節，讓這個失敗比第一個更棘手。**人力數字是對的**，因為三家公司裡有兩家的業務人力是零，所以 `SUM` 和 `MAX` 剛好一致；半對半錯的答案，比起徹底錯誤的答案更不容易引起懷疑。而且它是間歇性的：針對這一題觀測 14 次，失敗只出現過一次，所以單次通過根本無法證明任何事。

提示中原本就有一段講扇出的內容，也早就規定了標準的處理方式：在 JOIN 之前先用子查詢把兩邊都彙總好。就像計算禁止規則一樣，指引明明存在，失敗還是照樣發生了。

## 純量子查詢：單一實體時完全不要 JOIN

解決這個問題的關鍵發現是：**這支查詢本來就不需要 JOIN。** 問題只針對一個集團。分子是一個數字，分母也是一個數字，而 JOIN 是用來比對集合的機制：

```sql
SELECT ROUND(
  (SELECT SUM(revenue) FROM financials
    WHERE company_group = 'north' AND fiscal_year = 2025 AND segment = 'construction')
  / NULLIF((SELECT SUM(sales_staff) FROM headcount
    WHERE company_group = 'north' AND fiscal_year = 2025 AND month = 3), 0)
, 2) AS revenue_per_head
```

**每一支子查詢都精確地只回傳一個值。** 既然沒有列可以被複製，**扇出在這裡不是被避免，而是根本無法表達出來。** 這與子查詢先 JOIN 的做法所提供的保證不同：後者雖然正確，但仍然仰賴智能體每次都確實把兩邊彙總好。

改寫後的段落，把這種寫法列在單一實體比率的最前面，並把 JOIN 這套模式降格為它真正該用的場合：比較或排名多個群組。這種情況下才真的需要 JOIN，而 `GROUP BY` 自然會把智能體導向把兩側都彙總好。

變更之後，測試套件中十一個比率相關的問題全數通過。**在八個單一實體的問題裡，有五個完全拿掉了 JOIN。** 剩下三個保留 JOIN 的問題，是針對年度快照用 `MAX` 來 JOIN，而 `MAX` 不受重複列影響，因為重複的列不會改變最大值。**危險的組合，也就是跨多列 JOIN 的 `SUM`，不再出現在產生的 SQL 裡。**

*Figure — ProhibitionVsShape: 左欄必須在每一次執行時都成立。右欄則根本沒有什麼需要成立的東西。*

## 只會拉高基準分數的修正

在這兩個提示變更之前，智能體其實已經實作了另一個做法：在 SQL 工具背後的 Lambda 裡計算欄位總計。任何多列的結果，都會自動附上可加總數值欄位的加總值，這樣一來，總計就永遠是一個可以直接引用的值，而不是需要計算的加總。這個做法還附上了八個單元測試，涵蓋要排除的欄位型別、單列結果，以及非數值儲存格。

但它在部署前就被撤回了。查證助理平台實際上如何執行 SQL 之後發現，查詢工具其實是一個**平台內建的功能**，靠一個旗標和一個資料庫名稱來設定，直接執行 Athena。**那個剛改良完的 Lambda，其實只有我們的基準測試環境才能碰到。**

*Figure — WhereFixLives: 第一個修正所鎖定的那一層，根本不在正式環境會走的路徑上。*

如果照樣出貨，結果會是正式環境完全沒有改變，基準分數卻上升了。**因為測量工具本身改善而變好的測量結果，比完全不測量還糟**，因為它回報了根本沒有發生的進步。發現這件事之後，我裁定變更範圍只限於主提示與審查器，上面提到的兩種寫法就是在這個限制下做出來的。

## 讓情況變得更糟的變更

另一個被否決的變更同樣值得記錄，因為它顯示這套方法不只能確認假設，也能推翻假設。

有幾個失敗都跟把日曆月份換算成會計年度有關，於是智能體把計算規則換成一張查表，明確列出範圍內每一年的對應關係。**移除計算這個做法才剛在兩個地方成功，把這個想法延伸出去，看起來顯然是對的。**

這個變更針對 13 個與日期相關的問題，測了四輪。**原本一直穩定通過的三個問題開始失敗**，而且都錯在同一個年份上，其中一個問題甚至在 SQL 註解裡寫著正確的年份，卻在 `WHERE` 子句裡放了另一個年份。可能的原因是互相干擾：提示現在用兩張重疊的表格描述一到三月的對應關係，模型要比對的不再是一條該套用的規則，而是兩個互相競爭的樣式。

還原之後，之前的行為就恢復了，並在受影響的三個問題上連續驗證了 18 次通過。**產生兩個好變更的同一種推論，也產生了一個壞變更**，能分辨兩者的只有實測。

## 總結

套用這些變更之後的那一輪執行，測試套件在 57 題中全數通過，計算與扇出相關的失敗都消失了。總共嘗試了四個變更，最後保留了兩個：

| 變更 | 結果 |
| --- | --- |
| 兩層總計改用 `GROUP BY ROLLUP` | 保留。三輪共 12/12，每次都採用了樣板 |
| 單一實體比率改用純量子查詢 | 保留。11/11，危險的 JOIN 形狀不再產生 |
| 在 SQL 工具裡計算欄位總計 | 撤回。正式環境不會經過那個工具 |
| 日期換算改用查表 | 還原。破壞了三個原本通過的問題 |

可以推廣的部分，在於這兩個保留下來的變更，與它們所取代的規則之間的差異。**禁止規則要求的是每一次未來執行都要遵守；而一種無法表達出錯誤的查詢形狀，則什麼都不要求。** 當一條附有範例的規則還是被打破時，再加第三個範例通常是最弱的做法。更有效的問法是：模型該改寫成什麼樣子，以及能不能讓這個失敗在那個位置上根本無法表達出來。

還有兩件比較小的事值得留下來。第一，在假定一條規則被忽略之前，先確認它在被打破的那個位置上是不是*可以同時滿足*的。這裡的第一個失敗，其實是兩條正確規則之間真正的衝突。第二，不要只信任套件的總分，要針對每個問題重複執行來測量：一個大約 14 次才出現一次的間歇性失敗，在單次執行中是看不見的，而正是重複測量，才同時抓出了那個毫無幫助的變更，以及那個造成傷害的變更。

由於這是人機協作完成的工作，這裡記錄一下判斷落在誰身上：診斷規則衝突、提出兩種機制、寫出提示變更、並發現 Lambda 不在正式路徑上的，都是智能體。在那個發現之後設定範圍、決定不要求證明效果就先出貨扇出的修正、並拍板最後保留哪些變更的，是我。**智能體提出的方案本身很好，但它對修正該放在哪裡的第一直覺是錯的。** 這大概就是該預期的分配方式。

## 參考連結

- [Presto 的 GROUP BY 文件，涵蓋 ROLLUP 以及它展開後的 GROUPING SETS](https://prestodb.io/docs/current/sql/select.html#group-by-clause)
- [Amazon Athena 的 SQL 參考文件，沿用了 Trino 的 GROUP BY 擴充功能](https://docs.aws.amazon.com/athena/latest/ug/ddl-sql-reference.html)
- [一份針對 in-context-learning text-to-SQL 錯誤的研究，將聚合結構不一致列為獨立的錯誤類型](https://arxiv.org/html/2501.09310v2)
- [大型語言模型智能體可以用工具完成臨床計算，談的是把計算從模型手中移交出去](https://www.nature.com/articles/s41746-025-01475-8)
