[SQL Server] パーティション化したテーブルのidのユニーク性を保障する方法に関する検討

DB

課題「パーティションテーブルではテーブル内でidがユニークであることを保障できない」

パーティションを導入したテーブルはパーティションキーとidの複合キーが主キーになる

そのため、原理上、テーブル内でidがユニークであることを保障できないと言う懸念点が存在する

また、idにidentity列を指定したとしても、identity列の値から同じ値が採番されるケースが稀にあるそうなので、完全にユニーク性を保障するには、それ以上のセーフティーネットが必要

一つのトランザクションで複数のレコードをINSERTするようなケースの場合には同じID値(IDENTITYプロパティで付与される値)が設定されてしまう可能性があるということです。

SQL ServerのIDENTITYプロパティについて – ITエンジニアの成長ブログ

前提

パーティション導入対象のテーブルは取引履歴系のテーブルを前提とする.

テーブル名称は仮称でtransaction_historyとする.

このうちパーティションキーはtransaction_dateという日付のカラムとする.

パーティション関数

CREATE PARTITION FUNCTION PartitionFunction_Date (DATE)
AS RANGE RIGHT FOR VALUES ('2024-01-01', '2024-04-01', '2024-07-01', '2024-10-01');

パーティションスキーマ

CREATE PARTITION SCHEME PartitionScheme_Date
AS PARTITION PartitionFunction_Date
ALL TO ([PRIMARY]);

パーティション付きtransaction_historyのテーブル定義

CREATE TABLE transaction_history (
   -- idはidentity列に指定し、自動でインクリメントされる
    id BIGINT IDENTITY(1,1) NOT NULL,
    ... 
    transaction_date DATE ,

    -- idとtransaction_dateの複合キーがPKになる
    CONSTRAINT PK_transaction_history PRIMARY KEY CLUSTERED (id, transaction_date)
) ON PartitionScheme_StartDate(transaction_date);

idの重複

上記のテーブル定義中では、idとtransaction_dateの組み合わせがテーブル内で重複していない場合に、PK制約に引っかからずにデータ挿入が可能となる。

そのため以下のようにtransaction_dateが2024-01-01と2024-01-02で異なる場合、テーブル内に同じid=1のレコードを挿入することが可能となる。

[row 1] id=1, transaction_date=2024-01-01
[row 2] id=1, transaction_date=2024-01-02

つまりこ別の日付であればidの重複をデフォルト設定では防げないと言える。

IDENTITY列に指定したIDが重複するケース

ケース1. idとパーティションキーの組み合わせがテーブル内で重複しないレコードをIDENTITY_INSERTした場合

idはIDENTITY列の指定をしているため、idを指定してinsertすることは基本的にできない。

ただし、identity insertをすることで、明示的にidを指定してinsertすることが可能。

例えば、id=1・transaction_date=2024-01-01のレコードが存在する状態で、以下のようにidとtransaction_dateの組み合わせがテーブル内に存在しないレコードをidentity_insertすると、テーブル内でidの重複が発生する。

-- identity insertでは可能
set identity_insert transaction_history on;
insert into transaction_history (id, transaction_date)
values (1, '2024-01-02')
set identity_insert transaction_history off;

-- 基本的にidにはinsertできない
insert into transaction_history (id, transaction_date)
values (1, '2024-01-01')

ただし、identity列に指定したカラムはDBに自動採番してほしいカラムになるので、identity列に指定したカラムにidentity insertでデータ挿入することはSE作業以外では発生しないと思われる。

ケース2. 同一トランザクション内で複数のレコードをINSERTする場合

以下のブログによると「一つのトランザクションで複数のレコードをINSERTするようなケースの場合には同じID値(IDENTITYプロパティで付与される値)が設定されてしまう可能性がある」との記載がある。

SQL ServerのIDENTITYプロパティについて – ITエンジニアの成長ブログ

またマイクロソフトのSQL Serverの記事によると「IDENTITY列で値の一位性を保障するには、ユニーク制約かユニークインデックスを利用する必要がある」ことについて言及されている。

値の一意性: PRIMARY KEY または UNIQUE 制約、または UNIQUE インデックスを使用して、一意性を強制する必要があります。

IDENTITY (プロパティ) (Transact-SQL) – SQL Server

ただし、この事象を意図的に再現することは難しい。

なお、identity列の数値の値は再利用されないとの記載もあるため、稀に重複する場合は挿入時刻がかなり近い値同士で重複する可能性が高いと思われる。

値の再利用: 特定のシードと増分値が指定された特定の ID プロパティでは、ID 値がエンジンによって再利用されることはありません。

classmethod経由でawsにRDS For SQL Serverのidentity列の重複が発生する条件について問い合わせしたところ、「AWS内ではidentity列の重複についての報告はなかった」「検証したが発生しなかった」とのことだった。

identity列の重複防止策「DMLトリガーの利用」

公式おすすめの方法. DMLトリガーの利用

マイクロソフト公式が公表しているパーティション作成対象のテーブルのidの重複を防止するための対策として、DMLトリガーを利用する方法がある。

Special Guidelines for Partitioned Indexes

以下のようにテーブルに対して、insert/updateの操作が行われる際にidの重複チェックを実施し、重複発生時はエラーを出し、トランザクションをロールバックさせることが可能。

ただし、挿入/更新のたびにトリガーが起動するため、性能のオーバーヘッドについて要検証

-- トリガーの作成
CREATE TRIGGER trg_CheckIdUniqueness
ON transaction_history
AFTER INSERT, UPDATE
AS
BEGIN
    -- 挿入または更新された行について、既存の行とIDが重複しているかチェック
    IF EXISTS (
        SELECT 1
        FROM transaction_history
        WHERE id IN (SELECT id FROM inserted)
        AND NOT EXISTS (
            -- 挿入/更新される行が既存の行と同一でないことを確認
            SELECT 1
            FROM deleted
            WHERE deleted.id = transaction_history.id
        )
    )
    BEGIN
        RAISERROR ('Duplicate ID detected in transaction_history.', 16, 1);
        ROLLBACK TRANSACTION; -- トランザクションをロールバックして操作をキャンセル
    END
END;

DMLトリガーで大元のトランザクションがロールバックされることは以下のコードで確認可能

CREATE TABLE TestTable (
    id INT PRIMARY KEY,
    value INT NOT NULL
);

CREATE TRIGGER trg_RollbackTest
ON TestTable
AFTER INSERT
AS
BEGIN
    IF EXISTS (SELECT 1 FROM inserted WHERE value > 10)
    BEGIN
        RAISERROR ('Value cannot be greater than 10. Rolling back transaction.', 16, 1);
        ROLLBACK TRANSACTION;
    END
END;

INSERT INTO TestTable (id, value) VALUES (1, 5);
SELECT * FROM TestTable; -- このクエリではid 1, value 5のレコードが表示される

INSERT INTO TestTable (id, value) VALUES (2, 15);
SELECT * FROM TestTable; -- このクエリでは引き続きid 1, value 5のみが表示され、id 2のレコードは表示されない
Msg 50000, Level 16, State 1, Procedure trg_RollbackTest, Line 8
Value cannot be greater than 10. Rolling back transaction.
Msg 3609, Level 16, State 1, Line 2
The transaction ended in the trigger. The batch has been aborted.

利用できない方法. idにのみユニーク制約をはる

以下のようにidにユニーク制約をかけることはエラーが発生するためできない。

ALTER TABLE transaction_history
ADD CONSTRAINT UC_id UNIQUE (id);

この方法は以下のエラーが出るため利用不可能(2025/01/22)

Msg 1908, Level 16, State 1, Line 2
Column 'transaction_date' is partitioning column of the index 'UQ_transaction_history'. Partition columns for a unique index must be a subset of the index key.

「transaction_date」列はインデックス「UQ_transaction_history」の分割列です。一意のインデックスのための分割列は、インデックスキーのサブセットでなければなりません。

Msg 1750, Level 16, State 1, Line 2
Could not create constraint or index. See previous errors.
データベース エンジンイベントとエラー (1000 から 1999) - SQL Server
SQL Server データベース エンジン イベントのエラー メッセージの説明については、この SQL Server エラー コードの一覧 (1000 ~ 1999) を参照してください。

他の方法. 定期ジョブでidのユニーク性をチェックする

定期ジョブを作成し、一括でidのユニーク性をチェックする方法。

ただし、この方法だと即時でid重複を検出することができないため、あくまで重複被害が拡大しないことを確認するための方法

まとめ

  • パーティション化したテーブルではidのユニーク性をデフォルトでは保証できない
  • identity列は稀に重複する可能性がある
  • DMLトリガーによってid重複は防止可能だが、性能検証が必要
  • 定期ジョブでidのユニーク性をチェックすることで、id重複時の被害拡大を防止することが可能

参考

IDENTITY (プロパティ) (Transact-SQL) – SQL Server

SQL ServerのIDENTITYプロパティについて – ITエンジニアの成長ブログ

Dealing with Unique Columns when Using Table Partitioning | Microsoft Community Hub
First published on MSDN on Apr 02, 2013 Recently, I had the opportunity to present table and index partitioning and a fo...
Special Guidelines for Partitioned Indexes

タイトルとURLをコピーしました