编程后端开发者

如何实现 SQL 中复杂业务键的数据唯一性验证和保障机制,特别是在允许 NULL 值的情况下?

用 Hintsage AI 助手通过面试

答案。

在关系数据库中,唯一性保障的任务通常由 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 的组合,显式管理唯一性规则。
  • 可以在数据库层面实现自动化和支持。
  • 支持复杂和变化的架构。

复杂问题。

如果某些列允许 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 在应用层面上解决唯一性问题吗?

可以,但仅用于选择——这并不能防止在数据修改时引入重复项,也不是表级约束的替代品。

常见错误与反模式

  • 尝试在可能包含 NULL 的组合情况下使用 UNIQUE 约束。
  • 仅在应用层面检查唯一性(绕过数据库)。
  • 触发器的复杂性过高而没有明确的索引——导致性能下降。

生活中的例子

负面案例

公司通过 UNIQUE(email, phone) 实现 email+电话的唯一性。当电话为 NULL 时,数据库会出现重复的 email。

优点:

  • 实现简单。

缺点:

  • 完整性丧失,出现重复。
  • 间接错误,分析中难以追踪。

正面案例

使用计算列 (COALESCE(email, '##') + '#' + COALESCE(phone, '')), 在其上施加唯一索引。

优点:

  • 数据库排除重复,任何操作(UPDATE, INSERT)都会立即生效。

缺点:

  • 对于大数据量,重新索引可能需要时间。
  • 影响索引的大小。