【问题标题】:What does/should NULL mean along with FK relationships - DatabaseNULL 与 FK 关系是什么/应该是什么意思 - 数据库
【发布时间】:2010-10-13 23:28:47
【问题描述】:

我在我的关系 SQL 数据库中创建 FK 关系时遇到了困难,在工作中进行了简短讨论后,我们意识到我们有最有可能导致问题的可空列。我一直认为 NULL 意味着未分配、未指定、空白等,并且真的从未见过这样的问题。

与我交谈的其他开发人员认为,如果 2 个实体之间确实存在关系,那么您必须创建一个表来连接来自这两个实体的数据...

至少对我来说似乎很直观,对于包含来自另一个表的 ID 的列,如果该列不为空,那么它必须具有来自另一个表的 ID,但如果它是 NULL,那么就是好的,继续前进。这似乎与某些人的说法和建议相矛盾。

处理两个表之间可能存在关系的情况的最佳实践或正确方法是什么,如果指定了一个值,那么它必须在另一个表中...

【问题讨论】:

  • 一个表值——基础或查询(子)表达式——表示每个谓词(语句模板)的关系(船)/关联,它将行映射到命题(语句)。提出真实命题的行进入表格。 DBA 给出基本谓词;查询的表达式确定其结果谓词。 “表之间的关系 [原文如此]”是 FK 约束。约束是为了完整性——所以 DBMS 拒绝无效状态。以声明或命令的方式执行它们——或者不执行。他们不需要记录或查询。但是 SQL 对包含 null 的约束标识符的支持很差。

标签: sql database-design null


【解决方案1】:

我不得不说,即使这显然是可能的,但按照 Jonathon Leffler 的观点,使用连接表有什么问题?

我遇到了这个问题,因为我有完全相同的需求,但我的设计现在明显“更干净”了一个连接表。我的数据库图现在清楚地向我展示了我的字段是可选的,从模式 POV 对我来说效果很好。

然后为了简化我的查询,我只是创建了一个视图 LEFT JOINing 两个表一起,它给出了一个可选连接的外观,但实际上使用了更清晰的数据库结构。在我看来,也使用 ISNULL(MyField, 'None') 可以提供“不存在”附加行设计的好处,但没有痛苦。

鉴于此处提到的要点,我与 DBA 一起讨论这一点 - 当您可以拥有更“稳固”的关系并使其更易于与视图一起使用时,为什么还要使用空列?而且也不需要真正的额外努力。

【讨论】:

    【解决方案2】:

    如果您将 NULL 分配给业务原因,那么您实际上是在重新定义 NULL 在您的域中的含义,并且必须为用户和未来的开发人员记录下来。如果将 NULL 作为外键存在商业原因,那么我建议您像其他人提到的那样做,并添加一个加入记录,其值类似于“N/A”或“未分配”。

    当您的数据库中的 NULL 现在变成多重含义(业务含义、错误或未正确输入)时,也可能会出现复杂情况,这可能会导致问题更难追踪。

    【讨论】:

      【解决方案3】:

      我倾向于采用能够传达该栏目含义的设计。就领域而言,null 可能意味着任何数量的事情。在相关表中输入“不需要”或“未选择”的值至少可以传达目的,而无需询问开发人员或查阅文档。

      【讨论】:

      • 但是以必须确保您的报告不输出“未选择”为代价,如果此列驱动某些事情...确保“未选择”显示在下拉列表的顶部下拉菜单等
      • 无论如何,您必须多次订购这些菜单,因为您可能无法控制如何添加新值。另外,如果出于某种原因您想查看没有关系的项目,您正在查询 null 而不是有意义的值。我同意彼得的观点,null 表示“我不知道”
      【解决方案4】:

      我认为这场辩论是object-relational impedence mismatch 的另一个副产品。基于对关系代数语义的更深入理解,一些 DBA 类型会迂腐地说永远不允许在 FK 中使用 null,但应用程序开发人员会争辩说这使他们的领域层更加优雅。

      “尚未建立”关系的用例是有效的,但对于 null FK,一些人发现它通过引入更复杂的 SQL 功能(特别是 LEFT JOIN)增加了查询的复杂性。

      我见过的一个常见替代解决方案是在每个表中引入一个“空行”或“哨兵行”,其中 pk=0 或 pk=1(基于您的 RDBMS 支持的内容)。这允许您设计一个具有“尚未建立”关系的域层,但也避免引入 LEFT JOIN,因为您保证总会有一些东西可以加入。

      当然,这种方法也需要勤奋,因为您基本上是在权衡 LEFT JOIN 以在查询中检查哨兵行的存在,因此您不会更新/删除它等。无论是否权衡有道理是另一回事。我倾向于同意,仅仅为了避免更高级的连接而重新发明 null 似乎有点愚蠢,但我也在一个应用程序开发人员不会赢得与 DBA 辩论的环境中工作。

      编辑

      我删除了一些“事实问题”的措辞,并试图澄清我所说的“失败”连接是什么意思。 @wcoenen 的示例是我个人最常听到的避免空 FK 的原因。并不是他们像“破碎”那样失败,而是失败——有些人会争辩——坚持最小意外原则。

      另外,我把这个回复变成了一个 wiki,因为我基本上把它从原来的状态砍掉了,并从其他帖子中借来。

      【讨论】:

      • 您的意思是“从车辆 ID 不为空的引擎中选择 *;”,对吗?
      • 不错的答案顺便说一句,很高兴看到这个 POV 的一些推理:)
      • 如果你不能举个例子,你为什么说连接会失败?涉及 NULL FK 的连接的行为完全符合它们应有的行为 - 不会连接任何内容。
      • 同意 cdonner - 我不明白。
      • 我理解你的意思,但我认为加入失败是合适的。这不是空值的问题,更多的是逻辑上(错误)理解从多个表的连接中投影行的含义的问题。
      【解决方案5】:

      我强烈支持在外键中使用 NULL 来表示 OLTP 系统中的非父项的参数,但在决策支持系统中它很少能正常工作。最合适的做法是使用特殊的“不适用”(或类似)值作为子记录(在事实表中)可以链接到的父(在维度表中)。

      这样做的原因是,向下钻取/跨越等的探索性可能会导致用户在仅询问有关指标的更多信息时不了解指标如何变化。例如,如果财务数据集市包含产品销售和其他收入来源的组合,则深入到“产品类型”应该将非产品销售相关数据归类,而不是让这些数字从报告中删除,因为没有从事实表到产品维度表的连接。

      【讨论】:

        【解决方案6】:

        当外键是复合的时,会出现在外键列中允许空值的问题。如果两列之一为空,这意味着什么?另一列是否必须与引用表中的任何内容匹配?使用简单的(单列)外键约束,您可以摆脱空值。

        另一方面,如果两个表之间的关系是有条件的(两个实体可以单独存在,但可能几乎巧合地相关),那么最好使用“连接表”来建模 - 表它包含一个指向被引用表的 FK 和另一个指向引用表的 FK,并且具有自己的主键作为两个 FK 的组合。

        作为连接表的示例,假设您的数据库包含俱乐部和人员表。有些人属于某些俱乐部。加入表将是 club_members 并且将包含引用“people”表的人的 FK,并将包含该人所属俱乐部的另一个 FK,并且 person 和 club 标识符的组合将是主键连接表。 (连接表的另一个名称是“关联”或“关联”表。)

        【讨论】:

        • @Jonathan - 我认为如果你有一个复合外键,那么这个论点本质上是不适用的,因为它根本行不通......不过 +1,非常有帮助。
        【解决方案7】:
        CREATE TABLE [tree]
        {
            [id] int NOT NULL,
            [parent_id] int NULL
        };
        
        ALTER TABLE [tree] ADD CONSTRAINT [FK_tree_tree] FOREIGN KEY([parent_id])
        REFERENCES [tree] ([id]);
        

        这并没有错!根节点将永远有一个 NULL 父节点,这不是“尚未建立”关系的情况。在这里加入也没有问题。

        让根节点指向自身作为父节点以避免 NULL FK 或任何其他创造性解决方法,这意味着现实世界不再在数​​据库中准确建模。

        没有人提到的一个潜在问题是包含大量 NULL 值的列的索引性能。虽然这本身与外键问题无关,但它会使连接性能不佳。

        我确实理解,如果您是使用具有数亿行的超大型数据库的 DBA,您不会想要 NULL 外键,因为它们根本无法执行。然而,事实是,大多数开发人员在他们的一生中永远不会使用如此大的数据库,而今天的数据库可以处理这种情况,只需几十万行。强调一个(可怜的)比喻,我们大多数人都不驾驶 F1 赛车,而我妻子雅阁的自动变速器可以很好地完成它需要做的事情(或者至少,它曾经是,直到几周前它坏了...)。

        【讨论】:

          【解决方案8】:

          这是完全可以接受的,这意味着,如果该列有任何值,它的值必须存在于另一个表中。 (我看到其他答案另有说法,但我不敢苟同。)

          想一想车辆和引擎表,引擎尚未安装在车辆中(因此 VehicleID 为空)。或者一个包含主管列和公司 CEO 的 Employee 表。

          更新:根据 Solberg 的要求,以下是两个具有外键关系的表的示例,显示外键字段值可以为空。

          CREATE TABLE [dbo].[EngineTable](
              [EngineID] [int] IDENTITY(1,1) NOT NULL,
              [EngineCylinders] smallint NOT NULL,
           CONSTRAINT [EngineTbl_PK] PRIMARY KEY NONCLUSTERED 
          (
              [EngineID] ASC
          )WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
          ) ON [PRIMARY]
          
          CREATE TABLE [dbo].[CarTable](
              [CarID] [int] IDENTITY(1,1) NOT NULL,
              [Model] [varchar](32) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
              [EngineID] [int] NULL
           CONSTRAINT [PK_UnitList] PRIMARY KEY CLUSTERED 
          (
              [CarID] ASC
          )WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
          ) ON [PRIMARY]
          
          ALTER TABLE [dbo].[CarTable]  WITH CHECK ADD CONSTRAINT [FK_Engine_Car] FOREIGN KEY([EngineID])
          REFERENCES [dbo].[EngineTable] ([EngineID])
          
          
          Insert Into EngineTable (EngineCylinders) Values (4);
          Insert Into EngineTable (EngineCylinders) Values (6);
          Insert Into EngineTable (EngineCylinders) Values (6);
          Insert Into EngineTable (EngineCylinders) Values (8);
          

          -- 现在进行一些测试:

          Insert Into CarTable (Model, EngineID) Values ('G35x', 3);  -- References the third engine
          
          Insert Into CarTable (Model, EngineID) Values ('Sienna', 13);  -- Invalid FK reference - throws an error
          
          Insert Into CarTable (Model) Values ('M');  -- Leaves null in the engine id field & does NOT throw an error 
          

          【讨论】:

          • 经过测试,发现可以在引用外键的字段中放置空值。我认为我在实践中从未这样做过,但它是合法的 SQL。因此,我删除了我的答案,并为你的答案投了赞成票。
          • @Mark - 你能提供创建带有空值的 FK 的语法吗?
          • Solberg - 给我一分钟。如果 le dorfier 不介意,我将编辑他的示例以添加表格(le dorfier?)
          • 谢谢马克 - 我出去遛狗了。 (我可以自己走路,但如果我带上皮带,我必须带上我的一只狗。):D
          • 以最简单的方式查看它,您有两个正在使用的约束。一个是具有 EngineID 列的 Vehicle 表,该列可能为空,也可能不为空。其次,您可以对 EngineID 列进行 FK 约束,并声明其 EngineID 必须存在于那里。两个单独的约束。
          【解决方案9】:

          假设您需要生成所有客户的报告。每个客户都有一个国家的 FK,国家数据需要包含在报告中。现在假设您允许 FK 为 null,然后执行以下查询:

          SELECT * FROM customer, country WHERE customer.countryID = country.ID
          

          国家/地区 FK 为 null 的任何客户都将从报告中被忽略(您需要使用 LEFT JOIN 来修复它)。我发现这不直观且令人惊讶,因此我不喜欢 NULL FK 并在我的数据库模式中避免使用它们。相反,我使用哨兵值,例如一个特殊的“未知国家”。

          【讨论】:

          • 就我个人而言,我非常非常很少使用除了左连接之外的任何连接类型。我认为它们是默认值。
          【解决方案10】:

          你没看错。对于 FK,NULL 表示没有值(表示没有关系)。如果 FK 中有一个值,它必须与它引用的 PK 中的一个值完全匹配。

          允许这样做不一定是糟糕的设计。如果关系是一对多且可选的,则可以在一侧向表中添加 FK,在多侧引用 PK。

          如果关系是多对多的,则它需要一个自己的表,称为联结表。该表有两个 FK,每个 FK 引用其中一个相关表中的一个 PK。在这种情况下,可以通过简单地从联结表中省略一整行来表示省略的关系。

          有些人的设计是为了避免允许 NULLS 的必要性。这些人将使用联结表进行多对一关系,并在省略关系时省略一行。

          我自己并没有遵循这种做法,但它确实有一定的好处。

          【讨论】:

            【解决方案11】:

            连接表是正确的方法。

            键中的空值表示糟糕的数据库设计。

            空值不是未分配/空/空白/等,它是缺失/未知数据。

            在外键字段中使用空值并不意味着“没有关系”,而是意味着“我不知道是否存在关系”——这显然很糟糕。

            【讨论】:

            • 为什么?我很想知道为什么这是一个糟糕的设计。
            • 您是否在每个表中存储标记行以表示“未设置”外键?
            • 因为 null 不是未分配/空的,它是缺少/未知的数据。使用 null 不是说“没有关系”,而是说“我不知道是否有关系”。
            • 嗯,我认为这是主观的 - null 可以用来表示“没有价值”。而且我不同意“我不知道是否有关系”是“明显不好”——如果你知道怎么办?在非 FK 列中使用 null 表示“未知”是否不好?如果不是,为什么对 FK 不利?
            • 数据库中有未知数据是不好的——因为知道数据是数据库的。我个人对日期值进行了例外处理,仅此而已。
            【解决方案12】:

            如果字段可以为空,我认为空值没有问题。滥用是在该字段中应该有信息时允许空值。

            【讨论】:

              猜你喜欢
              • 2022-12-15
              • 1970-01-01
              • 2014-01-19
              • 2010-10-19
              • 2013-05-09
              • 2011-01-08
              • 2017-05-25
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多