使用通常唯一标识行的字段对表进行规范化,但有时为空

Har*_*ley 7 postgresql normalization database-design

如果之前有人问过并回答过这个问题,请原谅我。

我正在粗略地制定一个库存管理系统的架构,将在 PostgreSQL 中实现。我们所有的产品和服务都有一个 SKU。我们的大部分产品来自制造商或分销商,并带有单独的“项目编号”(无论是分销商的目录号、制造商的型号,等等)。然而,并非所有人都有这样的数字。我们有内部制造的小组件,通常没有项目编号。我们的服务没有项目编号。由于这些原因,以下 CREATE TABLE 对我来说很有意义。

场景A:

CREATE TABLE product (
   sku            text PRIMARY KEY,
   name           text UNIQUE NOT NULL, -- alternate key
   price          numeric NOT NULL CHECK (price > 0),
   quantity       numeric NOT NULL CHECK (quantity > 0),
   item_number   text -- hmmm...
);
Run Code Online (Sandbox Code Playgroud)

但是,我对此有两个问题。

  1. 有时(可能有 3% 到 5% 的时间),item_number 实际上等于SKU。也就是说,我的一个供应商特别在他们的产品上贴上了我怀疑不是全球唯一的 SKU,按照他们的项目编号设计。

  2. 无论是否等于 SKU,item_number(如果存在)在几乎所有情况下都足以唯一标识我的小商店域中的产品。

我担心将其标准化为 3NF。如果 item_number 有时为 null,则显然不能将其声明为备用键。但是,从语义上讲,它是一个唯一标识符,在我能想到的每种情况下都存在。那么我上面的表格,其中每个属性在功能上都依赖于非主要属性 item_number每当 item_number 存在时,是否标准化?我想不,但我当然不是专家。我想过做以下事情:

情景B

CREATE TABLE product (
   sku            text PRIMARY KEY REFERENCES product_item_number (sku),
   name           text UNIQUE NOT NULL, -- alternate key
   price          numeric NOT NULL CHECK (price > 0),
   quantity       numeric NOT NULL CHECK (quantity > 0),
);

CREATE TABLE product_item_number (
   sku            text PRIMARY KEY,
   item_number    text
);
Run Code Online (Sandbox Code Playgroud)

由于我确实不需要保留功能依赖项 item_number -> 价格、item_number -> 数量等,因此场景 B 对我来说似乎有点合理。我不会有决定任何其他非主要属性的非主要属性。

我的最终想法是在 item_number 不存在的所有情况下简单地使用 sku 作为项目编号,但我想知道这是否是一个好的做法。

情景C

CREATE TABLE product (
   sku            text PRIMARY KEY,
   name           text UNIQUE NOT NULL, -- alternate key
   price          numeric NOT NULL CHECK (price > 0),
   quantity       numeric NOT NULL CHECK (quantity > 0),
   item_number    text UNIQUE NOT NULL -- alternate key???
);
Run Code Online (Sandbox Code Playgroud)

我对场景 C 的担忧是,在某些情况下,供应商可能会回收具有不同 sku 的目录号(也许?),或者两个制造商都生产“d57-red”或类似的情况。在这种情况下,我想我必须以编程方式在违规 item_numbers 前面加上制造商名称或类似的前缀。

当然,也许我想多了。

谢谢阅读。根据 MDCCL 的评论,有几点说明:

  • SKU 在我的域中将始终是唯一的(供应商提供的少量非全局唯一 SKU 不太可能发生冲突)。
  • item_number 将是一个面向公众的属性,由客户和有时我自己用来识别产品。例如,假设客户跳过我的网站并打电话问我是否有 xyz-white;item_number 有助于消除歧义。项目编号在我的经验中是独一无二的(也就是说,我的库存中没有反例),但这本身并不是规则。有一天我可能会遇到 item_number 名称空间冲突。也许,如果发生这种情况,我会将制造商名称的前三个字母作为 item_number 的前缀。
  • item_numbers 并不总是存在。我想我可以为那些没有的人提供某种“代理 item_number”,但任意的 item_number 会适得其反。正如上面所解释的,如果 item_number 存在,它应该存在以帮助我和我的客户消除产品之间的歧义。如果 item_number 是我自己编造的,他们可能会认为他们看错了产品。我不知道。

MDC*_*CCL 9

只要SKUItemNumber总是意味着独特价值

我认为您已经通过发现从概念上讲ItemNumber是一个可选属性而找到了答案;也就是说,当你确定它适用于每个并通过逻辑电平rows- -represented的发生的每一件产品的实体类型。因此,正如您正确指出的那样,该item_number列不应在表中声明为 ALTERNATE KEY(为简洁起见为 AK)product

在这方面,您的场景 B 是非常合理的,正如以下概念级公式所示:

  • 一个产品可能会或可能不会有一个项目编号

换句话说,ProductItemNumber之间存在一比零或一(1:0/1) 的基数比。

然后,是的,您应该引入一个新表来处理可选列,我同意这product_item_number是一个非常具有描述性的名称。该表应该sku作为其 PRIMARY KEY (PK)进行约束,以确保不会sku像您一样将具有相同值的行插入其中。

值得一提的是,它也product_item_number.sku应该像引用product.sku.

下面是一个示例 SQL-DDL 逻辑级设计,它说明了之前的建议:

-- You should determine which are the most fitting 
-- data types and sizes for all your table columns 
-- depending on your business context characteristics.

-- Also, you should make accurate tests to define
-- the most convenient INDEXing strategies.

CREATE TABLE product ( 
    sku      TEXT    NOT NULL, 
    name     TEXT    NOT NULL, 
    price    NUMERIC NOT NULL, 
    quantity NUMERIC NOT NULL,
    --
    CONSTRAINT product_PK        PRIMARY KEY (sku), 
    CONSTRAINT product_AK        UNIQUE      (name), -- AK.
    CONSTRAINT valid_price_CK    CHECK       (price > 0),
    CONSTRAINT valid_quantity_CK CHECK       (quantity > 0)
); 

CREATE TABLE product_item_number ( 
    sku         TEXT NOT NULL, -- To be constrained as PK and FK to ensure the 1:0/1 correspondence ratio between the relevant rows.
    item_number TEXT NOT NULL, 
    --
    CONSTRAINT product_item_number_PK            PRIMARY KEY (sku),
    CONSTRAINT product_item_number_AK            UNIQUE      (item_number), -- In this context, ‘item_number’ is an AK. 
    CONSTRAINT product_item_number_TO_product_FK FOREIGN KEY (sku) 
        REFERENCES product (sku)  
);
Run Code Online (Sandbox Code Playgroud)

此 db<>fiddle 中在 PostgreSQL 11 上进行了测试

此外,还有另一个概念公式可以指导塑造上述数据库设计:

  • 如果它存在,ItemNumber一的产品必须是唯一的。

因此,该item_number列实际上应该声明为 AK 的位置就在product_item_number表中,因为只有在提供相关值时,所述列才需要唯一性保护,因此必须相应地配置 UNIQUE 和 NOT NULL 约束。

缺失值和“封闭世界的解释”

前面描述的逻辑 SQL-DDL 排列是处理缺失值的关系方法的一个示例,尽管它不是最流行的——或通常的——。这种方法与“封闭世界解释”——或“假设”——有关。采取这一立场,(a) 数据库中记录的信息始终被视为真实,并且 (b) 未记录在其中的信息始终被视为虚假。通过这种方式,可以完全保留已知的事实。

在当前的业务场景中,当用户提供product表中包含的所有数据点时,您必须插入相应的行,并且当且仅当用户提供item_number数据可用时,您还必须插入product_item_number对应的行。如果该item_number值未知或根本不适用,则不要插入product_item_number一行,仅此而已。

使用这种方法,您可以避免在表中保留 NULL 标记/标记——以及我将在下一节详细介绍的逻辑级后果——但您应该意识到这是数据库管理领域中的一个“有争议”的话题。在这一点上,您可能会发现 Stack Overflow 问题的答案很有价值:

流行的做法

然而,我猜想,流行的(或常见的)程序是有一个product包含item_number列的表,而该列将被设置为 NULLable,同时使用 UNIQUE 约束进行定义。在我看来,这种方法会使您的数据库和适用的数据操作操作不那么优雅(例如,在这个出色的 Stack Overflow 答案中显示),但这是一种可能性。

请参阅举例说明此操作过程的连续 DDL 语句:

CREATE TABLE product ( 
    sku         TEXT    NOT NULL, 
    name        TEXT    NOT NULL, 
    price       NUMERIC NOT NULL, 
    quantity    NUMERIC NOT NULL, 
    item_number TEXT    NULL, -- Accepting NULL marks. 
    --
    CONSTRAINT product_PK        PRIMARY KEY (sku), 
    CONSTRAINT product_AK1       UNIQUE      (name), -- AK.
    CONSTRAINT product_AK2       UNIQUE      (item_number), -- Being ‘NULLable’, this is not an AK. 
    CONSTRAINT valid_price_CK    CHECK       (price > 0),
    CONSTRAINT valid_quantity_CK CHECK       (quantity > 0)
);
Run Code Online (Sandbox Code Playgroud)

此 db<>fiddle 中在 PostgreSQL 11 上进行了测试

因此,已建立item_number为可以包含 NULL 的列,从逻辑上讲,说它是 AK 是不正确的。此外,您将存储不明确的NULL 标记——它们不是值,无论 PostgreSQL 文档是否以这种方式标记它们——,因此可以说该表不是已适应数学关系的正确表示,并且规范化规则不能应用于它。

由于 NULL 表示列值是 (1) unknown或 (2) inapplicable,因此不能正确声明所述标记属于item_number值的有效域。如您所知,这种标记说明了一个真实值的“状态”,但它本身并不是一个值,自然不会如此表现——顺便说一下,值得一提的是NULL 在不同的 SQL 数据库管理系统中的行为不同,即使在同一数据库管理系统的不同版本之间。

然后,如果 (i) 某个列的值域和 (ii)由于包含 NULL ,该列所承载的含义并不完全清楚:

  • 如何评估和定义相关的功能依赖?

  • 如何将其识别并声明为 PRIMARY 或 ALTERNATE KEY(如item_number)?

尽管理论实践(例如关于数据操作)都涉及在数据库中保留 NULL 标记的问题,但这是处理丢失数据的方法,您会在绝大多数 SQL 平台上构建的数据库中找到这种方法,因为它允许将可选值的列附加到重要的基表,从而避免创建 (a) 补充表和 (b) 相关任务。

决定

我已经介绍了两种选择,以便您可以自行确定哪一种更适合实现您的目标。


假设SKUItemNumber值最终可以被复制

你的问题中有一些点以特殊的方式引起了我的注意,所以我列出了它们:

  • 有时(可能是 3% 到 5% 的时间),item_number 实际上等于 SKU。也就是说,我的一个供应商特别在他们的产品上贴上了我怀疑不是全球唯一的 SKU,按照他们的项目编号设计。

  • […] 可能存在供应商回收具有不同 sku 的目录号(也许?)的情况,或者两个制造商都生产“d57-red”或类似的情况。在这种情况下,我想我必须以编程方式在违规 item_numbers 前面加上制造商名称或类似的前缀。

  • SKU 在我的域中将始终是唯一的(供应商提供的少量非全局唯一 SKU 不太可能发生冲突)。

这些观点可能会产生显着的影响,因为它们似乎表明:

  • ItemNumber值可以最终成为复制和,这种情况发生的时候,你可能会评估结合两种不同的信息是承担同一列不同的含义。

  • 毕竟,Sku值可能会重复(即使是少量重复的Sku实例)。

在这方面,值得注意的是数据建模练习的两个首要目标是 (1) 确定每个单独的重要数据和 (2) 防止在同一列中保留多个数据。例如,这些因素有助于描述稳定和通用的数据库结构,并有助于避免重复信息——这有助于通过各自的约束保持与业务规则一致的数据值。

处理Sku重复项的替代方法:将manufacturer表引入场景

因此,如果可以在不同的制造商之间共享相同的Sku值,您可以使用表中的复合PK 约束,它将由 (i) 制造商 PK 列和 (ii) 组成。例如:productsku

CREATE TABLE manufacturer (
    manufacturer_number INTEGER  NOT NULL, -- This could be something more meaningful, e.g., ‘manufacturer_code’.
    name                TEXT NOT NULL,
    --
    CONSTRAINT manufacturer_PK PRIMARY KEY (manufacturer_number), 
    CONSTRAINT manufacturer_AK UNIQUE      (name) -- AK.
);

CREATE TABLE product (
    manufacturer_number INTEGER NOT NULL, 
    sku                 TEXT    NOT NULL,
    name                TEXT    NOT NULL, 
    price               NUMERIC NOT NULL,
    quantity            NUMERIC NOT NULL,
    --
    CONSTRAINT product_PK                 PRIMARY KEY (manufacturer_number, sku), -- Composite PK.
    CONSTRAINT product_AK                 UNIQUE      (name), -- AK.
    CONSTRAINT product_TO_manufacturer_FK FOREIGN KEY (manufacturer_number)
        REFERENCES manufacturer (manufacturer_number),
    CONSTRAINT valid_price_CK             CHECK       (price > 0),
    CONSTRAINT valid_quantity_CK          CHECK       (quantity > 0)
);
Run Code Online (Sandbox Code Playgroud)

并且,如果ItemNumber在适用时要求保持唯一性,则该product_item_number表的结构可以如下:

CREATE TABLE product_item_number (
    manufacturer_number INTEGER NOT NULL,  
    sku                 TEXT    NOT NULL,
    item_number         TEXT    NOT NULL,
    --
    CONSTRAINT product_item_number_PK            PRIMARY KEY (manufacturer_number, sku), -- Composite PK.
    CONSTRAINT product_item_number_AK            UNIQUE      (item_number), -- AK.  
    CONSTRAINT product_item_number_TO_product_FK FOREIGN KEY (manufacturer_number, sku)
        REFERENCES product (manufacturer_number, sku)  
);
Run Code Online (Sandbox Code Playgroud)

此 db<>fiddle 中在 PostgreSQL 11 上进行了测试

在情况下ItemNumber没有要求防止重复,你只需删除UNIQUE约束申报这样的列,如在未来的DDL语句:

CREATE TABLE product_item_number (
    manufacturer_number INTEGER NOT NULL,  
    sku                 TEXT    NOT NULL,
    item_number         TEXT    NOT NULL, -- In this case, ‘item_number’ does not require a UNIQUE constraint.
    --
    CONSTRAINT product_item_number_PK            PRIMARY KEY (manufacturer_number, sku), -- Composite PK.
    CONSTRAINT product_item_number_TO_product_FK FOREIGN KEY (manufacturer_number, sku)
        REFERENCES product (manufacturer_number, sku)  
);
Run Code Online (Sandbox Code Playgroud)

在另一方面,假设ItemNumber没有实际意味着避免重复值完全与问候到相关的制造商,你可以建立一个复合UNIQUE约束其中将包括manufacturer_numberitem_number,如下面的代码行所示:

CREATE TABLE product_item_number (
    manufacturer_number INTEGER NOT NULL,  
    sku                 TEXT    NOT NULL,
    item_number         TEXT    NOT NULL,
    --
    CONSTRAINT product_item_number_PK            PRIMARY KEY (manufacturer_number, sku),         -- Composite PK.
    CONSTRAINT product_item_number_AK            UNIQUE      (manufacturer_number, item_number), -- Composite AK.
    CONSTRAINT product_item_number_TO_product_FK FOREIGN KEY (manufacturer_number, sku)          -- Composite FK.
        REFERENCES product (manufacturer_number, sku)  
);
Run Code Online (Sandbox Code Playgroud)

Sku值始终唯一但特定的ItemNumber值可以在不同的制造商之间共享时

如果你能保证Product.Sku永远意味着重复,但一个ItemNumber可能由不同的使用厂家,你可以配置你的数据库这里暴露:

CREATE TABLE manufacturer (
    manufacturer_number INTEGER NOT NULL, 
    name                TEXT    NOT NULL,
    --
    CONSTRAINT manufacturer_PK PRIMARY KEY (manufacturer_number), 
    CONSTRAINT manufacturer_AK UNIQUE      (name) -- AK.
);

CREATE TABLE product ( 
    sku      TEXT    NOT NULL, 
    name     TEXT    NOT NULL, 
    price    NUMERIC NOT NULL, 
    quantity NUMERIC NOT NULL,
    --
    CONSTRAINT product_PK        PRIMARY KEY (sku), 
    CONSTRAINT product_AK        UNIQUE      (name), -- AK. 
    CONSTRAINT valid_price_CK    CHECK       (price > 0),
    CONSTRAINT valid_quantity_CK CHECK       (quantity > 0)
); 

CREATE TABLE product_item_number ( 
    sku                 TEXT    NOT NULL,
    manufacturer_number INTEGER NOT NULL,
    item_number         TEXT    NOT NULL,
    --
    CONSTRAINT product_item_number_PK                 PRIMARY KEY (sku, manufacturer_number),  
    CONSTRAINT product_item_number_AK                 UNIQUE      (manufacturer_number, item_number), -- In this context, ‘manufacturer_number’ and ‘item_number’ compose an AK. 
    CONSTRAINT product_item_number_TO_product_FK      FOREIGN KEY (sku)
        REFERENCES product (sku),  
    CONSTRAINT product_item_number_TO_manufacturer_FK FOREIGN KEY (manufacturer_number) 
        REFERENCES manufacturer (manufacturer_number)  
);
Run Code Online (Sandbox Code Playgroud)

在此 db<>fiddle 中在 PostgreSQL 11 上进行了测试。


物理层面的考虑

我们还没有讨论列的确切类型和大小,product.sku但是,如果它在字节方面“很大”,那么它最终可能会破坏系统的数据检索速度——由于物理抽象级别的各个方面,相关的例如,索引的大小和磁盘空间使用情况—。

通过这种方式,您可能想评估 INTEGER 列的合并,它可以提供比可能的“重”TEXT 更快的响应——但这一切都取决于比较列的精确特征——。product_number正如预期的那样,它很可能代表一个代表记录集的序列中的数值products

包含这一新元素的说明性安排如下:

CREATE TABLE product ( 
    product_number INTEGER NOT NULL,
    sku            TEXT    NOT NULL, 
    name           TEXT    NOT NULL, 
    price          NUMERIC NOT NULL, 
    quantity       NUMERIC NOT NULL,
    --
    CONSTRAINT product_PK        PRIMARY KEY (sku), 
    CONSTRAINT product_AK        UNIQUE      (name), -- AK. 
    CONSTRAINT valid_price_CK    CHECK       (price > 0),
    CONSTRAINT valid_quantity_CK CHECK       (quantity > 0)
); 

CREATE TABLE product_item_number 
( 
    product_number INTEGER NOT NULL,
    item_number    TEXT    NOT NULL,
    --
    CONSTRAINT product_item_number_PK            PRIMARY KEY (product_number),  
    CONSTRAINT product_item_number_AK            UNIQUE      (item_number), -- AK.
    CONSTRAINT product_item_number_TO_product_FK FOREIGN KEY (product_number)
       REFERENCES product (product_number)   
);
Run Code Online (Sandbox Code Playgroud)

我强烈建议在大量数据负载下进行大量测试会话,以确定哪些键更方便——从物理上来说——,始终考虑整体数据库特性(所有表的列数、类型和大小)列、约束和基础索引等)。


类似场景

您感兴趣的业务环境与这些帖子中处理的场景有一定的相似性,因此您可能会发现某些讨论的要点具有相关性。