優惠碼欄位加了UNIQUE,資料表卻還能存入兩筆NULL。這不是索引沒生效:在MySQL裡,可空欄位的唯一索引允許多個NULL。先確認你想防止的是「重複已填寫的代碼」,還是「每筆都一定要有唯一代碼」,兩種需求需要不同限制。
UNIQUE不等於每列必填
UNIQUE檢查的是唯一性;NULL代表缺少值,它與一般已填寫的字串不同。若欄位允許NULL,就不能期待UNIQUE同時要求每列都有代碼。MySQL的CREATE INDEX文件明確列出,允許NULL的UNIQUE索引可以包含多個NULL。
不要把這條規則延伸成所有資料庫都完全一樣,也不要把它說成「NULL等於NULL所以算一次」。NULL的一般比較有另外的語意,唯一索引的可空行為則是這裡實際要核對的規則。這篇用MySQL 8.4.9的單一字串欄位做直接插入,不混入其他引擎或複合鍵的例外。
兩個NULL成功,第二個A失敗
在自己的測試資料庫建立下面這張小表。id作為每筆資料的主鍵,code允許NULL且有UNIQUE。先一次放入兩筆沒有代碼的資料與一筆A,再嘗試新增第二筆A,才能同時看到可空行為與唯一限制是否生效。
CREATE TABLE codes (
id INT PRIMARY KEY,
code VARCHAR(30) NULL UNIQUE
);
INSERT INTO codes VALUES
(1,NULL), (2,NULL), (3,'A');
INSERT INTO codes VALUES (4,'A');
本次前三筆插入成功;第二個A被拒絕,錯誤碼1062。最後查詢id由小到大,結果仍是1=NULL、2=NULL、3=A,沒有id 4。這個對照說明唯一限制有工作,只是它不會把兩個NULL當成第二個A那樣的重複值。
SELECT id, code
FROM codes
ORDER BY id;
-- 實測結果
-- 1 NULL
-- 2 NULL
-- 3 A
負測的錯誤訊息是案例的一部分。這次命令行讓後續查詢繼續執行,以便查看失敗後的資料,因此不能只看整個程序最後的退出碼就宣稱四筆都插入了。自己重現時,把每個INSERT的成功或1062和最後列數一起保存。
查沒有代碼的資料,要用IS NULL
SELECT id, code
FROM codes
WHERE code IS NULL;
這個條件會找到兩筆沒有代碼的資料。不要改成code = NULL;一般等號比較遇到NULL,不會得到你要的真值。也不要寫code = ‘NULL’,那是四個字母構成的字串,與資料庫的空值完全不同。
管理工具常把NULL顯示成灰字或特殊標記,匯出工具又可能寫成空白。看到畫面空白時,先查IS NULL,再查等於空字串的情況;兩者不能只靠視覺判定。回到網站表單,也要確認未填欄位最後送入資料庫的是NULL、空字串,還是程式預設值。
MySQL也提供NULL安全的比較運算子,但那是在查詢中定義比較方式,不會把現有UNIQUE索引自動改成「只允許一個NULL」。查詢條件與資料限制分開處理,才不會因為某個SELECT能比對空值,就誤認為INSERT應有相同規則。
空字串不是NULL,必填也不是唯一
回傳JSON時也應保留這個差別。NULL可以對應JSON的null,空字串仍應是字串;如果程式為了方便顯示把兩者都轉成同一句「沒有資料」,只能把它當作顯示文字,不能再拿那句文字寫回代碼欄位。資料層的原始型態應留著,才能讓後面的檢查與更新作出正確選擇。
空字串是已存在的字串值;在一般單欄唯一限制下,重複的相同空字串仍會遇到唯一性檢查。這篇真插入案例只測了NULL與A,沒有另外插入兩筆空字串,所以不要把它的輸出冒充所有輸入型態都已實測。文件區分NULL與空字串的規則,足以提醒你先核對實際資料。
如果業務要求每筆都有代碼,欄位可考慮NOT NULL加UNIQUE,再由表單與後端處理是否允許空字串、只含空白或特定格式。NOT NULL不會自行禁止空字串,也不會完成格式驗證;欄位限制與輸入規則各有責任。
反過來,若會員可以暫時沒有優惠碼,NULL加UNIQUE就可能正好符合需求:多個未領取者可以共存,已領取的代碼不能重複。不要為了讓資料看起來每列都有值,就把所有未填值改成同一個字串「未設定」,那會把原來合法的多個空值變成同一個重複字串。
修改舊表前先核對實際資料與業務規則
這個測試只建立自己的小表,沒有把正式欄位改成NOT NULL。舊表若已有NULL,直接更改可空設定可能遇到錯誤或需要先處理資料;你也必須決定每一筆空值應補什麼。不能把所有空值改成A來湊齊,因為那正好違反唯一規則。
先統計NULL筆數,再核對非空代碼是否已符合預期的唯一性。若資料庫排序規則把某些大小寫視為相同,業務卻把它們當不同代碼,還要另外檢查字串比較規則。這個案例只用同一個大寫A,沒有測大小寫、尾端空白或不同語系比較,不能替那些情況下結論。
查表時留意COUNT(*)與COUNT(code)也不是同一個統計。前者計算列數,後者不計入code為NULL的列;要問「共有多少會員」與「有多少列已填代碼」,應使用對應的條件。不要把已填欄位數量當成全表列數,再誤以為資料庫漏存了兩筆。
複合唯一鍵也值得另立案例。當限制包含兩個以上欄位時,業務檢查的是欄位組合,不能直接把這張單欄code表的三筆結果當成所有組合的完整測試。先核對SHOW CREATE TABLE的真正限制,再確認你看到的1062究竟來自哪個索引,尤其是同表已有其他唯一欄位的時候。
應用程式常先查一次代碼是否存在,再執行INSERT。這種前置查詢可以提供較友善的提示,但不能取代資料庫唯一限制:兩個請求仍可能同時讀到「不存在」。處理正式新增時,程式也應能辨認1062並回傳合適結果,而不是把所有資料庫錯誤都叫做重複代碼。
若你真正需要的規則是「未填的列最多只能有一筆」,可空UNIQUE欄位本身不符合這個需求。先把規則寫清楚,再設計其他資料模型或限制;不要一邊允許任意列沒有代碼,一邊期待同一個限制把這些列擠成一筆。這已超出本篇單欄案例的範圍。
最後把驗收結果分三項記錄:兩個NULL是否成功、第二個A是否1062,以及失敗後是否仍只有前三列。能同時回答這三個問題,就知道索引有工作,也知道它實際保護的範圍。下一步才是決定網站要允許缺值,還是要求每列填入真正唯一的代碼。

評論0