h3r*_*ler 10 index database-design unique-constraint
我有两张桌子;一种用于存储thing
,一种用于存储relationship
两个thing
对象之间的。
认为:
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
AB
和BA
:
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 是当前的首选。
在 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 个选项之一:
IDENTITY
列作为Primary Key
,那么性能最佳的方法似乎是INSTEAD OF Trigger - NOT EXISTS
。IDENTITY
列作为Primary Key
,并且您可以接受其他 2 列的存在,Persistent Computed Columns
那么性能最佳的方法似乎是Persistent Computed Columns
。IDENTITY
列作为Primary Key
,并且您不同意存在 2 个其他列,Persistent Computed Columns
那么最佳执行方法似乎是INDEXED VIEW
整体测试方法的背景
我创建了一个thing
与您提供的架构相同的表,并用INT
1 到 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
唯一组合的语句。这样我们就可以了解在测试设置的框架中可能达到的速度有多快。
每种方法的详细信息
INSTEAD Trigger
thing_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)
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)
Unique Indexed View
通过这种方法,我们创建了一个View
并Unique 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)
AFTER INSERT and UPDATE Trigger
这里我们有另一个TRIGGER
实现可以同时处理INSERT
和UPDATES
。INSERT
or完成后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)
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)
希望这有助于设计决策,或者至少是一本有趣的读物。