就算是最先進的車子,如果你加的是檸檬汽水而不是汽油,也會開不動。索引在 PostgreSQL 也是這樣,雖然很強,但要會用才行。來看看幾個跟索引有關的常見問題吧。
問題 1:索引太多
先複習一下前幾堂課的內容。如果你在一個表上加太多索引,PostgreSQL 每次都得維護這些索引,才能讓它們保持最新。這會直接影響到寫入、更新和刪除操作。因為每個索引都要更新、同步!
假設我們有個 students 表:
CREATE TABLE students (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(255) UNIQUE,
age INTEGER,
grade INTEGER
);
然後你決定「以防萬一」每個欄位都加個索引:
CREATE INDEX idx_students_name ON students(name);
CREATE INDEX idx_students_age ON students(age);
CREATE INDEX idx_students_grade ON students(grade);
現在想像你要插入一萬筆新資料。PostgreSQL 不只要把資料寫進表裡,還要更新這三個索引。如果資料量很大,寫入速度就會變慢,整個系統效能也會掉下來。
怎麼避免這種情況? 在建立索引前,先問自己兩個問題:
- 這個欄位有多常被用在篩選(
WHERE)、排序(ORDER BY)或分組(GROUP BY)? - 查詢真的會用到這個索引,還是最後還是會做全表掃描?
如果這兩題的答案都是「很少」或「幾乎不會」,那這個索引就沒必要。
問題 2:選錯欄位來建索引
在變化很少的資料上建索引,就像想把茶倒進已經封死的杯子裡:幾乎沒什麼用。如果一個欄位只有兩三個不同的值,PostgreSQL 很可能還是會直接全表掃描,不會用索引。
假設我們有個 courses 表:
CREATE TABLE courses (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
level VARCHAR(10) -- 只能是 'Beginner'、'Intermediate' 或 'Advanced'
);
然後你決定在 level 欄位建索引:
CREATE INDEX idx_courses_level ON courses(level);
但這個查詢:
SELECT * FROM courses WHERE level = 'Beginner';
可能不會用到索引,因為 PostgreSQL 會覺得直接掃整個表比較快,根本不用查索引。這在小表和變化很少的資料特別明顯。
所以索引比較適合用在高基數欄位(就是有很多不同值的欄位)。如果資料變化很少,建議用其他優化方法,比如表分割(partitioning)。
問題 3:過時的索引
有時候索引建了,後來卻忘記刪掉,即使已經沒人在用。就像桌面上的檔案,一開始只有兩三個,後來一堆,找東西還要花時間一個個看過去...是不是很熟悉?
假設我們為舊功能建了一個索引,後來查詢邏輯改了,又加了新索引。舊的索引沒人用,卻還佔空間、拖慢寫入。
為了避免這種情況,要定期檢查和分析索引。PostgreSQL 有個很方便的指標:
SELECT
relname AS table_name,
indexrelname AS index_name,
idx_scan AS total_scans
FROM
pg_stat_user_indexes
WHERE
idx_scan = 0;
這裡 idx_scan 代表這個索引被查詢用過幾次。如果是 0,就表示沒人在用,可以刪掉:
DROP INDEX idx_courses_level;
問題 4:常常被更新的欄位建索引
如果你在一個經常被更新的欄位上建索引,PostgreSQL 每次改動都要重建索引,這會大幅拖慢效能。
想像有個訂單資料表:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
status VARCHAR(20), -- 可能會改很多次(像是 "新訂單"、"處理中"、"已完成")
total NUMERIC(10, 2)
);
你為 status 欄位建了索引,想讓狀態查詢更快:
CREATE INDEX idx_orders_status ON orders(status);
但如果每筆資料的狀態都會改很多次,這個索引反而會拖慢效能。
要避免這種情況,別在常常變動的欄位建索引。如果真的需要索引,可以考慮用部分索引:
CREATE INDEX idx_orders_status_partial
ON orders(status)
WHERE status = '處理中';
這樣只有指定狀態的資料才會更新索引。
問題 5:在不必要的欄位加 UNIQUE 限制
唯一索引(UNIQUE)會自動建立來保證資料唯一。但如果沒必要唯一,這些索引只會增加負擔。
假設我們有個 log 表:
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
message TEXT,
created_at TIMESTAMP UNIQUE
);
如果每秒都插入上千筆資料,created_at 的唯一性會造成很大壓力。
要一切順利,只有真的需要唯一時才加 UNIQUE 限制。如果 created_at 不一定要唯一,可以改成一般索引:
CREATE INDEX idx_logs_created_at ON logs(created_at);
問題 6:錯誤使用複合索引
複合索引(multi-column indexes)很有用,當查詢會同時用到多個欄位來篩選或排序。但這種索引要建對,不然根本不會被用到。
假設我們有這個索引:
CREATE INDEX idx_students_name_grade ON students(name, grade);
這個索引會在查詢同時用到兩個欄位時被用到:
SELECT * FROM students WHERE name = 'Alice' AND grade = 90;
但這個查詢:
SELECT * FROM students WHERE grade = 90;
就不會用到這個索引,因為 name 是第一個欄位。
為了避免這種問題,複合索引要按照查詢最常用的順序來建。如果只會用到其中一個欄位,建議另外建單獨的索引。
實用小建議
監控索引使用情況。PostgreSQL 有個系統視圖 pg_stat_user_indexes,可以看到哪些索引有被用,哪些沒被用。
查詢和索引一起優化。爛查詢加索引還是爛查詢。
記得刪除不用的索引。過時的索引只會佔空間、拖慢寫入。
就這樣啦,朋友們!索引真的很強,但記住「能力越大,責任越大」。用索引要有意識,這樣你的資料庫就會像 SpaceX 火箭一樣衝超快!
GO TO FULL VERSION