2つのテーブルがあります。 1つはthing
を格納するためのもので、もう1つは2つの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)
);
INSERT
AB
およびBA
がないことを確認するために:
CREATE UNIQUE INDEX unique_pair_ix
ON relationships (
least(thing_one, thing_two),
greatest(thing_one, thing_two)
);
例 よりもこのデータを格納/モデル化するより良いまたはより効率的な方法はありますか?
編集:より大きなアプリケーションのために検討されている複数のDBMSがあります。それらには、PostgreSQL、MariaDB、およびMySQLが含まれます。 PostgreSQLが現在の設定です。
これは必要ないかもしれませんし、私が最良の答えを持っているかどうかは完全にはわかりません。ある種のhash
や他のタイプの比較メカニズムを利用するものを思いつくことはできませんでした。 SQL Serverでも実行しているため、least()
関数とgreatest()
関数を使用できません。こちらをご覧ください 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 |
+---------------------------------------------------------+-----------+--------------------+--------------------+--------------+----------------+-----------------------------+
このデータは、3つのオプションのいずれかを示唆しています:
Primary Key
としてIDENTITY
列を使用しても問題がない場合、最もパフォーマンスの高いメソッドはINSTEAD OF Trigger - NOT EXISTS
であると思われます。Primary Key
としてIDENTITY
列を使用することに問題があり、他に2つのPersistent Computed Columns
が存在することに問題がない場合、最もパフォーマンスが高い方法はPersistent Computed Columns
。Primary Key
としてIDENTITY
列を使用することに問題があり、他に2つのPersistent Computed Columns
が存在することに問題があるとすると、最良の方法はINDEXED VIEW
テスト方法全体の背景
あなたが提供したのと同じスキーマでthing
テーブルを作成し、1〜500の各INT
を入力しました。次に、INSERT
の組み合わせごとに一連の単一のthing
ステートメントをスクリプト化しました(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
これが正しく行われると、合計250,000回の試行から合計124,750件のレコードが追加されます。
このプロセスを4つの異なる方法で繰り返し、パフォーマンスがどのようなものかを確認しました。また、一意の組み合わせに対するINSERT
ステートメントのみを含む「ベースライン」クエリを実行しました。このようにして、テストが設定されているフレームワークでどれだけ速く実行できるかについていくつかのアイデアがありました。
各方法の詳細
INSTEAD Trigger
-thing_one
およびthing_two
の入力値を並べ替えるためのケース式。この実装では、提供されたデータを取得し、thing_one
とthing_two
の小さい方の値が最終的にthing_one
列ともう一方(2つのうち大きい方)に配置されるようにしますthing_two
列に配置されました。そこからPrimary Key
は、一意の値のみが残ることを保証します。
[〜#〜]注[〜#〜]:この実装では、Primary Key
(これは正しいテーブル構造)、INSTEAD Trigger
を介してUPDATE
ステートメントを処理することは困難です。これは以前の Stack Overflow Question で尋ねられたものであり、基本的な結果として、別のメソッドまたは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
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
Unique Indexed View
この方法で、View
を作成し、その上にUnique Clustered Index
を配置しました。重複するレコードが追加された場合、このチェックは失敗し、変更はロールバックされます。これを行うには、以下のようなCASE
式を使用する方法と、UNION
を使用する方法の2つがありました。私のテストでは、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
AFTER INSERT and UPDATE Trigger
ここに、TRIGGER
とINSERT
の両方を処理する別のUPDATES
実装があります。 INSERT
またはUPDATE
が終了すると、重複する値が追加されているかどうかを確認し、重複している値が見つかった場合はROLLBACK
を実行します。
[〜#〜]注[〜#〜]:この方法は非常にうまく機能しませんでした。実行してから約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
Sum and Absolute Difference Comparison using Physical Computed Columns
これから確認を得た後 Math Stack Exchange Question 。与えられた関係(thing_one、thing_two)または(thing_two、thing_one)は、合計とそれらの差の絶対値を調べることにより、一意としてテストできることがわかっています。 2 Computed Persisted Columns
を作成し、Unique Constraint
を作成できます。
テーブルスキーマを少し変更するだけで、INSERT
スクリプトを変更しなくても一意性を確保できます。
唯一の欠点は、テーブルにさらに2つの列を保持する必要があることです。それが問題ない限り、これはオーバーヘッドの最小量の1つであるように見え、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)
);
うまくいけば、これは設計の決定に役立つか、少なくとも興味深い読み物です。