絕大多數 Oracle 資料庫效能問題的根源在於資料庫設計不合理,而非記憶體配置、I/O 或 CPU 爭用等 DBA 維護層面的因素。除非面對無法變更的既有系統,否則我們應將焦點放在資料表設計本身的合理性上,這才是程式效能的關鍵決策點。合理的資料庫設計需考量三大面向:業務資料的邏輯表達方式、資料的物理儲存策略、以及索引的建立與選擇。本文將以一個訂單主從表實例為核心,深入剖析常見設計缺失,並示範如何透過 PowerDesigner 10 進行優化實作。
某開發人員設計訂單系統,包含主表 T_ORDER 與從表 T_ORDER_ITEM。主鍵 ORDER_ID、ITEM_ID 分別由序列 SEQ_ORDER_ID、SEQ_ORDER_ITEM 產生,從表透過 ORDER_ID 外鍵關聯主表。查詢需求為:
CLIENT + ORDER_DATE + IS_SHIPPED 查詢ORDER_DATE + IS_SHIPPED 查詢初始設計於 T_ORDER 建立複合索引 IDX_ORDER_COMPOSITE (CLIENT, ORDER_DATE, IS_SHIPPED),於 T_ORDER_ITEM 外鍵建立 IDX_ORDER_ITEM_ORDER_ID。雖 ER 關聯無誤,但存在四大優化空間:
ORDER_ID 重複值高,普通索引存儲冗余,宜改為壓縮型 B-Tree 索引。CLIENT 的第二種查詢條件,將導致全表掃描。Oracle 強烈建議至少建立兩個表空間分別存放表資料(如 APP_DATA)與索引資料(如 APP_IDX),並在物理層將其置於不同磁碟,以消除 I/O 競爭。獨立表空間亦允許針對資料與索引的不同特性設定專屬儲存參數(如 PCTFREE、BUFFER_POOL),並便於備份遷移時僅導出表資料,目標端重建索引即可。
-- 表資料存入 APP_DATA
CREATE TABLE T_ORDER (...) TABLESPACE APP_DATA;
-- 索引存入 APP_IDX
CREATE INDEX IDX_ORDER_ITEM_ORDER_ID ON T_ORDER_ITEM (ORDER_ID ASC) TABLESPACE APP_IDX;
Model → Tablespace...,新增 APP_DATA、APP_IDX 兩個表空間。Table Properties,切換 Options 頁,在 Tablespace 欄選擇 APP_DATA。Indexes 頁雙擊目標索引,於 Index Properties → Options 頁設定 Tablespace 為 APP_IDX。
圖 1 建立表空間

圖 2 指定表資料儲存表空間

圖 3 指定索引資料儲存表空間
PCTFREE=0、BUFFER_POOL=KEEP 等參數。惟過度細分會增加管理複雜度,需在效能與維護性間取得平衡。
序列產生的主鍵值嚴格遞增,導致 B-Tree 索引右側節點頻繁分裂,樹高快速增長,查詢 I/O 次數增加。反向鍵索引將鍵值位元反轉(如 1000 → 0001),使插入值分布隨機化,索引樹趨向對稱,降低樹高與 I/O。但反向索引不支援範圍查詢(BETWEEN、<、>),僅支援等值(=)與不等值(<>)比較。

圖 4 不對稱的 B-Tree 索引(「歪樹」)

圖 5 對稱的 B-Tree 索引
-- 先建立反向唯一索引
CREATE UNIQUE INDEX IDX_ORDER_ID ON T_ORDER (ORDER_ID ASC) REVERSE;
-- 再建立主鍵並指定使用該索引
ALTER TABLE T_ORDER ADD CONSTRAINT PK_ORDER PRIMARY KEY (ORDER_ID) USING INDEX IDX_ORDER_ID;
注意:索引建立語句必須先於主鍵建立語句執行。
Table Properties → Indexes 新增索引 IDX_ORDER_ID,選欄位 ORDER_ID,於 Options 頁勾選 Reverse。Keys 頁將主鍵更名為 PK_ORDER,雙擊進入 Key Properties → Options,指定 Index Name 為 IDX_ORDER_ID。Database → Generate Database... → Keys & Indexes 頁,取消 Primary Keys、Foreign Keys,僅勾選 Indexes 先生成表與索引腳本;執行後再勾選 Primary Keys、Foreign Keys 取消 Tables & Views 生成約束腳本。
圖 6 設定反向鍵索引

圖 7 為主鍵指定特定索引

圖 8 設定生成鍵與索引 SQL 選項
一筆訂單對應多筆明細,T_ORDER_ITEM.ORDER_ID 大量重複。普通 B-Tree 索引每筆重複值皆完整存儲,佔用空間大且查詢需讀取更多區塊。壓縮索引(COMPRESS)將相同前導欄位值壓縮為單一條目,後接所有對應 ROWID,同時節省空間與提升掃描效率。

圖 9 未壓縮索引存儲:重複值佔據大量空間

圖 10 壓縮索引存儲:消除重複值,關聯 ROWID 群聚
CREATE INDEX IDX_ORDER_ITEM_ORDER_ID ON T_ORDER_ITEM (ORDER_ID ASC) COMPRESS;
PowerDesigner:Table Properties → Indexes 新增索引,於 Options 頁勾選 Compressed。

圖 11 將索引指定為壓縮型
複合索引 (COL_1, COL_2, ..., COL_n) 僅當 WHERE 條件包含 COL_1 時才會被使用。本案例中,第二種查詢 ORDER_DATE + IS_SHIPPED 缺少前導欄位 CLIENT,故無法使用 IDX_ORDER_COMPOSITE,導致全表掃描。
驗證方式:SET AUTOTRACE ON 觀察執行計畫。
CLIENT + ORDER_DATE + IS_SHIPPED → INDEX (RANGE SCAN) OF 'IDX_ORDER_COMPOSITE'ORDER_DATE + IS_SHIPPED → TABLE ACCESS (FULL) OF 'T_ORDER'| 方案 | 做法 | 優點 | 缺點 |
|---|---|---|---|
| 方案一 | 分別建立 (CLIENT, ORDER_DATE, IS_SHIPPED) 與 (ORDER_DATE, IS_SHIPPED) 兩個複合索引 |
查詢效率最快 | CLIENT、ORDER_DATE 在索引中重複存儲,空間佔用大 |
| 方案二(採用) | 分別在 CLIENT、ORDER_DATE 建立單欄索引,IS_SHIPPED 基數極低(0/1)不建索引 |
空間節省,查詢效率略低但可接受 | 需依賴索引合併(AND-EQUAL)機制 |
-- 建立單欄索引
CREATE INDEX IDX_CLIENT ON T_ORDER (CLIENT ASC);
CREATE INDEX IDX_ORDER_DATE ON T_ORDER (ORDER_DATE ASC);
-- 查詢 CLIENT + ORDER_DATE + IS_SHIPPED
SELECT STATEMENT Optimizer=CHOOSE
TABLE ACCESS (BY INDEX ROWID) OF 'T_ORDER'
AND-EQUAL
INDEX (RANGE SCAN) OF 'IDX_CLIENT'
INDEX (RANGE SCAN) OF 'IDX_ORDER_DATE'
-- 查詢 ORDER_DATE + IS_SHIPPED
SELECT STATEMENT Optimizer=CHOOSE
TABLE ACCESS (BY INDEX ROWID) OF 'T_ORDER'
INDEX (RANGE SCAN) OF 'IDX_ORDER_DATE'
透過索引合併,兩種查詢均避免全表掃描,兼顧空間與效能。
本文透過簡單訂單案例,揭示資料庫設計中常被忽視的物理層面優化點。精益求精是優秀設計師的基本素養,設計層面的效能提升往往比編碼階段更具杠桿效應。其他關鍵優化方向尚包括:
資料庫效能調優博大精深,本文僅拋磚引玉,期望拋磚引玉,引導設計師重視物理設計細節,打造高效穩健的 Oracle 系統。