在关系数据库中,唯一性保障的任务通常由 UNIQUE 约束承担。但在业务实践中,有时需要根据字段的组合来确保唯一性,而这些字段中有些可能是 NULL(例如,唯一组合 email+电话,但电话可能未知)。
问题的背景: 在 SQL 中,标准的 UNIQUE 约束在某个列为 NULL 时并不保证唯一性——规范认为这些值是互斥的。
问题: 对于包含 NULL 的复杂键,标准约束会导致重复的出现。当数据完整性是必要时,这尤其重要:在导入、迁移、大规模更新时。
解决方案: 使用一个计算列,考虑所有值(包括 NULL),并在该列上施加唯一性,或者使用触发器逻辑。
示例代码:
ALTER TABLE my_table ADD computed_uniqueness AS ( ISNULL(email, '') + '#' + ISNULL(phone, '') ); CREATE UNIQUE INDEX idx_my_table_computed_uniqueness ON my_table(computed_uniqueness);
或者(对于 PostgreSQL,因为索引中的表达式和 NULL 被视为不同):
CREATE UNIQUE INDEX idx_unique_email_phone ON my_table ((COALESCE(email, '##')),(COALESCE(phone, '##')));
关键特点:
如果某些列允许 NULL,是否可以使用普通的 UNIQUE 索引来确保唯一性?
不可以。根据 ANSI SQL,UNIQUE 索引允许有多行,其中至少一个列在组合中为 NULL,因为 NULL 被视为不等于任何其他值,包括 NULL。
使用表达式的唯一索引和使用 BEFORE INSERT 触发器检查唯一性之间的区别是什么?
唯一索引更易于维护且执行速度更快,但并不总能实现复杂的业务规则(例如,例外或自定义组合)。触发器更灵活,但速度较慢且维护更复杂。
示例:
CREATE OR REPLACE FUNCTION check_custom_unique() RETURNS TRIGGER AS $$ BEGIN IF EXISTS ( SELECT 1 FROM my_table WHERE COALESCE(NEW.email, '##') = COALESCE(email, '##') AND COALESCE(NEW.phone, '##') = COALESCE(phone, '##') ) THEN RAISE EXCEPTION 'Duplicate found'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_custom_unique BEFORE INSERT OR UPDATE ON my_table FOR EACH ROW EXECUTE FUNCTION check_custom_unique();
可以在 SELECT 中使用 DISTINCT 在应用层面上解决唯一性问题吗?
可以,但仅用于选择——这并不能防止在数据修改时引入重复项,也不是表级约束的替代品。
公司通过 UNIQUE(email, phone) 实现 email+电话的唯一性。当电话为 NULL 时,数据库会出现重复的 email。
优点:
缺点:
使用计算列 (COALESCE(email, '##') + '#' + COALESCE(phone, '')), 在其上施加唯一索引。
优点:
缺点: