对唯一对的关系建模的最佳方法

h3r*_*ler 10 index database-design unique-constraint

我有两张桌子;一种用于存储thing,一种用于存储relationship两个thing对象之间的。

dbfiddle 示例

认为:

  • AB == BA. 存储两者都是多余的
  • A != B. 一个thing与自身的关系是没有用的
  • 计算之间的关系AB是昂贵但幂等的
CREATE TABLE thing (
    id INT PRIMARY KEY
);
CREATE TABLE relationships (
    thing_one INT REFERENCES thing(id),
    thing_two INT REFERENCES thing(id),
    relationship INT NOT NULL,
    PRIMARY KEY (thing_one, thing_two),
    CHECK (thing_one != thing_two)
);
Run Code Online (Sandbox Code Playgroud)

为了确保我们不会INSERT ABBA

CREATE UNIQUE INDEX unique_pair_ix
    ON relationships (
        least(thing_one, thing_two),
        greatest(thing_one, thing_two)
    );
Run Code Online (Sandbox Code Playgroud)

是否有比示例更好或更有效的方法来存储/建模此数据?

编辑:有多个 DBMS 正在考虑用于更大的应用程序。它们包括 PostgreSQL、MariaDB 和 MySQL。PostgreSQL 是当前的首选。

Kir*_*ers 2

在 SQL Server 2022 中,您现在可以使用GREATEST()andLEAST()函数。Brent Ozar 在他的博客文章中谈到了这一点。

如果您没有使用早于 2022 年的 SQL Server 实例,可能还有其他选择,我不完全确定我有最佳答案。我无法想出任何利用某种hash或其他类型的比较机制的东西。我也在 SQL Server 中运行,所以我无法使用least()andgreatest()函数。请参阅此DBA Stack Exchange Question

然而,我用几种不同的方法做了一些性能测试。我捕获的数据如下:

+---------------------------------------------------------+-----------+--------------------+--------------------+--------------+----------------+-----------------------------+
|                                                         |           | Instead of Trigger | Instead of Trigger |              | After Trigger  | Sum and Absolute Difference |
|                          Event                          | Baseline  |  Case Statements   |     Not Exists     | Indexed View | Only Completed | Computed Persisted Columns  |
+---------------------------------------------------------+-----------+--------------------+--------------------+--------------+----------------+-----------------------------+
| Execution Time                                          | 07:06.510 | 13:46.490          | 08:47.594          | 22:18.911    | 30:38.267      | 11:24:38                    |
| Query Profile Statistics                                |           |                    |                    |              |                |                             |
|   Number of INSERT, DELETE and UPDATE statements        | 125250    | 249500             | 499000             | 250000       |                | 249999                      |
|   Rows affected by INSERT, DELETE, or UPDATE statements | 124750    | 249500             | 374250             | 124750       |                | 124750                      |
|   Number of SELECT statements                           | 0         | 0                  | 0                  | 0            |                | 0                           |
|   Rows returned by SELECT statements                    | 0         | 0                  | 0                  | 0            |                | 0                           |
|   Number of transactions                                | 125250    | 249500             | 499000             | 250000       |                | 249999                      |
| Network Statistics                                      |           |                    |                    |              |                |                             |
|   Number of server roundtrips                           | 1         | 250000             | 250000             | 250000       |                | 250000                      |
|   TDS packets sent from client                          | 6075      | 250000             | 250000             | 250000       |                | 250000                      |
|   TDS packets received from server                      | 462       | 250000             | 250000             | 250000       |                | 250000                      |
|   Bytes sent from client                                | 24882190  | 62068000           | 62568000           | 59568000     |                | 61567990                    |
|   Bytes received from server                            | 1888946   | 76910970           | 8782500            | 67527710     |                | 69783720                    |
| Time Statistics                                         |           |                    |                    |              |                |                             |
|   Client processing time                                | 420901    | 269564             | 18202              | 240341       |                | 238190                      |
|   Total execution time                                  | 424682    | 811028             | 512726             | 1325281      |                | 665491                      |
|   Wait time on server replies                           | 3781      | 541464             | 494524             | 1084940      |                | 427301                      |
+---------------------------------------------------------+-----------+--------------------+--------------------+--------------+----------------+-----------------------------+
Run Code Online (Sandbox Code Playgroud)

该数据会建议以下 3 个选项之一:

  1. 假设您可以使用IDENTITY列作为Primary Key,那么性能最佳的方法似乎是INSTEAD OF Trigger - NOT EXISTS
  2. 假设您不同意使用一IDENTITY列作为Primary Key,并且您可以接受其他 2 列的存在,Persistent Computed Columns那么性能最佳的方法似乎是Persistent Computed Columns
  3. 假设您不同意使用一IDENTITY列作为Primary Key,并且您不同意存在 2 个其他列,Persistent Computed Columns那么最佳执行方法似乎是INDEXED VIEW

整体测试方法的背景

我创建了一个thing与您提供的架构相同的表,并用INT1 到 500 的每个值填充该表。INSERT然后,我为每个组合编写了一堆单个语句thing(使用了 a GO,以便当给定的条目在任一检查中失败时,脚本的其余部分将运行,并且我们可以收集statistics整个过程的语句)。

INSERT INTO relationship (thing_one, thing_two, relationship) VALUES(1, 1, 1 * 1)
GO

INSERT INTO relationship (thing_one, thing_two, relationship) VALUES(1, 2, 1 * 2)
GO

INSERT INTO relationship (thing_one, thing_two, relationship) VALUES(1, 3, 1 * 3)
GO
...
INSERT INTO relationship (thing_one, thing_two, relationship) VALUES(500, 498, 500 * 498)
GO

INSERT INTO relationship (thing_one, thing_two, relationship) VALUES(500, 499, 500 * 499)
GO

INSERT INTO relationship (thing_one, thing_two, relationship) VALUES(500, 500, 500 * 500)
GO

Run Code Online (Sandbox Code Playgroud)

如果操作正确,这将导致从 250,000 次总尝试中添加 124,750 条总记录。

使用 4 种不同的方法重复此过程,以查看性能如何。我还运行了一个“基线”查询,其中仅包含INSERT唯一组合的语句。这样我们就可以了解在测试设置的框架中可能达到的速度有多快。

每种方法的详细信息

  1. INSTEAD Triggerthing_one- 用于对和 的输入值进行排序的 Case 表达式thing_two

通过此实现,我们将获取提供的数据,确保thing_one和的较小值thing_two最终放入thing_one列中,而另一个值(两者中较大的一个)放入列中thing_two。从那里Primary Key将确保仅保留唯一的值。

注意:因为通过这个实现,我们在(我认为这是正确的表结构)上做唯一性Primary Key,所以很难UPDATE通过INSTEAD Trigger. 这是在之前的堆栈溢出问题中提出的,基本结果是您要么需要不同的方法,要么需要一个Identity列。我个人认为,如果存在自然主键,那么添加列并不是一个好主意,Identity就像您在这里所说的那样。

代码Trigger

CREATE TRIGGER InsteadOfInsertTrigger on [dbo].[relationship]
INSTEAD OF INSERT
AS
INSERT INTO [dbo].[relationship]
(
    thing_one,
    thing_two,
    relationship
)
SELECT
CASE
    WHEN I.thing_one <= I.thing_two
        THEN I.thing_one
    ELSE
        I.thing_two
    END
,CASE
    WHEN I.thing_one <= I.thing_two
        THEN I.thing_two
    ELSE
        I.thing_one
    END
,I.relationship
FROM inserted I
GO
Run Code Online (Sandbox Code Playgroud)
  1. INSTEAD Trigger-NOT EXISTS查看

INSERT如果关系已经存在,这是一个停止的触发器。thing_one进入和 的值thing_two没有排序,但希望这不是问题。就像之前的一样,Trigger这仍然有同样的陷阱UPDATES

代码Trigger

CREATE TRIGGER InsteadOfInsertTrigger on [dbo].[relationship]
INSTEAD OF INSERT
AS
INSERT INTO [dbo].[relationship]
(
    thing_one,
    thing_two,
    relationship
)
SELECT
I.thing_one
,I.thing_two
,I.relationship
FROM inserted I
WHERE NOT EXISTS
(
    SELECT 1
    FROM [dbo].[relationship] t
    WHERE (t.thing_one = i.thing_two AND t.thing_two = i.thing_one)
    --This one shouldn't be needed because of the Primary Key
    --AND (t.thing_one = i.thing_one AND t.thing_two = i.thing_two)
)
GO
Run Code Online (Sandbox Code Playgroud)
  1. Unique Indexed View

通过这种方法,我们创建了一个ViewUnique Clustered Index在其上放置了一个。如果添加了重复记录,则此检查将失败,并且更改将回滚。我看到有两种方法可以做到这一点,要么使用CASE像下面这样的表达式,要么使用某种UNION. 在我的测试中,CASE表现要好得多。

View相关Index代码:

CREATE VIEW dbo.relationship_indexedview_view
WITH SCHEMABINDING
AS
    SELECT 
    CASE
        WHEN thing_one <= thing_two
            THEN thing_one
        ELSE
            thing_two
        END as thing_one_sorted,
    CASE
        WHEN thing_one <= thing_two
            THEN thing_two
        ELSE
            thing_one
        END as thing_two_sorted
    FROM [dbo].[relationship_indexedview]
GO

CREATE UNIQUE CLUSTERED INDEX relationship_indexedview_view_unique
    ON dbo.relationship_indexedview_view (thing_one_sorted, thing_two_sorted)
GO
Run Code Online (Sandbox Code Playgroud)
  1. AFTER INSERT and UPDATE Trigger

这里我们有另一个TRIGGER实现可以同时处理INSERTUPDATESINSERTor完成后UPDATE,它会检查是否添加了重复值,ROLLBACK如果找到则执行 a。

注意:这种方法的效果非常差。我在运行约 30 分钟后停止了它,它只添加了预期 124,750 行中的 51,378 行(约INSERT执行命令的 24%)。

Trigger代码:

CREATE TRIGGER AfterTrigger ON [dbo].[relationship]
AFTER INSERT, UPDATE
AS
BEGIN
    IF EXISTS
    (
        SELECT 1
        FROM [dbo].[relationship] T1
            INNER JOIN [dbo].[relationship] T2
                ON T1.thing_one = T2.thing_two
                AND T1.thing_two = T2.thing_one
    )
    BEGIN
        RAISERROR ('Duplicate Relationship Value Added', 16, 1);
        ROLLBACK TRANSACTION; --stops the Insert/Update
    END
END
GO
Run Code Online (Sandbox Code Playgroud)
  1. Sum and Absolute Difference Comparison using Physical Computed Columns

从这个数学堆栈交换问题得到确认后。我们知道给定的关系 (thing_one, thing_two) 或 (thing_two, thing_one) 可以通过查看它们的总和及其差值的绝对值来测试是否唯一。我们可以创建 2Computed Persisted Columns并创建Unique Constraint.

通过对表模式进行少量修改,我们可以确保唯一性,而无需修改INSERT脚本。

唯一的缺点是必须在桌子上多保留 2 列。只要没问题,这似乎是最小的开销之一,并且不存在与基于TRIGGER方法所发现的必须处理主键更改相同的陷阱。

这可能会被推送到一个单独的表或其他一些索引视图,但我还没有对此进行任何测试。

CREATE TABLE relationships (
    thing_one INT REFERENCES thing(id),
    thing_two INT REFERENCES thing(id),
    thing_one_thing_two_sum AS thing_one + thing_two PERSISTED,
    thing_one_thing_two_absolute_difference AS ABS(thing_one - thing_two) PERSISTED,
    relationship INT NOT NULL,
    PRIMARY KEY (thing_one, thing_two),
    CHECK (thing_one != thing_two),
    UNIQUE(thing_one_thing_two_sum, thing_one_thing_two_absolute_difference)
);
Run Code Online (Sandbox Code Playgroud)

希望这有助于设计决策,或者至少是一本有趣的读物。