MySQL表的約束
MySQL表的約束
1. 空屬性
2. 默認值
3. 列描述
4. zerofill
5. 主鍵
6. 自增長
7. 唯一鍵
8. 外鍵
9. 綜合案例
MySQL表的約束
說到 MySQL 表的約束,這絕對是數據庫設計里繞不開的一個話題。很多人一提“約束”,第一反應可能是字段的數據類型,比如 INT 不能存字符串,VARCHAR 有長度限制。這當然是最基本的一層限制,但光靠這些,遠遠不夠。就像你家門口裝了密碼鎖,結果廚房窗戶忘關了——防得了一部分,還是會漏。
這時候就得靠表級的各種約束來兜底,從業務邏輯的角度出發,幫你把不合理的數據擋在門外。
舉個栗子🌰:假設你建了一個用戶表,里面有個“年齡”字段,類型是 INT。這時候要是有人輸入個 -100 或者 999999999 歲,數據庫是能存下沒錯,但你一看就知道這不靠譜。這種數值范圍的控制,其實屬于“檢查約束(CHECK)”。不過早期的 MySQL 并不支持這玩意兒(8.0 開始才支持),所以我們今天先不展開這個,重點聊聊其他你在開發里經常用到的約束類型。
這篇文章我們就來盤講講這些實用技能:空屬性、默認值、列描述、zerofill、主鍵、自增長、唯一鍵、外鍵。最后還會用一個綜合案例,把這些知識點串起來,幫你真正掌握這些“看起來簡單但常常出問題”的細節。
1. 空屬性
1.1 NULL和NOT NULL的相愛相殺
數據庫里默認所有字段都是允許為空的(NULL),就像你家的冰箱——什么都能塞進去。但實際開發中,咱們得學會"斷舍離",把不該空的字段鎖死。為啥?因為空值是個麻煩精!它參與運算時會直接讓結果變NULL,就像往火鍋里倒可樂——全毀了。
舉個例子🌰:假設你統計銷售額,某個訂單金額是NULL,那么SELECT SUM(price)
的結果也會是NULL。這時候老板要是問"今天賺了多少",你只能尷尬地回一句:“不知道啊,有筆訂單金額沒填…”
所以遇到必填項,一定要用NOT NULL。比如班級表的教室字段:
CREATE TABLE class(class_name VARCHAR(20) NOT NULL,classroom VARCHAR(20) NOT NULL
);
這時候插入數據時,這兩個字段就必須填,否則MySQL會直接報錯:
INSERT INTO class(class_name) VALUES('三年二班');
-- 報錯:Field 'classroom' doesn't have a default value
1.2 NULL的隱藏陷阱
很多人以為NULL就是"沒有值",其實它更像"未知值"。比如兩個NULL比較時,既不等于也不等于不,永遠返回UNKNOWN。這會導致查詢時出現意想不到的結果:
SELECT * FROM users WHERE email = NULL; -- 查不到任何數據
SELECT * FROM users WHERE email IS NULL; -- 才能查到空郵箱用戶
1.3 NOT NULL的進階玩法
有時候我們會給字段設置默認值,這時候再加NOT NULL就顯得多余了。比如:
CREATE TABLE user(id INT PRIMARY KEY AUTO_INCREMENT,gender ENUM('男','女') DEFAULT '男' NOT NULL
);
這里的NOT NULL其實可以省略,因為默認值已經保證了字段不為空。但要注意默認值的類型匹配,比如用字符串’0’當默認值,字段類型卻是INT的話,可能會觸發隱式轉換。
2. 默認值
2.1 DEFAULT的魔法時刻
默認值就像自動售貨機——當你不投幣時,它自己吐出預設的商品。比如用戶表的性別字段:
CREATE TABLE user(id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(20),gender ENUM('男','女') DEFAULT '男'
);
這時候插入數據時,如果不指定gender字段,就會自動填充"男":
INSERT INTO user(name) VALUES('張三');
-- 實際插入的是('張三', '男')
2.2 默認值的進階套路
默認值不僅能用常量,還能用表達式(MySQL 8.0+支持):
CREATE TABLE orders(order_id INT PRIMARY KEY AUTO_INCREMENT,create_time DATETIME DEFAULT NOW(),expire_time DATETIME DEFAULT (NOW() + INTERVAL 7 DAY)
);
不過要注意,同一個字段不能同時有默認值和NOT NULL約束(除非默認值明確指定),否則會觸發沖突。
2.3 默認值的隱藏彩蛋
對于日期時間類型字段,默認值有特殊規則:
DATETIME
默認值只能是常量,不能用函數TIMESTAMP
會自動設置當前時間作為默認值(如果未顯式指定)
比如:
CREATE TABLE test(id INT PRIMARY KEY AUTO_INCREMENT,dt DATETIME, -- 默認值為NULLts TIMESTAMP -- 默認值自動變為CURRENT_TIMESTAMP
);
3. 列描述
3.1 COMMENT的文藝復興
列描述就像給數據庫字段寫小紙條,方便后來人看懂你的設計思路。比如用戶表:
CREATE TABLE user(id INT PRIMARY KEY COMMENT '用戶ID',name VARCHAR(20) COMMENT '用戶真實姓名',nickname VARCHAR(20) COMMENT '用戶昵稱(可為空)'
);
通過SHOW CREATE TABLE user;
就能看到這些注釋:
CREATE TABLE `user` (`id` int(11) NOT NULL COMMENT '用戶ID',`name` varchar(20) NOT NULL COMMENT '用戶真實姓名',`nickname` varchar(20) DEFAULT NULL COMMENT '用戶昵稱(可為空)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 描述的最佳實踐
- 業務含義:說明字段的業務邏輯,比如
status TINYINT COMMENT '狀態:1-待支付 2-已支付 3-已發貨'
- 數據來源:標明數據是怎么來的,比如
score DECIMAL(5,2) COMMENT '根據考試成績自動同步'
- 變更記錄:重要字段的變更歷史,比如
address VARCHAR(255) COMMENT '2022.3新增,原地址字段已廢棄'
3.3 描述的隱藏功能
結合INFORMATION_SCHEMA.COLUMNS
表,可以實現自動化文檔生成:
SELECT COLUMN_NAME, COLUMN_COMMENT
FROM information_schema.columns
WHERE TABLE_SCHEMA='your_db' AND TABLE_NAME='user';
4. zerofill
4.1 數字顯示的藝術
zerofill就像給數字穿上西裝打上領帶——讓它看起來更正式。比如訂單編號:
CREATE TABLE orders(order_id INT(6) ZEROFILL PRIMARY KEY AUTO_INCREMENT
);
插入數據時:
INSERT INTO orders() VALUES();
-- 實際顯示order_id為000001
注意這里的INT(6)不是指6位數字,而是顯示寬度。底層存儲還是標準的INT類型(4字節)。
4.2 zerofill的連帶效應
使用zerofill會自動觸發UNSIGNED屬性:
CREATE TABLE test(a INT(5) ZEROFILL);
-- 實際相當于 INT(5) UNSIGNED ZEROFILL
所以字段只能存儲正數,負數插入會變成0。
4.3 使用場景分析
適合需要固定顯示位數的業務場景:
- 學號:202301010001(年份+學院代碼+序號)
- 發票號碼:0000123456
- 產品編號:P000001
但要注意,這種格式化更適合前端處理,數據庫層面保持純粹數字更利于計算。
5. 主鍵
5.1 數據的身份證
主鍵就像每個人的身份證號碼——必須唯一且不能為空。創建學生表:
CREATE TABLE student(stu_id CHAR(10) PRIMARY KEY COMMENT '學號(唯一標識)',name VARCHAR(20)
);
這時候插入重復學號會直接報錯:
INSERT INTO student VALUES('2023010101','張三');
INSERT INTO student VALUES('2023010101','李四');
-- 報錯:Duplicate entry '2023010101' for key 'PRIMARY'
5.2 主鍵的進階操作
-
刪除主鍵:
ALTER TABLE student DROP PRIMARY KEY;
注意:如果該列有自增屬性,需要先刪除自增
-
修改主鍵:
ALTER TABLE student MODIFY stu_id CHAR(12); -- 修改字段類型不影響主鍵約束
-
復合主鍵:
CREATE TABLE cart(user_id INT,product_id INT,quantity INT,PRIMARY KEY(user_id, product_id) );
這時候單個字段可以重復,但組合必須唯一
5.3 主鍵選擇的玄學
-
自增主鍵 vs 業務主鍵
自增主鍵(如AUTO_INCREMENT)更利于索引性能,業務主鍵(如身份證號)更直觀,需要根據場景權衡 -
UUID的另類玩法
CREATE TABLE orders(order_id CHAR(36) PRIMARY KEY DEFAULT UUID(),... );
適合分布式系統,但會占用更多存儲空間
6. 自增長
6.1 AUTO_INCREMENT的魔法
自增字段就像自動步槍——每次扣動扳機都會自動裝彈。創建文章表:
CREATE TABLE articles(article_id INT AUTO_INCREMENT PRIMARY KEY,title VARCHAR(100)
);
插入數據時:
INSERT INTO articles(title) VALUES('MySQL約束詳解');
-- article_id自動分配為1
INSERT INTO articles(title) VALUES('性能優化技巧');
-- article_id自動分配為2
6.2 自增的隱藏技巧
-
指定初始值:
CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY ) AUTO_INCREMENT = 1000;
-
跳增現象:
如果插入失敗或事務回滾,自增值不會回退。比如插入100后失敗,下一個值會是101而不是100 -
手動賦值:
INSERT INTO articles(article_id, title) VALUES(10000, '特別推薦'); -- 下次自增值從10001開始
6.3 自增的性能考量
-
并發問題:
InnoDB引擎使用互斥鎖來確保自增的連續性,在高并發場景可能成為瓶頸 -
緩存配置:
innodb_autoinc_lock_mode
參數影響自增行為,需要根據業務調整 -
遷移風險:
導出導入數據時,記得檢查自增字段的當前值:SHOW TABLE STATUS LIKE 'articles'; -- 查看Auto_increment列
7. 唯一鍵
7.1 除主鍵外的唯一保障
唯一鍵就像班級里的學號和電話號碼——都可以唯一標識學生,但只能選一個當主鍵。創建用戶表:
CREATE TABLE user(id INT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(20) UNIQUE,email VARCHAR(50) UNIQUE
);
這時候用戶名和郵箱都必須唯一:
INSERT INTO user(username, email)
VALUES('zhangsan', 'zhangsan@example.com');-- 插入相同用戶名會失敗
INSERT INTO user(username, email)
VALUES('zhangsan', 'zs@example.com');
-- 報錯:Duplicate entry 'zhangsan' for key 'username'
7.2 唯一鍵的騷操作
-
復合唯一鍵:
CREATE TABLE exam(student_id INT,subject VARCHAR(20),score INT,UNIQUE(student_id, subject) );
保證同一個學生同一科目只有一條記錄
-
空值處理:
唯一鍵允許有多個NULL值(這與主鍵不同):INSERT INTO user(username) VALUES(NULL); INSERT INTO user(username) VALUES(NULL); -- 兩條記錄都能成功插入
-
延遲約束:
在事務中,唯一約束檢查可以延遲到提交時:SET CONSTRAINTS ALL DEFERRED; -- 需要配合支持的存儲引擎
7.3 唯一鍵的優化技巧
-
前綴索引:
對長字符串字段,可以只索引前綴:CREATE TABLE products(id INT PRIMARY KEY,product_code VARCHAR(100) UNIQUE ); -- 改為 CREATE UNIQUE INDEX idx_code ON products(product_code(20));
-
合并索引:
如果某個查詢經常同時用到username和email,可以創建聯合唯一索引:CREATE UNIQUE INDEX idx_user_email ON user(username, email);
8. 外鍵
8.1 表之間的羈絆
外鍵就像親子關系——孩子必須知道自己爹是誰。創建訂單表:
CREATE TABLE orders(order_id INT PRIMARY KEY AUTO_INCREMENT,user_id INT,FOREIGN KEY(user_id) REFERENCES users(id)
);
這時候插入訂單時:
-- 假設users表中沒有id=100的用戶
INSERT INTO orders(user_id) VALUES(100);
-- 報錯:Cannot add or update a child row
8.2 外鍵的連環反應
-
級聯更新:
當父表主鍵變更時,子表自動更新:CREATE TABLE orders(order_id INT PRIMARY KEY AUTO_INCREMENT,user_id INT,FOREIGN KEY(user_id) REFERENCES users(id) ON UPDATE CASCADE );
-
級聯刪除:
刪除父表記錄時,自動刪除子表關聯數據:FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
-
置空操作:
刪除父表記錄時,將子表外鍵字段設為NULL:FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL -- 注意:字段必須允許NULL
8.3 外鍵的性能博弈
-
鎖機制:
修改父表主鍵時,會鎖定子表相關記錄,可能引發死鎖 -
批量導入:
導入大量數據時,建議先關閉外鍵檢查:SET FOREIGN_KEY_CHECKS=0; -- 執行導入操作 SET FOREIGN_KEY_CHECKS=1;
-
索引優化:
外鍵字段必須有索引,否則會影響性能:CREATE INDEX idx_user ON orders(user_id); -- 如果創建外鍵時未自動創建
9. 綜合案例
9.1 商城系統的數據設計
讓我們來設計一個簡單的電商系統,包含三個核心表:
9.1.1 商品表(goods)
CREATE TABLE goods(goods_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',goods_name VARCHAR(100) NOT NULL COMMENT '商品名稱',unitprice DECIMAL(10,2) NOT NULL COMMENT '單價',category VARCHAR(50) COMMENT '分類',provider VARCHAR(100) COMMENT '供應商',stock INT DEFAULT 0 COMMENT '庫存',INDEX idx_category(category)
) ENGINE=InnoDB;
9.1.2 客戶表(customer)
CREATE TABLE customer(customer_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '客戶ID',name VARCHAR(20) NOT NULL COMMENT '姓名',address VARCHAR(200) COMMENT '住址',email VARCHAR(50) UNIQUE COMMENT '郵箱',sex ENUM('男','女') COMMENT '性別',card_id CHAR(18) UNIQUE COMMENT '身份證',reg_time DATETIME DEFAULT NOW() COMMENT '注冊時間'
) ENGINE=InnoDB;
9.1.3 購買表(purchase)
CREATE TABLE purchase(order_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '訂單號',customer_id INT NOT NULL COMMENT '客戶ID',goods_id INT NOT NULL COMMENT '商品ID',nums INT NOT NULL DEFAULT 1 COMMENT '購買數量',order_time DATETIME DEFAULT NOW(),FOREIGN KEY(customer_id) REFERENCES customer(customer_id),FOREIGN KEY(goods_id) REFERENCES goods(goods_id),INDEX idx_time(order_time)
) ENGINE=InnoDB;
9.2 設計亮點解析
-
約束組合拳:
- 客戶姓名NOT NULL保證必填
- 郵箱和身份證UNIQUE防止重復
- 性別用ENUM限制取值范圍
- 購買數量DEFAULT 1避免零值
-
性能優化:
- 商品分類添加索引
- 訂單時間建立索引方便按時間查詢
-
數據完整性:
- 外鍵約束確保訂單中的客戶和商品真實存在
- 級聯操作可自行添加(根據業務需求)
9.3 擴展思考
-
庫存扣減:
在購買表插入記錄時,需要更新商品表庫存。可以通過觸發器實現:DELIMITER // CREATE TRIGGER after_purchase_insert AFTER INSERT ON purchase FOR EACH ROW BEGINUPDATE goods SET stock = stock - NEW.nums WHERE goods_id = NEW.goods_id; END// DELIMITER ;
-
訂單狀態:
可以添加status字段表示訂單狀態(待支付、已發貨等),配合外鍵關聯狀態字典表 -
分區策略:
對于大規模數據,可以按訂單時間做分區:CREATE TABLE purchase(...) PARTITION BY RANGE (YEAR(order_time)) (PARTITION p2022 VALUES LESS THAN (2023),PARTITION p2023 VALUES LESS THAN (2024) );