web-dev-qa-db-ja.com

スーパー/サブタイプ(継承)エンティティのT-SQL / SQL ServerでBEFORE INSERTトリガーをエミュレートする方法は?

これはAzure上にあります。

スーパータイプエンティティといくつかのサブタイプエンティティがあり、後者は各挿入でスーパータイプエンティティのプライマリキーから外部キーを取得する必要があります。 Oracleでは、BEFORE INSERTトリガーを使用してこれを実現します。 SQL Server/T-SQLでこれをどのように実現しますか?

DDL

CREATE TABLE super (
 super_id int IDENTITY(1,1)
 ,subtype_discriminator char(4) CHECK (subtype_discriminator IN ('SUB1', 'SUB2')
 ,CONSTRAINT super_id_pk PRIMARY KEY (super_id)
);
CREATE TABLE sub1 (
 sub_id int IDENTITY(1,1)
,super_id int NOT NULL
,CONSTRAINT sub_id_pk PRIMARY KEY (sub_id)
,CONSTRAINT sub_super_id_fk FOREIGN KEY (super_id) REFERENCES super (super_id)
);

sub1に挿入して、superに実際に値を挿入し、super_idに入れるために生成されたsub1を使用するトリガーを起動することを希望します。

Oracleでは、これは次の方法で実現されます。

CREATE TRIGGER sub_trg
    BEFORE INSERT ON sub1
    FOR EACH ROW
DECLARE
    v_super_id int; //Ignore the fact that I could have used super_id_seq.CURRVAL
BEGIN
    INSERT INTO super (super_id, subtype_discriminator) 
        VALUES (super_id_seq.NEXTVAL, 'SUB1') 
        RETURNING super_id INTO v_super_id;
    :NEW.super_id := v_super_id;
END;

T-SQLにBEFORE INSERT機能がない場合、T-SQLでこれをどのようにシミュレートするかアドバイスしてください。

18
Matthew Moisen

時にはBEFOREトリガーをAFTERトリガーに置き換えることもできますが、挿入が行われる前に値を提供する必要があるため、状況によってはそうではないようです。そのため、 @marc_sが彼のコメントで を示唆しているように、最も近い機能は_INSTEAD OF_トリガー1のようです。

ただし、これら2つのトリガータイプの名前が示すように、BEFOREトリガーと_INSTEAD OF_トリガーには根本的な違いがあることに注意してください。どちらの場合も、トリガーが呼び出されたステートメントによって決定されたアクションが実行されていないときにトリガーが実行されますが、_INSTEAD OF_トリガーの場合、アクションは想定されませんまったく行われます。実行する必要がある実際のアクションは、トリガー自体によって実行する必要があります。これは、もちろん明示的にロールバックしない限り、ステートメントが常に実行されるためのBEFOREトリガー機能とは非常に異なります。

しかし、実際に対処すべき問題がもう1つあります。 Oracleスクリプトが示すように、変換する必要があるトリガーは、SQL Serverでサポートされていない別の機能、_FOR EACH ROW_を使用します。 SQL Serverにも行ごとのトリガーはなく、ステートメントごとのトリガーのみがあります。つまり、挿入されたデータは単一の行ではなく、行setであることに常に留意する必要があります。これにより複雑さが増しますが、おそらく、説明する必要のあるもののリストはおしまいです。

そのため、実際に解決する必要があるのは次の2つです。

  • BEFORE機能を置き換えます。

  • _FOR EACH ROW_機能を置き換えます。

これらを解決しようとする私の試みは以下のとおりです。

_CREATE TRIGGER sub_trg
ON sub1
INSTEAD OF INSERT
AS
BEGIN
  DECLARE @new_super TABLE (
    super_id int
  );
  INSERT INTO super (subtype_discriminator)
  OUTPUT INSERTED.super_id INTO @new_super (super_id)
  SELECT 'SUB1' FROM INSERTED;

  INSERT INTO sub (super_id)
  SELECT super_id FROM @new_super;
END;
_

これが上記の仕組みです:

  1. _sub1_に挿入されるのと同じ行数が最初にsuperに追加されます。生成された_super_id_値は、一時ストレージ(_@new_super_と呼ばれるテーブル変数)に保存されます。

  2. 新しく挿入された_super_id_ sが_sub1_に挿入されました。

それほど難しいことではありませんが、質問で指定した列以外の列が_sub1_にない場合にのみ上記は機能します。他の列がある場合、上記のトリガーはもう少し複雑である必要があります。

問題は、新しい_super_id_ sを挿入されたすべての行に個別に割り当てることです。マッピングを実装する1つの方法は次のようになります。

_CREATE TRIGGER sub_trg
ON sub1
INSTEAD OF INSERT
AS
BEGIN
  DECLARE @new_super TABLE (
    rownum   int IDENTITY (1, 1),
    super_id int
  );
  INSERT INTO super (subtype_discriminator)
  OUTPUT INSERTED.super_id INTO @new_super (super_id)
  SELECT 'SUB1' FROM INSERTED;

  WITH enumerated AS (
    SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rownum
    FROM inserted
  )
  INSERT INTO sub1 (super_id, other columns)
  SELECT n.super_id, i.other columns
  FROM enumerated AS i
  INNER JOIN @new_super AS n
  ON i.rownum = n.rownum;
END;
_

ご覧のとおり、IDENTIY(1,1)列が_@new_user_に追加されるため、一時的に挿入された_super_id_値が1からさらに列挙されます。新しい_super_id_ sおよび新しいデータ行、_ROW_NUMBER_関数を使用してINSERTED行も列挙します。その結果、INSERTEDセットのすべての行を単一の_super_id_にリンクできるようになり、_sub1_に挿入される完全なデータ行を補完できるようになりました。

新しい_super_id_ sが挿入される順序は、割り当てられた順序と一致しない場合があることに注意してください。私はそれを問題ではないと考えました。生成されたすべての新しいsuper行は、IDの保存と同じです。したがって、ここで必要なのは、新しい_super_id_行ごとに1つの新しい_sub1_を取得するだけです。

ただし、superに挿入するロジックがより複雑で、何らかの理由で、どの新しいsub行に対してどの新しい_super_id_が生成されたかを正確に覚えておく必要がある場合、おそらく、このStack Overflowの質問で説明されているマッピング方法を検討したいと思うでしょう。

29
Andriy M

Andriyの提案は少数のレコードのINSERTでうまく機能しますが、 'enumerated'と '@new_super'の両方がインデックス付けされていないため、最終的な結合でテーブル全体のスキャンが行われ、大きな挿入のパフォーマンスが低下します。

これは、次のように@new_superテーブルで主キーを指定することで解決できます。

DECLARE @new_super TABLE (
  row_num INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
  super_id   int
);

これにより、SQLオプティマイザーは 'enumerated'テーブルをスキャンしますが、@ new_superでインデックス付き結合を実行して新しいキーを取得します。

1
Mark S. Allen