反範式設計中常見「一字段存逗號分隔值」(如 pnum='1,2,4')。MySQL 提供 FIND_IN_SET() 專用函數,也可用 REGEXP 正則匹配。本文實測對比兩者語義差異、性能與適用場景。
CREATE TABLE test (
id INT(6) NOT NULL AUTO_INCREMENT PRIMARY KEY,
pname VARCHAR(20) NOT NULL,
pnum VARCHAR(50) NOT NULL
);
INSERT INTO test(pname,pnum) VALUES
('產品1','1,2,4'), ('產品2','2,4,7'), ('產品3','3,4'),
('產品4','1,7,8,9'), ('產品5','33,4');
-- 查找 pnum 包含 3 或 9 的記錄
SELECT * FROM test
WHERE FIND_IN_SET('3', pnum) OR FIND_IN_SET('9', pnum);
結果:
+----+-------+---------+ | id | pname | pnum | +----+-------+---------+ | 3 | 產品3 | 3,4 | | 4 | 產品4 | 1,7,8,9 | +----+-------+---------+
33 不會匹配 3)。REPLACE(pnum,' ','') 預處理。SELECT * FROM test WHERE pnum REGEXP '(3|9)';
結果:
+----+-------+---------+ | id | pname | pnum | +----+-------+---------+ | 3 | 產品3 | 3,4 | | 4 | 產品4 | 1,7,8,9 | | 5 | 產品5 | 33,4 | <-- 誤匹配:33 含 3 +----+-------+---------+
3 會匹配 33、13 等任意包含 '3' 的子串。REGEXP '(^|,)3(,|$)' 模擬集合邊界,但寫法繁瑣、性能更差。-- 需按 id 4,2,3 順序返回 SELECT * FROM test WHERE id IN (4,2,3) ORDER BY FIND_IN_SET(id, '4,2,3');
結果:
+----+-------+---------+ | id | pname | pnum | +----+-------+---------+ | 4 | 產品4 | 1,7,8,9 | | 2 | 產品2 | 2,4,7 | | 3 | 產品3 | 3,4 | +----+-------+---------+
巧妙利用 FIND_IN_SET 返回位置作排序鍵,避免 CASE WHEN 或 FIELD() 寫法。
| 維度 | FIND_IN_SET | REGEXP |
|---|---|---|
| 精確集合匹配 | ✅ 原生支援 | ❌ 需複雜邊界正則 |
| 索引利用 | ❌ 全掃 | ❌ 全掃 |
| 子串誤匹配 | ❌ 不會 | ✅ 會 |
| 複雜模式 | ❌ 不支援 | ✅ 支援 |
| 自定義排序 | ✅ 妙用 | ❌ 不直接支援 |
逗號分隔字段違反第一範式,**查詢、索引、統計、維護皆痛點**。建議:
product_tag(product_id, tag_id),外鍵約束、索引加速、JOIN 查詢。tags JSON,JSON_CONTAINS(tags, '3') 精確匹配,支援 GIN 索引(8.0+)。FIND_IN_SET 保證語義正確。FIND_IN_SET 是精確集合成員檢測的正解,REGEXP 適合模糊/複雜模式。切記:33 不等於 3,不要用 REGEXP '(3|9)' 查集合。長期看,重構為關聯表或 JSON 才能獲得索引加速、數據完整性與可維護性。