核心原理:行越窄,性能越強
MySQL 按行存儲,IO 以頁(16KB)為單位。單行體積縮小 → 每頁容納行數增加 → 單次 IO 訪問行數增加 → 物理 IO 次數減少、緩存命中率提升。這是所有表結構優化的物理基礎。
法則一:精準選擇數據類型(存儲空間的「瘦身術」)
1. 數字類型
- 拒絕 DOUBLE:精度陷阱、存儲佔用大。定點數用
DECIMAL 或 乘固定倍數轉 BIGINT(如金額分為單位存儲),零精度損失、極省空間。
- 整數分級:大表嚴格區分
TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT,能用小類型絕不用大類型。
- 非負加 UNSIGNED:正數範圍翻倍,無額外成本。
2. 字符類型
- 拒絕 TEXT/BLOB:處理路徑特殊、不走內存臨時表、排序落盤、極大拖慢查詢。改用
VARCHAR(3000+) 或垂直拆分。
- 定長用 CHAR:MD5、UUID、代碼類,固定長度處理更快、不產生碎片。
- 變長用 VARCHAR(N):
N 按實際業務最大長度設定,切勿隨意給大值(影響排序緩衝、臨時表內存估算)。
3. 時間類型
- 首選 TIMESTAMP:4 字節,
DATETIME 一半大小,自帶時區轉換。
- 精確到天用 DATE:僅 3 字節,比 TIMESTAMP 更省。
- 拒絕 INT 存 Unix 時間戳:不可讀、維護地獄、無任何收益。
4. 枚舉與集合
- 狀態字段用 ENUM:內部存數值,1-2 字節,極省空間。末尾新增值只需元數據鎖,秒級完成。
- 多屬性標籤用 SET:位掩碼存儲,1-8 字節支援 64 個屬性組合。
5. LOB 類型
強烈反對在數據庫存 LOB(圖片、文件、大文本)。數據庫不擅長此道,請用對象存儲(S3/OSS/MinIO),庫中只存路徑/元數據。這如讓營銷專業寫 Java 代碼一樣荒謬。
法則二:字符集精細化(同樣內容,空間減半)
- 純拉丁字符 →
latin1(1 字節/字符),極致省空間。
- 確定單語言 → 用該語言專用字符集(如中文
gbk 2 字節 vs utf8mb4 3-4 字節)。
- 列級字符集:MySQL 支援列級指定,核心表核心列用
utf8mb4,輔助表輔助列用 latin1/gbk,最大程度壓縮存儲。
法則三:適當垂直拆分(冷熱分離,緩存命中率倍增)
大字段(長文本、JSON、BLOB)若高頻查詢不需要,義無反顧拆到附屬表(一對一關聯)。
- 主表行長大幅縮減 → 每頁行數激增 → 熱數據緩存命中率飆升。
- 附屬表低頻訪問,即使全表掃描影響也可控。
- 典型案例:用戶主表(id、name、phone、status)+ 用戶詳情表(id、bio、address、preferences_json)。
法則四:適度冗余(空間換時間,消除大表 JOIN)
反範式設計,在特定場景極其高效:
- 場景:高頻查詢需通過 JOIN 2+ 張大表才能得到的「小字段」(如訂單表冗余買家昵稱、商品表冗余分類名)。
- 收益:省去大表 JOIN 的海量 IO、CPU、鎖開銷。
- 代價:寫入時需同步更新冗余列(應用層雙寫、觸發器、Binlog 異步同步),必須保證最終一致性。
法則五:堅持 NOT NULL(消除三值邏輯陷阱)
NULL 進入索引(MySQL 特性),但組合索引中含 NULL 列會嚴重降低索引效率(優化器難以利用)。
NULL 索引存儲額外佔用空間(位圖標記)。
NULL 導致 COUNT(col)、ORDER BY、GROUP BY、比較運算 語義複雜,優化器難處理。
- 最佳實踐:所有列
NOT NULL DEFAULT 默認值(空串、0、'1970-01-01'、'unknown' 等),從源頭消除 NULL。
總結:優化清單
- 審視每一列類型:能否更小?能否
UNSIGNED?能否 ENUM/SET?
- 審視每一張表:是否有冷熱不均的大字段需垂直拆分?
- 審視高頻 JOIN:是否有小字段可冗余消除 JOIN?
- 審視字符集:能否列級降級?
- 全庫掃描
NULL 列:逐個加 NOT NULL DEFAULT。
表結構優化是「一次設計,終身受益」的高 ROI 投資。上線前多花一天推敲,上線後少熬無數個夜優化 SQL。