【问题标题】:Allow null in unique column允许在唯一列中为空
【发布时间】:2013-12-07 20:59:36
【问题描述】:

我创建了下表:

CREATE TABLE MMCompany (
   CompanyUniqueID BIGSERIAL PRIMARY KEY NOT NULL, 
   Name VARCHAR (150) NOT NULL,
   PhoneNumber VARCHAR(20) NOT NULL UNIQUE, 
   Email VARCHAR(75) UNIQUE,
   CompanyLogo BYTEA
 );

电子邮件列是唯一的,它会在我的场景中导致“错误”,因为只能有一条为空的记录。我正在尝试获取没有相同电子邮件的公司的记录,但同时允许公司没有电子邮件。

我怎样才能做到这一点?

【问题讨论】:

    标签: sql postgresql database-design null unique-constraint


    【解决方案1】:

    这是一个误解。
    UNIQUE constraint 正是你想要的。多个NULL 值可以共存于定义UNIQUE 的列中。

    The manual:

    一般来说,当有超过 表中包含所有列的值的一行 在约束中是相等的。但是,两个空值不是 在这个比较中被认为是相等的。这意味着 即使在场 对于唯一约束,可以存储重复的行 在至少一个受约束的列中包含空值。这 行为符合 SQL 标准,但我们听说其他 SQL 数据库可能不遵循此规则。所以当你要小心 开发旨在便携的应用程序。

    我的大胆强调。

    请注意,character types 允许使用空字符串 (''),这不是 NULL 值,并且会像输入任何其他非空值一样触发唯一违规超过一排。

    【讨论】:

    • @foibs:这是 100% 正确和故障安全的,并且符合 SQL 标准以及它应该如何。 NULL 以这种方式定义是有原因的。
    • @sqlvogel:要避免该功能,请定义列NOT NULL。两全其美,这没什么丑陋的。正如这个请求所展示的那样很有用。所以,很明显,我不太同意你的判决。
    • @sqlvogel:我强调的是该功能的有用性(虽然它可以在不需要时轻松关闭)。你写It is not useful to do so 是在一个OP 要求的问题下,证明它的用处?你是否失去了讽刺?
    • 我完全赞成规范化,只要它是明智的。但我看到 no “关键依赖项”也没有违反任何正常形式。你还没有说出一个名字。不管怎样,这纯粹是学术性的。只要没有附加要求(例如每个电子邮件地址的属性或每个公司的多个电子邮件),为电子邮件属性创建单独的表是不明智的。可为空的列完全符合要求。我建议我们同意不同意。
    • @sqlvogel 和 ErwinBrandstetter 看起来您已经陷入了关于 NULL 的价值或危险性的古老争论。有关 NULL 的详尽评论,请阅读 Dr. Chris Date 的书 Guide to the SQL Standard。您还可以阅读学术文章,解释 NULL 可以采用的多种含糊含义。虽然我在阵营中认为 NULL 比帮助更麻烦,但其他人却发现了实际用途。我建议放手。
    【解决方案2】:

    在 Postgres 中没有这样的问题

    在 Erwin Brandstetter 的正确 answer 中,他解释说您确实应该看到您想要的行为(在唯一约束中允许多个 NULL)。您应该特别在 Postgres 以及任何符合 SQL 标准的数据库中看到这种行为。

    其他数据库的解决方法

    但是,Postgres 文档对可移植性提出警告,因为已知某些数据库违反了此功能。对于这样一个不合规的系统,我建议在此类字段中使用虚假值替换 NULL 值的使用。虚假值将是一个字符串,例如“unknown_”加上一些几乎可以肯定是唯一的任意值。该任意值可能类似于当前日期时间加上一个随机数。

    UUID

    但是,与其滚动您自己的任意值,不如生成一个UUID。最初的第 1 版 UUID 确实是当前日期时间、随机数和计算机几乎唯一的 MAC address 的组合。

    使用连字符的规范格式以十六进制字符串形式呈现的 UUID 如下所示:

    93e6f268-5c2d-4c63-9d9c-40e6ac034f88

    所以我的建议是组合一个任意字符串,例如“unknown_”加上一个 UUID,看起来像这样:

    unknown_93e6f268-5c2d-4c63-9d9c-40e6ac034f88

    所以我对不合规数据库的建议是生成这样一个值并使用它来代替 NULL,在特定行的该列中还没有已知值的情况下使用它。与其编写查询在该列中查找具有(或不具有)NULL 值的行,不如编写查找具有(或不具有)以任意字符串“unknown_”开头的值的行的查询例子。然后每一行将满足具有唯一值的约束。

    确实,我会将这个“unknown_”+ UUID 值指定为该列的默认值。

    您还可以向该列添加 NOT NULL 约束。

    生成 UUID 值

    Postgres 内置了对 UUID 数据类型的支持,但这与这里的答案无关。您需要的是生成一个 UUID

    要生成 UUID,您需要一个扩展(插件)来将此功能添加到 Postgres。大多数 Postgres 安装程序都包含这样的扩展。这个扩展名为uuid-ossp。通常默认情况下不会激活扩展程序。要在最新版本的 Postgres 中这样做,请使用 CREATE EXTENSION 命令。有关说明,请参阅我在 installing in Postgres 9.1 and latermy other post on Postgres 9.0 and earlier 上的博客文章。只要扩展/插件已编译并与您的 Postgres 安装捆绑在一起,新旧安装方式都很容易。

    总结

    让我明确一点,仅对于 Postgres,不需要这种解决方法,因为 Postgres 符合 SQL 标准。但是如果:

    • 您担心将代码移植到其他不兼容的数据库系统,或者
    • 您需要与不兼容的数据库系统交换数据,或者
    • 您同意 Dr. Chris Date 的观点,即 NULL 是魔鬼的杰作,应该避免

    ...那么像这样的解决方法是必要的。

    【讨论】:

    • 谢谢,我已经看过了,我想向你保证,我在发帖前确实费心搜索过,我通常会这样做,不幸的是,所提供的答案不适合我,无论如何比你的帮助,下次尽量不要这么粗鲁!
    • @liva (a) 我错了,你没有费心去研究。我的原始答案链接到未能解决您的问题的问题答案中是错误的。我站得更正了。对于我的粗鲁和粗心,我深表歉意。 (b) 我用一个新的答案替换了我原来的答案,它可以帮助您或其他可能需要使用 Postgres 以外的数据库的人,这些数据库未能遵循 SQL 规范的要求,即唯一约束应允许多个 NULL 值。
    • 一个非常全面的评估,为此 +1。
    • @KamilKiełczewski 不,那不行。 Postgres 中没有row.id。在每个表上的 ctid system column 中发现了相同的想法,它唯一地标识了行。但作为一个物理标识符,它可以在 VACUUM FULL 期间发生变化,并且可能被重新分配。正如文档所说:“ctid 作为长期行标识符是无用的”。当您需要一个通用唯一标识符时,请使用……universally unique identifier (UUID)
    【解决方案3】:

    一些数据库不允许多个空值,例如SQL Server documentation 声明“多个空值被认为是重复的”。在不允许可为空的 UNIQUE 约束的数据库上,您可以试试这个(从GuidoG's answer 到另一个问题):

    CREATE UNIQUE NONCLUSTERED INDEX IDX_Email
    ON MMCompany (Email)
    WHERE Email IS NOT NULL;
    

    【讨论】:

      【解决方案4】:

      从表格中删除电子邮件列。将它放在一个新表中,它可以是 NOT NULL 和 UNIQUE:

      CREATE TABLE CompanyEmail
       (
          CompanyUniqueID INT NOT NULL PRIMARY KEY
             REFERENCES MMCompany (CompanyUniqueID),
          Email VARCHAR(75) NOT NULL UNIQUE
       );
      

      避免可为空的 UNIQUE 约束。

      【讨论】:

        【解决方案5】:

        unique 和 null 相处得并不多,因为 null 在定义上是未定义的——你无法知道两个 null 是否相同的未知数。

        从这个意义上说,您当前对电子邮件的独特限制是正确的做法,应该按原样工作。


        如果你需要做其他事情,部分索引可以工作:

        create unique index on MMCompany((email is null)) where (email is null);
        

        另一种方法是定义一个约束触发器。比如:

        create function email_chk() returns trigger as $$
        begin
          if exists (
            select 1 from mmcompany where email is null and companyuniqueid <> new.id
          ) then
            raise 'dup null found';
          end if;
          return null;
        end;
        $$ language plpgsql;
        
        create constraint trigger after insert or update on mmcompany
        for each row when (new.email is null)
        execute procedure email_chk();
        

        【讨论】:

        • 其实两个空值总是被认为不相等的。这就是为什么你从不做WHERE email = NULL 而是做WHERE email IS NULL 的原因。很抱歉回复了 5 年的评论。但这在 5 年前和现在一样真实......
        • 嗯,“is null”的意思是“null 是一个单例”,任何两个 null 值都应该相等。
        【解决方案6】:

        如果您使用 EF Code First 生成数据库表,请编辑迁移类的 Up 方法,如下所示,以强制您的 UNIQUE KEY 约束忽略 NULL。

        migrationBuilder.Sql(@"CREATE UNIQUE NONCLUSTERED INDEX[IX_Employees_TaskId] ON[dbo].[Employees]([TaskId] ASC)
                                        WHERE [TaskId] IS NOT NULL"
                                        );
        

        然后您可以通过 SQL Server Management Studio 或类似的工具登录到您的数据库来测试您的唯一约束。就像在这种情况下,Employee Table 很乐意在 TaskId 中接受 2 个 NULL 值,尽管它是一个 UNIQUE 列。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-10-02
          • 1970-01-01
          • 2010-11-23
          • 1970-01-01
          • 1970-01-01
          • 2021-05-09
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多