确保 SQL 服务器中的两个列值相关

Ensuring that two column values are related in SQL Server

我正在使用 Microsoft SQL Server 2017,我很好奇如何约束特定关系。我在表达上有点困难,所以我更愿意通过一个例子来分享。

考虑以下假设的数据库。


Customers
+---------------+
|  Id  |  Name  |
+---------------+
|  1   |  Sam   |
|  2   |  Jane  |
+---------------+
Addresses
+----------------------------------------+
|  Id  |  CustomerId  |  Address         |
+----------------------------------------+
|  1   |  1           |  105 Easy St     |
|  2   |  1           |  9 Gale Blvd     |
|  3   |  2           |  717 Fourth Ave  |
+------+--------------+------------------+
Orders
+-----------------------------------+
|  Id  |  CustomerId  |  AddressId  |
+-----------------------------------+
|  1   |  1           |  1          |
|  2   |  2           |  3          |
|  3   |  1           |  3          |  <--- Invalid Customer/Address Pair
+-----------------------------------+

请注意,最后的 Order 将客户链接到不属于他们的地址。我正在寻找一种方法来防止这种情况。

(您可能会问为什么我需要 Orders table 中的 CustomerId。明确地说,我认识到 Address 已经为我提供了相同的信息,并且没有无效对的可能性。但是,我更愿意将 Order 扁平化,这样我就不必通过地址来检索客户。)

从我能够找到的相关阅读中,似乎一种方法可能是启用针对用户定义函数的 CHECK 约束。此用户定义的函数将类似于以下内容:

WHERE EXISTS (SELECT 1 FROM Addresses WHERE Id = Order.AddressId AND CustomerId = Order.CustomerId)

虽然我认为这会奏效,但考虑到我 能够找到的文章中的一些 "generality",我并不完全相信这是我的最佳选择。

另一种方法可能是从 Addresses table 中完全删除 CustomerId 列,而是添加另一个 table 和 IdCustomerIdAddressId。然后 Order 将引用 this Id。同样,我不喜欢必须通过辅助 table 来获得 CustomerAddress.

的想法

有没有更简洁的方法来做到这一点?还是我只是把这一切都弄错了?

好问题,但从根本上看,您似乎正在努力为不是外键的内容创建外键约束:

Orders.CustomerId -> Addresses.CustomerId

没有简单的 built-in 方法可以做到这一点,因为通常不会这样做。在理想的 RDBMS 实践中,您应该努力将特定类型的数据封装在它们自己的 tables only 中。换句话说,尽量避免冗余数据。

在上面的示例中,地址所有权在地址 table 和订单 table 中都是冗余的,因此需要额外的检查以保持它们同步。对于更大的数据集,这很容易失控。

您提到:

However, I'd prefer to have an Order flattened such that I don't have to channel through an address to retrieve a customer.

但这就是关系数据库是关系数据库的原因。它这样做是为了使不同的数据可以保持不同并使用相对 ID 进行引用。

我认为最好的解决办法就是简单地放弃这个要求。

换句话说,只需选择:

Customers
+---------------+
|  Id  |  Name  |
+---------------+
|  1   |  Sam   |
|  2   |  Jane  |
+---------------+
Addresses
+----------------------------------------+
|  Id  |  CustomerId  |  Address         |
+----------------------------------------+
|  1   |  1           |  105 Easy St     |
|  2   |  1           |  9 Gale Blvd     |
|  3   |  2           |  717 Fourth Ave  |
+------+--------------+------------------+
Orders
+--------------------+
|  Id  |  AddressId  |
+--------------------+
|  1   |  1          |
|  2   |  3          |
|  3   |  3          |  <--- Valid Order/Address Pair
+--------------------+

话虽如此,为了准确地实现您的目的,您确实有 视图 可用于此类事情:

create view CustomerOrders
as

select  o.Id OrderId,
        a.CustomerId,
        o.AddressId
from    Orders
join    Addresses a on a.Id = o.AddressId

我知道这对于视图来说是一个非常微不足道的 use-case 但我想为其添加一个插件,因为它们经常被忽视并且在组织大数据集时派上用场。使用 WITH SCHEMABINDING 它们也可以被索引以提高性能。

You may ask why I need the CustomerId in the Orders table at all. To be clear, I recognize that the Address already offers me the same information without the possibility of invalid pairs. However, I'd prefer to have an Order flattened such that I don't have to channel through an address to retrieve a customer.

如果遇到性能问题,第一件事就是创建或修改适当的索引。并且 DBMS 通常擅长连接操作(具有适当的索引)。但是,是的,规范化有时可以帮助性能调整。但这应该是最后的手段。如果走那条路,一个人应该真正知道自己在做什么,并且要非常小心,不要在一天结束时造成更多损失,因为那个人已经获得了。我怀疑你在这里没有选择,真的需要走那条路。你可能找错了树。因此,我建议您采用 "normal"、"sane" 方式,只需将 customerid 放入 orders 并创建适当的索引。

但如果你真的坚持,你可以尝试让 (id, customerid) 成为 addresses 中的一个键(具有唯一约束),然后基于它创建一个外键。

ALTER TABLE addresses
            ADD UNIQUE (id,
                        customerid);

ALTER TABLE orders
            ADD FOREIGN KEY (addressid,
                             customerid)
                            REFERENCES addresses
                                       (id,
                                        customerid);