web-dev-qa-db-ja.com

一意のペアの関係をモデル化する最良の方法

2つのテーブルがあります。 1つはthingを格納するためのもので、もう1つは2つのrelationshipオブジェクト間にthingを格納するためのものです。

dbfiddleの例

想定:

  • AB == BA。両方を保存すると冗長になります
  • A != Bthingとそれ自体の関係は役に立たない
  • 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)
);

INSERTABおよびBAがないことを確認するために:

CREATE UNIQUE INDEX unique_pair_ix
    ON relationships (
        least(thing_one, thing_two),
        greatest(thing_one, thing_two)
    );

よりもこのデータを格納/モデル化するより良いまたはより効率的な方法はありますか?

編集:より大きなアプリケーションのために検討されている複数のDBMSがあります。それらには、PostgreSQL、MariaDB、およびMySQLが含まれます。 PostgreSQLが現在の設定です。

9
h3rrmiller

これは必要ないかもしれませんし、私が最良の答えを持っているかどうかは完全にはわかりません。ある種の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つのオプションのいずれかを示唆しています:

  1. Primary KeyとしてIDENTITY列を使用しても問題がない場合、最もパフォーマンスの高いメソッドはINSTEAD OF Trigger - NOT EXISTSであると思われます。
  2. Primary KeyとしてIDENTITY列を使用することに問題があり、他に2つのPersistent Computed Columnsが存在することに問題がない場合、最もパフォーマンスが高い方法はPersistent Computed Columns
  3. 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ステートメントのみを含む「ベースライン」クエリを実行しました。このようにして、テストが設定されているフレームワークでどれだけ速く実行できるかについていくつかのアイデアがありました。

各方法の詳細

  1. INSTEAD Trigger-thing_oneおよびthing_twoの入力値を並べ替えるためのケース式。

この実装では、提供されたデータを取得し、thing_onething_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
  1. INSTEAD Trigger-NOT EXISTSチェック

これは、関係がすでに存在する場合にINSERTを停止したトリガーです。 thing_onething_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
  1. 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
  1. AFTER INSERT and UPDATE Trigger

ここに、TRIGGERINSERTの両方を処理する別の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
  1. 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)
);

うまくいけば、これは設計の決定に役立つか、少なくとも興味深い読み物です。

1
Kirk Saunders