查詢很慢,AI 說要加 index,我會先叫它看資料庫裡已經有哪些。之前查裝置搜尋,就遇過 trigram index 已經加了,SQL 還是在掃整張表。那再加一個之前,總要先知道原本的為什麼沒用到。

那次跟 AI 一起查,才看到 Django 4.2 的 icontains 把欄位包在 UPPER() 裡。原本的 trigram index 建在文字欄位上,查詢卻先把欄位轉成大寫,形式對不上。我們那次調整的是這個落差,下面只留搜尋條件:

WHERE UPPER(device_name::text) LIKE UPPER('%term%')

WHERE device_name ILIKE '%term%'

你的查詢能不能這樣改,還要看欄位型別、index 定義和原本的搜尋規則。

當時照實際資料表結構產生十四萬筆測試資料,在本機 PostgreSQL 13.23 測三個欄位一起搜尋,紀錄是從 111 ms 變成 0.34 ms,也用到了原本的 trigram index。差很多,但這是在測試資料上跑的,還不能說正式環境的 API 也快了這麼多。

沒找到當時完整的執行計畫,所以寫這篇時,我另外用 PostgreSQL 13.23、十四萬筆人工資料做了本機示範。兩種寫法找到的都是同樣的 32 筆。下面節錄的是這次的新結果,跟前面的歷史紀錄分開看。

修改前:

Gather Merge  (cost=5215.32..5224.41 rows=79 width=4) (actual time=384.911..395.064 rows=32 loops=1)
...
  Buffers: shared hit=2211
...
        ->  Parallel Seq Scan on demo_devices  (cost=0.00..4212.82 rows=79 width=4) (actual time=11.875..380.974 rows=16 loops=2)

修改後:

Sort  (cost=292.62..292.72 rows=42 width=4) (actual time=1.246..1.251 rows=32 loops=1)
...
  Buffers: shared hit=70
  ->  Bitmap Heap Scan on demo_devices  (cost=140.35..291.49 rows=42 width=4) (actual time=0.457..1.083 rows=32 loops=1)
...
        Heap Blocks: exact=32
...
        ->  BitmapOr  (cost=140.35..140.35 rows=42 width=0) (actual time=0.413..0.415 rows=0 loops=1)

我會先看 Parallel Seq Scan:原本是分工掃過整張表,逐筆確認有沒有符合搜尋條件。改完變成 Bitmap Heap Scan,先從 index 找出可能符合的位置,再集中讀取對應的資料區塊、確認結果。中間的 BitmapOr,是把三個欄位的 index 找到的位置合在一起,因為這次搜尋只要任一欄位符合就算。

再看讀了多少。Heap Blocks: exact=32 表示這一步碰了 32 個資料表區塊,不是 32 筆資料;這次結果也剛好是 32 筆,是兩回事。最上層的 shared hit 從 2211 降到 70,表示從 PostgreSQL 快取取得資料頁的次數少了很多,不能當成磁碟讀取次數。

兩邊找到的是同樣的 32 筆,改完少碰了很多資料,時間也縮短了。我會叫 AI 把這些差別指出來,光貼兩個時間還不夠。這次只找少量資料,index 很有幫助;如果大半張表都符合,直接掃表也可能比較划算。

原本那次修改還碰到另一個問題:其中一個平台的 IMEI,在資料庫裡其實是 text[],存的是一組文字。服務的 model 沒跟上,照一般文字欄位去查就報錯了。

所以現在它說改成 ILIKE 就好,我會再問:「你有去看資料庫裡的型別嗎?還是只看 model?」有權限和安全的測試環境,就叫它去查;沒有,就把實際 SQL、欄位型別和 index 定義補給它。少了哪一份資料,先講,不要照著 model 猜。

那個欄位後來用 EXISTS 和 unnest(),改成逐一比對陣列裡的值,搜尋才正常。這條查詢還沒用到 index,索引優化留待另外處理。我會叫 AI 分開列:這次修好了什麼,還剩什麼。

我還會叫它查:「改完,找到的資料會不會不一樣?」那次另外找 agent review,就發現自訂 lookup 在用運算式當搜尋值時,可能把裡面的 %、_ 當成萬用字元。原本只想找那個字,結果搜尋範圍變大了。有些 Unicode 字元的大小寫比對也不同,沒有想像中只是換個寫法。

所以我會叫它拿幾組正常和容易出問題的輸入,在隔離的測試資料庫跑修改前後的查詢,把結果攤開來比。不要只跟我說兩邊都是十筆,那十筆是不是同樣的資料?有排序、分頁的也一起看。如果本來就打算改搜尋規則,就說清楚哪些結果會變。

我也會問:「你量的是 SQL,還是整個 API?」SQL 快了,API 不一定跟著快。測的參數、資料和環境也要一樣,才有辦法比較。

現在 AI 說改好了,我會叫它把這三樣一起拿出來:

  • 修改前後的 SQL:用什麼參數、欄位是什麼型別、原本有哪些 index。
  • 修改前後的結果:比對實際資料,不只比筆數;包含 %、_ 的搜尋、排序和分頁也一起看。預期會改變的結果另外說明。
  • 實際執行計畫和時間:在哪裡測、用多少資料、跑了幾次,掃描方式、讀取量和時間差在哪。沒跑的就寫沒跑。

這些看完,我才知道下一步要改查詢、補 index,還是查別的地方。