National Taiwan University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

台灣大學 AQI 67 24°C PM2.5 11
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 16

資料庫最佳化

索引的設計、合適的資料型別與表格維護
Interaction實作 25 minute(s)

Learning Objectives

  • 用「書的索引」說明索引為什麼能加快查詢,說出 InnoDB 索引是 B-tree 結構
  • 判斷哪些欄位適合建立索引,說出索引的代價(寫入變慢、佔用空間)
  • 建立一般、唯一與複合索引,用 SHOW INDEX 觀察,並說明複合索引的最左前綴規則
  • 說明 InnoDB 依主鍵排列資料(叢集索引),以及覆蓋索引會改變沒有 ORDER BY 時列的順序
  • 依資料內容選擇合適的資料型別:整數大小、CHAR 與 VARCHAR、DECIMAL 與 FLOAT
  • 說明正規化與反正規化的取捨,使用 ANALYZE TABLE 與 OPTIMIZE TABLE 維護表格

查詢變慢的常見原因

表格只有幾十列時,怎麼查都很快;資料累積到幾十萬、幾百萬列,同一個查詢可能要好幾秒。常見的原因有:

原因 說明 本課與下一課的做法
沒有合適的索引 每次查詢都要從第一列讀到最後一列 本課:建立索引;下一課:用 EXPLAIN 確認查詢有沒有用上
資料型別太大 每一列佔的空間大,同樣的記憶體與磁碟能放的列變少 本課:選擇合適的資料型別
表格設計 要查一筆資料得連接很多張表格,或同一份資料存了很多份 本課:正規化與反正規化
查詢寫法 寫法讓索引無法使用 下一課:查詢最佳化
網路與硬體 傳回太多資料、伺服器記憶體不足 只取需要的欄位與列;調整伺服器設定(不在本課範圍)

這一課從資料庫的「設計」著手:索引、資料型別、表格結構與維護。下一課再從「查詢」著手。

索引像書後面的索引

一本 500 頁的書,想找「交易」出現在哪幾頁,有兩種方法:從第一頁翻到最後一頁,或翻到書後面的索引, 索引依筆畫或字母排好,很快就找到「交易:第 128、305 頁」,再直接翻過去。

資料庫的索引(index)是同一個想法:把某個欄位的值排好序另外存一份,每個值旁邊記著「這筆資料在哪裡」。 沒有索引時,WHERE name = 'Ben Smith' 只能逐列檢查整張表格,稱為全表掃描(full table scan); 有了 name 的索引,MySQL 在排好序的索引裡很快找到 Ben Smith,再去取那一列。

MySQL 8 預設的儲存引擎 InnoDB,索引是 B-tree(精確地說是 B+tree)結構:

  • 樹的每個節點存放一段排好序的值,從樹根往下比較,每一層都能排除大部分的資料
  • 資料量增加很多倍,樹也只會多一兩層,所以就算表格很大,找一個值也只需要讀少數幾個節點
  • 最底層的節點依序串在一起,所以範圍查詢(BETWEEN、>、LIKE 'B%')與排序也能利用索引

索引的種類:

種類 建立方式 特性
主鍵 PRIMARY KEY 建表時 PRIMARY KEY (欄位) 值不可重複、不可以是 NULL;一張表格只能有一個
唯一索引 UNIQUE CREATE UNIQUE INDEX 名稱 ON 表格 (欄位) 值不可重複,可以有多個 NULL
一般索引 INDEX CREATE INDEX 名稱 ON 表格 (欄位) 沒有限制,只為了加快查詢
複合索引 CREATE INDEX 名稱 ON 表格 (欄位1, 欄位2, …) 一個索引包含多個欄位
全文索引 FULLTEXT CREATE FULLTEXT INDEX 名稱 ON 表格 (欄位) 用在文章內容的關鍵字搜尋(第 09 課)

CREATE INDEX 名稱 ON 表格 (欄位) 和 ALTER TABLE 表格 ADD INDEX 名稱 (欄位) 效果相同。 外鍵欄位如果還沒有索引,MySQL 會自動替它建立一個;下面的範例裡,employee 的 departmentID 索引就是這樣來的。第 04 課建表時已經用過 PRIMARY KEY、UNIQUE 與 INDEX,這一課說明它們的原理與取捨。

建立索引,用 SHOW INDEX 觀察

SHOW INDEX FROM 表格 列出表格的所有索引,每個欄位一列。注意 Key_name(索引名稱)、Non_unique(0 表示值不可重複)、Seq_in_index(欄位在索引裡的順序)、Column_name。最後兩個查詢都沒有寫 ORDER BY,請比較它們列出的順序。

叢集索引與覆蓋索引

上面最後兩個查詢的順序不同,原因在 InnoDB 存放資料的方式:

  • 叢集索引(clustered index):InnoDB 的資料列本身就依主鍵排列,存在主鍵的 B-tree 裡。 所以 SELECT * FROM employee 讀出來的列,依 employeeID 由小到大。播放器右邊的表格也依主鍵排序,就是這個原因
  • 次要索引(secondary index,主鍵以外的索引):存的是「索引欄位的值+那一列的主鍵」。 用 idx_name 找到 Ben Smith 後,拿到主鍵 9842,再回主鍵的 B-tree 取出整列
  • 覆蓋索引(covering index):查詢需要的欄位全部都在某個索引裡(次要索引本來就帶著主鍵),MySQL 只讀索引就能回答, 不必回表格取資料,比較快。這時列的順序就是索引的順序

表格沒有主鍵時,InnoDB 會改用第一個所有欄位都是 NOT NULL 的唯一索引當叢集索引;連這種索引都沒有,就自動產生一個看不到的列編號。 所以每張表格都應該定義主鍵,而且主鍵最好短:每個次要索引都帶著一份主鍵,主鍵越長,所有索引都跟著變大。

第 06 課說過「沒有 ORDER BY 時列的順序不保證」,這裡就是實例:多建一個索引,同一個查詢的順序就可能改變。 程式需要固定順序時,一定要寫 ORDER BY。

複合索引與最左前綴

複合索引把幾個欄位依序組成一個索引,例如 (dept, grade):先依 dept 排序,dept 相同時再依 grade 排序, 就像電話簿先依姓排、同姓再依名排。

電話簿可以很快找到「姓陳的」,也可以很快找到「姓陳、名叫小明的」,但沒辦法直接找到「所有名叫小明的人」, 因為名字只在同一個姓之內才是排好的。複合索引也一樣,這叫做最左前綴(leftmost prefix)規則:

WHERE 條件 能不能用 (dept, grade) 索引
dept = 'CS' 可以,用到第一個欄位
dept = 'CS' AND grade = 2 可以,兩個欄位都用到
grade = 2 AND dept = 'CS' 可以,條件的書寫順序不重要,MySQL 會自己對應
grade = 2 不能用來查找,因為跳過了最左邊的 dept

所以設計複合索引時,最常單獨出現在條件裡的欄位放最前面。有了 (dept, grade),就不需要另外建 (dept) 的索引。 下一課會用 EXPLAIN 實際驗證這張表。

唯一索引除了加快查詢,也是一種資料規則:值重複時 INSERT 會失敗(ERROR 1062)。 學號、帳號、身分證字號這類不可重複的欄位,除了主鍵以外,都可以用唯一索引保護。

複合索引與唯一索引

執行前已建立 school 資料庫與 12 位學生的 student 表格(主鍵 studentID)。SHOW INDEX 裡,複合索引 idx_dept_grade 佔兩列,Seq_in_index 是 1 和 2。第二個查詢只要 dept、grade、studentID,三個都在 idx_dept_grade 裡(studentID 是主鍵,次要索引本來就帶著),所以列依 dept、再依 grade、再依 studentID 排列。

哪些欄位適合建索引

索引不是越多越好。每個索引都要另外佔空間,而且每次 INSERT、DELETE,以及修改到索引欄位的 UPDATE, MySQL 都要同時更新每一個相關的索引,索引越多,寫入越慢。

適合建立索引:

  • 經常出現在 WHERE 條件裡的欄位
  • 多表查詢中用來連接的欄位、外鍵(MySQL 會自動替外鍵建立索引)
  • 經常用來排序(ORDER BY)、分組(GROUP BY)的欄位
  • 不同的值很多的欄位,例如帳號、學號、姓名。索引能排除的資料越多,效果越好

不適合建立索引:

  • 很少出現在查詢條件裡的欄位
  • 不同的值很少的欄位,例如只有 0 和 1 的狀態欄位,一個值就對應一半的資料,用索引不比全表掃描快多少
  • 表格只有幾十、幾百列,全表掃描就很快
  • 非常頻繁更新的欄位,或大量新增、刪除的表格,要考慮維護索引的成本

建立索引之後,要用 EXPLAIN 確認查詢真的用上了(下一課)。用不到的索引只會拖慢寫入,應該刪除:

DROP INDEX idx_name ON employee;
ALTER TABLE employee DROP INDEX idx_name;      -- 效果相同

索引的欄位建好之後不能修改,要換欄位就刪除後重建。MySQL 8 可以改名,也可以先把索引設為看不見(invisible): 優化器不再使用它,但索引仍然存在、仍會被更新,觀察一陣子確定沒有查詢變慢再刪除;需要時隨時改回 VISIBLE,不必重建:

ALTER TABLE employee RENAME INDEX idx_name TO idx_employee_name;
ALTER TABLE employee ALTER INDEX idx_employee_name INVISIBLE;
ALTER TABLE employee ALTER INDEX idx_employee_name VISIBLE;

(播放器的直譯器沒有支援看不見的索引,這兩行請在真的 MySQL 8 上練習;SHOW INDEX 的 Visible 欄位會變成 NO。)

想一想:索引

1.

替 employee 的 name 建立索引之後,執行下面的查詢(沒有 ORDER BY),會得到哪一位?

CREATE INDEX idx_name ON employee (name);
SELECT name FROM employee LIMIT 1 OFFSET 1;

(員工依 employeeID 排列是 6651 Ajay Patel、7513 Nora Edwards、9006 Candy Burnett、9842 Ben Smith)

2.

student 表格有複合索引 (dept, grade)。下面哪一個條件不能利用這個索引查找?

3.

下面哪一個欄位最不值得單獨建立索引?

第 1 次作答

選擇合適的資料型別

資料型別越小,每一列越短,同樣大小的記憶體與磁碟頁(InnoDB 預設一頁 16 KB)就能放更多列,索引也越小。 原則是:選擇能正確存放資料的最小型別。

整數:依數值範圍選擇

型別 位元組 範圍(有正負號) UNSIGNED 範圍
TINYINT 1 -128~127 0~255
SMALLINT 2 -32768~32767 0~65535
MEDIUMINT 3 約 ±838 萬 0~16777215
INT 4 約 ±21 億 0~4294967295
BIGINT 8 約 ±9.2×10¹⁸ 0~約 1.8×10¹⁹

例如 1 到 10 的評分用 TINYINT UNSIGNED。超出範圍時,MySQL 8 預設的嚴格模式會直接拒絕(ERROR 1264),不會偷偷改成最大值。 數字型的代碼也要想清楚:學號、員工編號用 INT 比用 VARCHAR 小,比較也快; 但電話號碼、郵遞區號可能以 0 開頭,也不會拿來做加減,應該用字串。

CHAR 與 VARCHAR

  • CHAR(n):固定長度,存入時在右邊補空白,取出時把尾端空白去掉
  • VARCHAR(n):存多長就是多長,另外用 1~2 個位元組記錄長度,尾端空白會保留

原始課程建議「選固定長度的 CHAR,不用 VARCHAR、TEXT、BLOB」,這是 MyISAM 時代的做法: MyISAM 表格所有欄位都是固定長度時,每一列一樣長,讀寫比較快。MySQL 8 預設的 InnoDB 不靠這個加速, 而且 utf8mb4 的 CHAR 在 InnoDB 裡也不一定佔固定長度。現在的原則是:長度固定的代碼(州別 VA、國家代碼 TW)用 CHAR, 長度會變的文字(姓名、地址)用 VARCHAR,長度上限設成合理的值就好。

DECIMAL 與 FLOAT

  • DECIMAL(p, s):精確的十進位數,DECIMAL(8,2) 是共 8 位數、小數 2 位。金額一定用 DECIMAL
  • FLOAT(4 位元組)、DOUBLE(8 位元組):近似值,0.1 這種數在二進位裡無法精確表示,加總與比較會出現誤差。 適合科學量測這類本來就有誤差的資料

資料型別的差異

執行前已建立空的 shop 資料庫。先比較 FLOAT 與 DECIMAL:同樣存三個 0.1,加總與比較的結果不同。再看 TINYINT 的範圍與 CHAR、VARCHAR 對尾端空白的處理(用中括號把值框起來,才看得到空白)。

正規化與反正規化

第 03 課的正規化把資料拆成多張表格,每項事實只存一次:員工表格只存 departmentID,部門名稱存在部門表格。 好處是修改部門名稱只要改一列,不會出現同一個部門有兩種名稱的矛盾;表格也比較窄,通常有助於效能。

但查詢時要用 JOIN 把資料接回來,表格拆得越細(4NF、BCNF、5NF),一個查詢要連接的表格越多。 如果某個查詢非常頻繁、又要連接很多張表格,可以考慮反正規化(denormalization):刻意保留一些重複的資料,換取查詢速度。常見的做法:

做法 例子 代價
多存一個欄位 訂單表格多存「下單時的會員名稱」,列表頁不必連接會員表格 會員改名時,舊訂單的名稱要不要跟著改,要事先決定
存統計結果 文章表格多一個 reply_count,不必每次 COUNT(*) 回覆表格 新增、刪除回覆時要同步更新(程式裡處理,或用觸發程序 trigger)
拆開很寬的表格 上百個欄位的表格,把很少用的欄位移到另一張表格 用到那些欄位時要多一次 JOIN

反正規化是先量測、確定瓶頸之後才做的取捨,不是設計的起點:先做好正規化與索引,用 EXPLAIN 和慢查詢日誌找到真正慢的查詢,再決定。

維護表格:ANALYZE 與 OPTIMIZE

MySQL 決定要不要用某個索引時,依據的是統計資訊,例如 SHOW INDEX 的 Cardinality(不同值個數的估計)。 InnoDB 不是每次都數過所有資料,而是抽樣幾個資料頁估計;資料大量變動後,統計資訊可能跟實際情況差很多,導致 MySQL 選錯執行方式。

指令 作用 什麼時候用
ANALYZE TABLE 表格; 重新抽樣,更新索引的統計資訊 大量匯入或刪除資料之後;EXPLAIN 的估計列數明顯不合理時
OPTIMIZE TABLE 表格; 重建表格,回收刪除資料後留下的空間、整理索引 刪除了大量資料、表格檔案明顯比資料大時

InnoDB 預設會在一張表格大約一成的資料變動後自動重算統計資訊,所以平常不需要手動執行。 對 InnoDB 表格執行 OPTIMIZE TABLE 時,MySQL 會回應一則 note:Table does not support optimize, doing recreate + analyze instead, 意思是 InnoDB 用「重建表格再分析」的方式完成,這是正常的,最後一列的 status OK 表示成功。 重建大表格要花時間、佔用額外的磁碟空間,請在離峰時段執行。

執行 ANALYZE TABLE 與 OPTIMIZE TABLE

兩個指令都以表格形式回報結果:Table 是哪張表格,Op 是哪個操作,Msg_type 與 Msg_text 是訊息。最後建立再刪除一個索引,練習 DROP INDEX 的寫法。

想一想:資料型別與維護

1.

p 表格的欄位 f 是 FLOAT,存了三列 0.1。下面的查詢會得到什麼?

SELECT SUM(f) FROM p;
2.

欄位 a 是 TINYINT,在 MySQL 8 預設設定下執行 INSERT INTO p VALUES (300); 會怎樣?

3.

一張 InnoDB 表格剛刪除了一半的資料。關於 OPTIMIZE TABLE,哪一個說法正確?

第 1 次作答

動手寫:建立複合索引

公司常查詢「某個部門裡某種職稱的員工」,也常只依職稱查詢。請替 employee 建立複合索引 idx_job_dept,欄位是 job 和 departmentID(想一想哪個欄位放前面),再用 SHOW INDEX 列出 employee 的所有索引。

你的輸出
預期輸出

重點整理

主題 重點
索引 排好序的 B-tree,避免全表掃描;CREATE INDEX、DROP INDEX、SHOW INDEX
適合建索引 常用在 WHERE、JOIN、ORDER BY、GROUP BY,而且不同值多的欄位
索引的代價 佔空間;每次寫入都要更新所有索引
複合索引 最左前綴:(a, b) 可以查 a、a 和 b,不能只查 b
叢集索引 InnoDB 的資料依主鍵排列;次要索引帶著主鍵,所以主鍵要短
覆蓋索引 需要的欄位都在索引裡,只讀索引;沒有 ORDER BY 時順序會跟著索引改變
資料型別 能正確存放的最小型別;固定長度代碼用 CHAR;金額用 DECIMAL,不用 FLOAT
表格設計 先正規化,量測後才針對瓶頸反正規化
維護 ANALYZE TABLE 更新統計資訊;OPTIMIZE TABLE 重建表格回收空間

下一課用 EXPLAIN 看 MySQL 實際怎麼執行一個查詢,確認索引有沒有用上。

這一課完成了!
Next lesson:查詢最佳化
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。