作為一名資深後端開發,你有沒有遇到過這樣的場景:接手一個新項目,看到前任同事設計的數據庫表結構,簡直讓人懷疑人生?表名莫名其妙,字段命名混亂,關係不清不楚,查詢效率低下..
這讓我深刻意識到了:哪怕是一些"高級開發",也並不知道怎麼去設計一個好的表結構。
於是決定和大家一起探討如何更好的設計表結構。所有觀點都是結合多年的經驗得來,不一定正確,如有錯誤之處歡迎大家指正。
一、表名設計:第一眼就要知道是幹什麼的
1.1 有意義的前綴 + 清晰的表名
前綴在大型系統中是有必要的,可以區分不同業務模塊,但關鍵是前綴要有明確含義,表名要語義清晰。
❌ 不好的命名:
DC_COURSE_TESTPAPER -- TESTPAPER是問卷還是試卷?
TB_USER_INFO -- TB前綴無意義,INFO太泛泛
T_ORDER_DTL -- DTL是detail的縮寫?
DATA_TBL_001 -- 完全看不懂
✅ 好的命名:
DC_COURSE_QUESTIONNAIRES -- DC表示Distance Course遠程課程系統
SYS_USER_PROFILES -- SYS表示系統核心模塊
ORDER_ITEMS -- 訂單商品明細
LMS_STUDENT_SCORES -- LMS表示Learning Management System
什麼時候需要前綴?
- 多個業務系統共用數據庫:USER_, ORDER_, PRODUCT_
- 區分不同數據類型:LOG_, CONFIG_, TEMP_
- 大型項目的模塊劃分:CRM_, ERP_, CMS_
1.2 用完整的英文單詞而不是拼音
❌ 不好的命名:
kecheng_wenjuan -- 拼音
user_xinxi -- 中英混合
訂單_items -- 中英混合
✅ 好的命名:
course_questionnaires -- 純英文,語義清晰
user_profiles -- 純英文
order_items -- 純英文
原因:
- 英文是編程的通用語言,團隊成員更容易理解
- 避免編碼問題
1.3 表名要體現業務含義,不要只是技術實現
❌ 不好的命名:
data_table_001
temp_storage
middle_table
relation_mapping
✅ 好的命名:
student_scores -- 學生成績
file_uploads -- 文件上傳記錄
course_enrollments -- 課程報名
user_preferences -- 用户偏好設置
除非是臨時用的表,不參與任何業務邏輯,只是用來做數據處理或者測試。
二、字段命名:見名知意
2.1 布爾字段用 is_ 開頭
❌ 不好的命名:
active -- 是激活還是活躍?
delete -- 刪除狀態還是刪除動作?
flag -- 什麼標誌?
✅ 好的命名:
is_active -- 是否激活
is_deleted -- 是否已刪除
is_verified -- 是否已驗證
用 is_ 開頭的好處:
- 一眼就能看出是布爾值 - 看到 is_active 就知道這個字段要麼是 true 要麼是 false
- 避免歧義 - 像 active 這樣的名字,你搞不清楚它表示的是狀態還是動作
- 代碼可讀性更好 - 寫代碼的時候,if (user.is_active) 比 if (user.active) 更容易理解
2.2 時間字段統一後綴
❌ 不好的命名:
create_time
update_date
delete_at
register_datetime
✅ 好的命名:
created_at -- 創建時間
updated_at -- 更新時間
deleted_at -- 刪除時間
registered_at -- 註冊時間
好處:
- 一眼就能看出是時間字段
- 避免命名混亂
- _at 在英語裏表示"在某個時間點",比 _time 更準確
- 特別推薦用 deleted_at 做邏輯刪除字段,能看出來刪除時間,支持數據恢復
2.3 外鍵字段統一 _id 後綴
❌ 不好的命名:
user -- 這是用户ID還是用户對象?
course -- 課程ID?
teacher_key -- 什麼key?
✅ 好的命名:
user_id -- 用户ID
course_id -- 課程ID
teacher_id -- 教師ID
2.4 額外的經驗
除了基本的命名規範,還有一些實用的經驗:
- 邏輯刪除用時間字段 - 用 deleted_at 比 is_deleted 好,能看出來刪除時間,支持數據恢復和歷史追蹤
- 狀態字段用枚舉 - 不要用數字 1、2、3 表示狀態,用 status 字段,值用 'pending'、'approved'、'rejected' 這樣的英文單詞
- 金額字段用 decimal - 不要用 float 或 double,用 decimal(10,2) 這樣的類型,避免浮點數精度問題
- 密碼字段要加密 - 密碼字段名用 password_hash 或 encrypted_password,不要直接叫 password
- 軟刪除要加索引 - 如果經常查詢未刪除的數據,給 deleted_at 字段加索引,提高查詢性能
三、表結構設計:關係清晰、適度冗餘
3.1 一對多關係:外鍵放在多的一邊
讓我們用用户和訂單的業務關係來舉例:
用户表 (users):
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
created_at TIMESTAMP
);
訂單表 (orders):
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT, -- 外鍵放在多的一邊
order_no VARCHAR(32),
total_amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
這樣設計的好處:
- 通過 user_id 就知道訂單屬於哪個用户
- JOIN 一下就能拿到用户的所有訂單
- 新增訂單字段不影響用户表
3.2 多對多關係:中間表命名要體現關係
多對多關係的中間表命名要根據具體情況來選擇:
情況1:有業務含義的關係表 比如學生和課程的關係,不只是簡單關聯,還有報名時間、狀態等業務信息:
❌ 不好的設計:
student_course_rel -- rel是什麼關係?
sc_mapping -- 縮寫看不懂
middle_table -- 完全不知道什麼意思
✅ 好的設計: 課程報名表 (course_enrollments):
CREATE TABLE course_enrollments (
id BIGINT PRIMARY KEY,
student_id BIGINT,
course_id BIGINT,
enrolled_at TIMESTAMP,
status VARCHAR(20),
FOREIGN KEY (student_id) REFERENCES students(id),
FOREIGN KEY (course_id) REFERENCES courses(id)
);
情況2:純粹的關聯關係表 如果只是單純的多對多映射,沒有額外的業務屬性,用mapping也是可以的:
用户角色關聯表 (user_role_mappings):
CREATE TABLE user_role_mappings (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
如何選擇命名?
- 有業務含義的關係:用具體的業務名詞,如enrollments、orders、friendships
- 純粹的映射關係:可以用mappings、relations或直接用實體1_實體2s
- 關鍵是保持團隊內命名風格的統一
3.3 適當的字段冗餘:提升查詢效率
有時候為了避免複雜的JOIN查詢,適當冗餘是非常有必要的。
最典型的就是冗餘上級ID:
訂單詳情表 (order_items):
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT,
user_id BIGINT, -- 冗餘字段
product_id BIGINT,
quantity INT,
price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
為什麼要冗餘 user_id?
- 查詢用户的所有購買記錄時,直接查 order_items 表就行
- 不需要先通過 orders 表再關聯到 order_items
- 一個查詢代替了兩表JOIN
四、索引設計:讓查詢飛起來
4.1 索引基本原則
- 區分度最大的字段放在前面 - 在複合索引中,把選擇性高的字段放在前面
- 避免冗餘和重複索引 - (a,b)和(a)這樣的索引就是冗餘的
- 控制索引數量 - 每個表的索引數量建議不超過5個
- 不要索引大型字段 - 有很多字符的字段建議考慮前綴索引
4.2 常見索引設計場景
場景1:經常用於查詢條件的字段
-- 用户經常按郵箱查詢
CREATE INDEX idx_users_email ON users(email);
-- 訂單經常按用户和狀態查詢
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
場景2:經常用於排序的字段
-- 按創建時間排序
CREATE INDEX idx_orders_created_at ON orders(created_at);
場景3:經常用於JOIN的字段
-- 外鍵字段通常需要索引
CREATE INDEX idx_orders_user_id ON orders(user_id);
4.3 索引優化實戰
避免索引失效的情況:
-- ❌ 這樣查詢會導致索引失效
SELECT * FROM users WHERE YEAR(created_at) = 2023;
-- ✅ 應該這樣寫
SELECT * FROM users WHERE created_at >= '2023-01-01' AND created_at < '2024-01-01';
複合索引的最左前綴原則:
-- 創建複合索引
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at);
-- ✅ 這些查詢能用到索引
SELECT * FROM orders WHERE user_id = 1;
SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
SELECT * FROM orders WHERE user_id = 1 AND status = 'paid' AND created_at > '2023-01-01';
-- ❌ 這個查詢用不到索引
SELECT * FROM orders WHERE status = 'paid';
五、規範化與反規範化:平衡的藝術
5.1 規範化設計
規範化是為了減少數據冗餘,提高數據一致性和完整性。
第一範式(1NF):字段不可再分
-- ❌ 不符合1NF
CREATE TABLE orders (
id BIGINT,
product_names VARCHAR(255) -- 存儲"商品A,商品B,商品C"
);
-- ✅ 符合1NF
CREATE TABLE orders (
id BIGINT
);
CREATE TABLE order_items (
order_id BIGINT,
product_id BIGINT,
quantity INT
);
第二範式(2NF):消除部分依賴
-- ❌ 不符合2NF
CREATE TABLE order_items (
order_id BIGINT,
product_id BIGINT,
product_name VARCHAR(100), -- 依賴於product_id,不依賴於order_id
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- ✅ 符合2NF
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT
);
CREATE TABLE products (
id BIGINT PRIMARY KEY,
product_name VARCHAR(100)
);
CREATE TABLE order_items (
order_id BIGINT,
product_id BIGINT,
quantity INT,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);
5.2 反規範化設計
反規範化是為了優化數據的讀取性能,適用於讀取操作頻繁或數據量極大的系統。
增加冗餘列:
-- 在訂單詳情中冗餘商品名稱,避免JOIN查詢
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT,
product_id BIGINT,
product_name VARCHAR(100), -- 冗餘字段
quantity INT,
price DECIMAL(10,2)
);
預計算字段:
-- 在訂單表中預計算總金額,避免每次查詢都SUM
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
total_amount DECIMAL(10,2), -- 預計算字段
status VARCHAR(20),
created_at TIMESTAMP
);
六、性能優化實戰技巧
6.1 查詢優化
使用EXISTS替代IN:
-- ❌ 性能較差
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid');
-- ✅ 性能更好
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid');
*避免SELECT :
-- ❌ 不推薦
SELECT * FROM users WHERE email = 'user@example.com';
-- ✅ 推薦
SELECT id, username, email FROM users WHERE email = 'user@example.com';
6.2 分頁查詢優化
傳統分頁的問題:
-- ❌ 當OFFSET很大時性能很差
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 100000;
優化後的分頁:
-- ✅ 使用遊標分頁
SELECT * FROM orders WHERE id < 100000 ORDER BY id DESC LIMIT 10;
6.3 批量操作優化
批量插入:
-- ❌ 逐條插入
INSERT INTO users (username, email) VALUES ('user1', 'user1@example.com');
INSERT INTO users (username, email) VALUES ('user2', 'user2@example.com');
INSERT INTO users (username, email) VALUES ('user3', 'user3@example.com');
-- ✅ 批量插入
INSERT INTO users (username, email) VALUES
('user1', 'user1@example.com'),
('user2', 'user2@example.com'),
('user3', 'user3@example.com');
七、總結
數據庫表設計是一門藝術,需要在規範化與性能、可讀性與效率之間找到平衡點。一個好的表結構設計應該具備以下特點:
- 語義清晰:表名和字段名一看就知道是幹什麼的
- 關係明確:表之間的關係一目瞭然
- 性能良好:合理的索引設計,避免全表掃描
- 易於維護:結構清晰,便於後續擴展和修改
記住,沒有完美的設計,只有最適合當前業務場景的設計。在實際開發中,要根據具體的業務需求、數據量大小、查詢模式等因素來綜合考慮。
希望今天的分享能幫助你在下次設計數據庫表結構時,不再讓人"人麻了"!
在實際項目中,建議團隊制定統一的數據庫設計規範,並通過代碼審查來確保規範的執行。只有這樣,才能保證整個項目的數據庫設計質量。