AI 的資料庫查詢:為什麼它寫出慢查詢

artificial%20intelligence%20concept%2C%20digital%2...
發表時間:2026 年 10 月 28 日 | 更新日期:2026 年 10 月 28 日 | 編輯:雅寶社區編輯團隊
AI 的資料庫查詢:為什麼它寫出慢查詢 - 雅寶社區 · 頂客論壇

更麻煩的是,AI 很喜歡「把條件寫得漂亮」。例如它會寫出 WHERE DATE(created_at) = '2025-01-01',語意清楚、可讀性高,但在多數資料庫上這個函式會讓 created_at 上的索引無法走範圍掃描。正確又高效的寫法應該是 WHERE created_at >= '2025-01-01' AND created_at < '2025-01-02'。這種改寫需要對索引機制有意識,而不是對語意有理解。

統計資訊與基數估計:最佳化器的判斷依據,AI 一無所知

資料庫最佳化器在決定執行計畫時,靠的是統計資訊——某個欄位有多少不同的值(cardinality)、資料怎麼分布(histogram)、索引的選擇性如何。這些統計資訊會隨資料變動而變化,需要定期更新。AI 完全看不到這些數字,因此它無法判斷「這個條件會過濾掉 99% 的資料」還是「只過濾掉 1% 的資料」。

這個差異在實務上非常致命。假設有兩張表要做 JOIN,一邊過濾後剩 100 筆、另一邊剩 100 萬筆。若先掃大表再過濾,成本是百萬級;若先過濾小表再驅動,成本可能只有幾千次索引查找。最佳化器會根據統計資訊選擇後者,但如果 AI 寫的查詢結構把順序寫死了(例如用子查詢或 CTE 強制某種執行順序),最佳化器就沒有發揮空間。

資料量與熱冷分布:小表思維下的查詢,在大表上會爆炸

教學範例幾乎都是小表。幾百筆、幾千筆資料,怎麼寫都很快,於是「寫法」與「效能」之間的因果關係在學習階段完全被隱藏了。AI 的訓練語料大量來自這類範例,它自然會學到一種「小表思維」:直接全表掃描看起來沒問題、SELECT * 很方便、OFFSET 深分頁無所謂、相關子查詢很直覺。

但真實世界的大表會把這些習慣全部放大成災難。一億筆資料的全表掃描不是「慢一點」,而是「直接壓垮 I/O」。這就是為什麼 AI 寫的查詢在測試環境跑得飛快,上了生產環境卻變成慢查詢——測試環境的資料量,恰好就是它訓練語料裡的那種規模。

三、實戰盤點:AI 最常寫出的八種慢查詢模式

以下整理的是在實務 review 中出現頻率最高的幾種模式。它們未必是「錯的」,但幾乎都是「在真實資料量下會變慢」的。理解這些模式,比記住任何一句 prompt 都更有用。

模式一:讓索引失效的條件寫法

這是最常見、也最容易修的一類。AI 為了語意清晰,經常會在欄位上做運算或轉換,導致索引無法使用。

-- AI 常見寫法:索引失效

SELECT * FROM orders WHERE DATE(created_at) = '2025-03-01';

SELECT * FROM members WHERE phone = 0912345678; -- phone 是 varchar

SELECT * FROM products WHERE name LIKE '%無線耳機%';

SELECT * FROM orders WHERE total * 1.05 > 1000;

SELECT * FROM users WHERE YEAR(birthday) = 1990;

較佳寫法:保留欄位原樣,把運算移到常數側

SELECT * FROM orders

WHERE created_at >= '2025-03-01' AND created_at < '2025-03-02';

SELECT * FROM members WHERE phone = '0912345678';

SELECT * FROM orders WHERE total > 1000 / 1.05;

SELECT * FROM users

WHERE birthday >= '1990-01-01' AND birthday < '1991-01-01';

這些改寫不難,但需要「意識」。AI 在沒有被提醒的情況下,預設會選擇可讀性最高的寫法,而不是效能最好的寫法。前導模糊的 LIKE '%關鍵字%' 更是典型:它在語意上完全正確,在效能上等於全表掃描,除非資料庫支援全文檢索或 trigram 索引。

模式二:N+1 查詢與迴圈裡的小查詢

當你請 AI 寫「取得每位會員最近一筆訂單」,它很可能給你一段程式碼:先查出所有會員,再對每位會員發一次查詢。在 PHP、Python、Java 的 ORM 場景裡,這就是經典的 N+1 問題。單筆查詢只要 3 毫秒,一萬位會員就是 30 秒。

-- N+1:先抓會員,再逐筆查訂單

SELECT id, name FROM members WHERE status = 1;

然後在迴圈裡執行 N 次:

SELECT * FROM orders WHERE member_id = ? ORDER BY created_at DESC LIMIT 1;

較佳寫法:一次查完

SELECT m.id, m.name, o.id AS order_id, o.created_at, o.total

FROM members m

LEFT JOIN orders o ON o.id = (

SELECT o2.id FROM orders o2

WHERE o2.member_id = m.id

ORDER BY o2.created_at DESC

LIMIT 1

WHERE m.status = 1;

AI 不是不知道這個問題,但它在「生成程式碼」與「生成 SQL」之間的橋樑常常斷掉。它會分別寫出「正確的迴圈」與「正確的單筆查詢」,卻不一定會把它們合併成一個集合式查詢。這需要你在需求描述裡明確要求「避免 N+1」。

模式三:SELECT * 與過度取欄

AI 預設喜歡 SELECT *,因為它不確定你需要哪些欄位,索性全部給你。這在寬表(例如有 JSON 欄位、BLOB、長文本)上代價極高:資料庫需要讀取更多資料頁、網路需要傳輸更多位元組、應用層需要反序列化更多欄位。若查詢還搭配排序或暫存表,影響會再放大。

更隱蔽的問題是覆蓋索引(covering index)。當查詢只取索引裡有的欄位時,資料庫可以完全不回表;一旦寫成 SELECT *,這個最佳化機會就消失了。AI 不會主動幫你設計覆蓋索引,因為它不知道你的索引長什麼樣子。

模式四:深分頁 OFFSET

LIMIT 100000, 20 這種寫法在教學裡很常見,但在大表上,資料庫必須先掃過並丟棄前十萬筆,才能拿到那二十筆。AI 經常直接產出這種分頁查詢,因為它符合最直覺的「第 N 頁」語意。

-- 深分頁:掃描並丟棄前 100000 筆

SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

較佳寫法:以游標(keyset)分頁

SELECT * FROM orders WHERE id > 100000 ORDER BY id LIMIT 20;

這類問題在 API 分頁、後台列表、報表匯出中特別常見。AI 通常不會主動提出 keyset 分頁,除非你在需求裡說明「資料量很大、需要穩定效能」。

模式五:相關子查詢與重複聚合

AI 很愛用「相關子查詢」來表達業務邏輯,因為它讀起來最接近自然語言。例如「找出每位會員的訂單總金額」:

-- 相關子查詢:對每一列各執行一次

SELECT m.id, m.name,

(SELECT SUM(o.total) FROM orders o WHERE o.member_id = m.id) AS total_amount

FROM members m;

較佳寫法:一次聚合後 JOIN

SELECT m.id, m.name, COALESCE(t.total_amount, 0) AS total_amount

FROM members m

LEFT JOIN (

SELECT member_id, SUM(total) AS total_amount

FROM orders

GROUP BY member_id

) t ON t.member_id = m.id;

現代最佳化器有時會把相關子查詢改寫成 JOIN,但不保證每次都成功,尤其當子查詢裡還有聚合、排序或 LIMIT 時。這種查詢在資料量放大後,很容易變成「每列一次索引查找」的累加成本。

模式六:JOIN 爆炸與遺漏的關聯條件

多表 JOIN 時,只要漏掉一個關聯條件,就會產生笛卡爾積。AI 在生成多表查詢時,偶爾會漏掉某個 ON 條件,或把條件寫在 WHERE 而不是 ON(在 LEFT JOIN 下語意完全不同,效能也不同)。這類錯誤在測試資料上可能只是「多幾筆」,在生產環境就是「多幾億筆」。

另一種情況是「過度 JOIN」。AI 為了讓一段查詢能滿足所有需求,會把所有相關表都串進來,即使其中幾張表只是為了取一個名稱欄位。結果是 JOIN 的順序與方式變得複雜,最佳化器的估計誤差被放大,執行計畫變得不可預測。

模式七:ORDER BY 與 GROUP BY 沒有索引支撐

排序與分組是記憶體與暫存檔的大戶。當 ORDER BY 的欄位沒有對應索引時,資料庫必須先把結果集全部取出、排序、再回傳。AI 經常寫出「先過濾、再排序、再 LIMIT」的查詢,這在資料量小時完全沒問題,在大表上就變成 filesort 或外部排序。

GROUP BY 同理。對高基數欄位做分組,會產生大量暫存資料。AI 很少會提醒你「這個 GROUP BY 可能需要一個複合索引」或「這個聚合能不能改在應用層或預計算表做」。

模式八:忽略方言差異與鎖定行為

不同資料庫的方言差異,也會讓 AI 寫出「在 A 資料庫很快、在 B 資料庫很慢」的查詢。例如 CTE 在某些版本是最佳化屏障、UNION 與 UNION ALL 的差異、JSON 欄位的查詢方式、全文檢索的語法、視窗函式的支援程度。AI 通常會給出「通用」的寫法,而通用往往意味著沒有針對特定引擎最佳化。

此外,AI 寫交易時經常忽略鎖定範圍。SELECT ... FOR UPDATE 若沒有搭配適當索引,可能鎖住遠超預期的資料列,造成其他交易長時間等待。這種「慢」不是查詢本身慢,而是整體吞吐量被拖慢,卻同樣會出現在慢查詢日誌與監控儀表板上。

四、為什麼 AI 會「自信地」寫出慢查詢

理解了模式,還要理解成因。AI 不是隨便亂寫,它是「有理由地」寫出那些查詢的。這些理由,恰恰是我們在使用它時需要主動修正的地方。

訓練語料的偏差:教材範例都是小表

網路上流通的 SQL 範例,絕大多數是為了教學而設計的:員工表、部門表、訂單表,每張表幾十到幾千筆。這些範例的目標是「說明語法」,不是「示範效能」。AI 從中學到的,是「一種被廣泛接受的寫法」,而不是「一種在大資料量下仍可維運的寫法」。

這造成一個有趣的結果:AI 寫出的查詢往往「很標準」。標準意味著可讀性高、跨資料庫相容性好,但也意味著它不會為你的特定情境做取捨。它不會知道你的 orders 表有一億筆,所以它不會主動改用 keyset 分頁;它不會知道你的 status 欄位只有三種值,所以它不會提醒你這個條件的選擇性極低。

它被訓練成「追求正確」,而不是「追求快」

語言模型的優化目標,是生成「最可能被接受」的答案。在 SQL 的語境裡,「被接受」通常等於「語法正確、邏輯正確、結果正確」。效能不在這個目標函數裡。於是模型會傾向選擇那些「不容易出錯」的寫法——例如用子查詢把邏輯分層、用 DISTINCT 去重、用 LEFT JOIN 避免漏資料。這些寫法在正確性上是保守的,在效能上卻往往是昂貴的。

沒有回饋迴路,就不會自我修正

人類工程師寫完查詢後,會看執行計畫、會量測實際耗時、會根據慢查詢日誌回頭調整。這是一個閉環。AI 在多數使用情境下沒有這個閉環:它生成查詢、你複製貼上、跑完之後發現慢,於是你再問它一次。但這一次,它仍然不知道「上一次為什麼慢」,除非你把執行計畫貼給它。

這就是為什麼「把 EXPLAIN 的結果貼回給 AI」是一個效果極其顯著的技巧。一旦它看到 type: ALL、rows: 12000000、Using filesort,它就能給出完全不同的建議。它不缺知識,它缺的是觀測資料。

對話上下文的流失

還有一個實務上的細節:在長時間的對話中,AI 對早期提供的 schema 與約束會逐漸「淡忘」。當對話拉長到幾十輪,它可能開始假設一些不存在的欄位、忽略你先前說過的「這張表很大」。這不是模型的錯,而是上下文視窗與注意力機制的限制。解法很簡單:把關鍵約束寫成一段固定的「系統提示」,每次對話都帶上。

五、把 AI 從「寫得出」推進到「寫得快」

好消息是,這些問題幾乎都可以透過「提供更好的上下文」與「建立回饋迴路」來解決。AI 不是不會寫快查詢,它只是需要你把它缺的那一半資訊補上。

第一件事:把資料庫的「物理事實」寫進 prompt

不要只給欄位名稱,要給資料量、索引、約束與資料分布。這聽起來很瑣碎,但它對輸出品質的影響遠大於任何 prompt 技巧。

【資料庫環境】

  • 引擎:MySQL 8.0,InnoDB
  • orders 表:約 1.2 億筆,每日新增 30 萬筆
  • 索引:

PRIMARY KEY (id)

idx_member_created (member_id, created_at)

idx_status_created (status, created_at)

  • members 表:約 80 萬筆,PRIMARY KEY (id),UNIQUE (phone)
  • 資料分布:orders.status 只有 5 種值,其中 'completed' 佔 92%
  • 限制:禁止 SELECT *,禁止在索引欄位上使用函式,避免 N+1
  • 目標:查詢需在 200ms 內完成

把這段當成「資料庫履歷」固定附在對話開頭,你會發現 AI 的回覆風格明顯改變:它會開始使用範圍條件、會主動避開函式、會提醒你某個條件選擇性太低。它甚至會幫你判斷「這個查詢應該走哪個索引」。

第二件事:讓 AI 看執行計畫

這是最有效、卻最常被忽略的一步。當查詢變慢時,不要只說「太慢了,幫我優化」,而是把 EXPLAIN 或 EXPLAIN ANALYZE 的輸出貼給它,並附上實際耗時與資料量。AI 對執行計畫的解讀能力相當好,它能從 type、key、rows、Extra 這些欄位判斷問題出在哪裡。

EXPLAIN ANALYZE

SELECT m.id, m.name, o.total

FROM members m

JOIN orders o ON o.member_id = m.id

WHERE DATE(o.created_at) = '2025-03-01'

ORDER BY o.total DESC

LIMIT 20;

觀察重點:

type: ALL → 全表掃描

rows: 12000000 → 估計掃描列數過大

Using filesort → 排序未走索引

actual time: 38.2s → 與預估落差

把這段貼回去,AI 通常會立刻指出「DATE() 讓索引失效」與「ORDER BY 缺少支撐索引」,並給出改寫版本。這就是回饋迴路的力量:它把「文字生成」變成了「根據觀測結果修正」。

第三件事:用工具與規則約束,而不是只靠提示詞

提示詞是軟性的,工具是硬性的。成熟的團隊會把 AI 生成的 SQL 納入既有的工程流程:用 SQL linter 檢查禁用寫法(例如禁止 SELECT *、禁止前導模糊)、在 CI 中對高風險查詢跑 EXPLAIN、把慢查詢日誌與 APM 資料回饋給開發者。AI 只是流程中的一個環節,而不是流程的終點。

更進一步的做法,是把「查詢規範」文件化,並在每次請 AI 生成查詢時一併附上。這份規範不需要很長,但要具體:哪些寫法禁用、哪些索引必須走、哪些表禁止全表掃描、分頁一律使用 keyset。當規則被明文化,AI 的輸出就會自然收斂到你想要的形狀。

第四件事:要求 AI 自我檢查

在 prompt 結尾加上一句「請在給出 SQL 後,自行檢查是否會造成索引失效、是否需要額外排序、估計的掃描列數是否合理,並說明你的判斷依據」,效果往往出奇地好。這等於強迫模型在生成之後,用另一個角度審視自己的輸出。多數模型具備這個能力,只是預設不會主動啟用。

六、團隊實務:把 AI 生成的 SQL 納入工程流程

個人的使用技巧可以解決個案,但要讓整個團隊穩定受益,還是得回到流程。以下是一套可以在多數團隊落地的做法。

上線前的慢查詢檢查清單

查詢是否只取需要的欄位?有沒有 SELECT *?

WHERE 子句中的索引欄位,是否被函式、運算或隱式轉型包住?

  • 是否使用了前導模糊的 LIKE '%...%'?是否有全文檢索或 trigram 索引替代方案?
  • 複合索引的使用是否符合最左前綴原則?

    是否有多表 JOIN?每個 JOIN 是否都有明確且正確的 ON 條件?

    是否有相關子查詢?能否改寫為 JOIN 或預聚合?

  • ORDER BY 與 GROUP BY 是否有索引支撐?是否會產生 filesort 或暫存表?
  • 分頁是否使用深 OFFSET?能否改為 keyset 分頁?

    是否在應用層迴圈中逐筆查詢(N+1)?

    是否已用生產規模的資料量測過實際耗時,而不只是測試環境?

    交易與鎖定範圍是否明確?FOR UPDATE 是否走了索引?

    是否有對應的監控與告警,能在查詢退化時第一時間發現?

    建立「給 AI 的查詢規範」

    把上面這份清單轉成 AI 可讀的格式,會比人類可讀的版本更有效。例如用條列式的規則描述,並附上「正例」與「反例」。模型對範例的敏感度很高,一組好的正反例,往往比十條抽象規則更能改變它的輸出。

    【查詢規範】

    1. 一律明確列出欄位,禁止 SELECT *。
    2. 索引欄位禁止包函式。反例:DATE(created_at) = ?;正例:created_at >= ? AND created_at < ?。
    3. 字串欄位比較時,常數必須加引號,避免隱式轉型。
    4. 禁止前導模糊 LIKE '%x%',除非確認有全文檢索索引。
    5. 分頁一律使用 keyset(WHERE id > ? ORDER BY id LIMIT ?)。
    6. 禁止在迴圈中執行查詢,請改寫為集合式查詢。
    7. 多表查詢請確認 JOIN 條件完整,避免笛卡爾積。
    8. 產出 SQL 後,請自行說明預期的執行計畫與可能的效能風險。

    監控與回歸:讓慢查詢無所遁形

    再好的事前檢查,也無法完全取代事後監控。建議至少做到三件事:開啟慢查詢日誌並設定合理閾值、用 APM 追蹤每個查詢的 P95 與 P99、把高頻查詢加入回歸測試並在資料量成長時重新量測。當你發現某段 AI 生成的查詢開始變慢,第一時間把執行計畫與資料量變化回饋給模型,往往能在幾分鐘內得到修正版本。

    更重要的是建立一種文化:AI 生成的 SQL 不是「完成品」,而是「初稿」。初稿需要 review、需要量測、需要在真實資料上驗證。把它當成一位寫得很快但看不到執行環境的 junior 工程師,你會用完全不同的方式與它協作——你會給它更多上下文、會檢查它的產出、會在它卡住時提供觀測資料。這正是讓它從「寫得出」進化到「寫得快」的關鍵。

    結語:慢查詢不是 AI 的錯,是資訊落差的錯

    回到最初的問題:AI 為什麼會寫出慢查詢?答案其實不複雜——它在一個沒有索引、沒有統計資訊、沒有執行計畫、沒有資料量的真空環境裡生成文字,而效能恰恰是這些被抽掉的維度所決定的。它寫出的是「語意正確的 SQL」,而不是「在你這台資料庫上跑得快的 SQL」。這兩者之間的距離,需要由我們用上下文、用工具、用回饋迴路來補上。

    值得強調的是,這個落差正在快速縮小。具備工具呼叫能力的模型已經可以直接連上資料庫執行 EXPLAIN、讀取統計資訊、甚至在沙箱環境中量測耗時。當 AI 從「只能讀文字」變成「能觀測執行結果」,它寫出慢查詢的機率會大幅下降。但在那一天全面到來之前,最務實的做法仍然是:把它當成一位能力很強、但對你的系統一無所知的協作者,然後把它的眼睛補上。

    在雅寶社區·頂客論壇這類技術社群裡,我們經常看到兩種極端:一種是把 AI 當成萬能顧問,貼上需求就上線;另一種是徹底不信任,認為 AI 寫的 SQL 一定不能用在生產環境。事實往往在中間——AI 能大幅壓縮「從需求到初稿」的時間,但「從初稿到可上線」這一段,仍然需要人對資料庫的物理現實保持理解。慢查詢不是 AI 的失敗,而是提醒我們:真正決定效能的,從來不是語法,而是對資料的理解。

    下一次當你請 AI 寫查詢時,不妨多花三十秒,把資料量、索引、限制條件一併貼上。這三十秒,往往就是三十秒與三十毫秒之間的差別。

    🏠 返回首頁