web-dev-qa-db-ja.com

インデックスの一意性オーバーヘッド

私はオフィスのさまざまな開発者と、インデックスのコスト、および一意性が有益であるかコストがかかるか(おそらく両方)かについて、継続的に議論を重ねてきました。問題の核心は、競合するリソースです。

背景

UniqueオペレーションはBツリーのどこに収まるかを暗黙的にチェックするため、Insertインデックスを維持するための追加コストはないという説明を以前に読んだことがあります。一意でないインデックスで見つかり、一意の修飾子をキーの末尾に追加しますが、それ以外の場合は直接挿入します。この一連のイベントでは、Uniqueインデックスに追加のコストはかかりません。

私の同僚は、UniqueがBツリーの新しい位置へのシーク後の2番目の操作として強制されるため、一意でないインデックスよりも維持にコストがかかると言って、このステートメントと戦っています。

最悪の場合、テーブルのクラスタリングキーであるID列(本質的に一意)を持つテーブルを見ましたが、一意ではないと明示的に述べています。最悪の反対側では、一意性へのこだわりがあり、すべてのインデックスは一意として作成されます。インデックスに対して明示的に一意の関係を定義できない場合は、インデックスの最後にテーブルのPKを追加して、一意性が保証されます。

私は開発チームのコードレビューに頻繁に関与しており、彼らが従うための一般的なガイドラインを提供できるようにする必要があります。はい、すべてのインデックスを評価する必要がありますが、各サーバーに数千のテーブルがあり、テーブルに最大20のインデックスがある5台のサーバーがある場合、特定のレベルの品質を確保するためにいくつかの単純なルールを適用できる必要があります。

質問

一意性は、一意でないインデックスを維持するコストと比較して、Insertのバックエンドに追加コストがありますか?次に、一意性を確保するためにテーブルの主キーをインデックスの最後に追加することの何が問題になっていますか?

テーブル定義の例

create table #test_index
    (
    id int not null identity(1, 1),
    dt datetime not null default(current_timestamp),
    val varchar(100) not null,
    is_deleted bit not null default(0),
    primary key nonclustered(id desc),
    unique clustered(dt desc, id desc)
    );

create index
    [nonunique_nonclustered_example]
on #test_index
    (is_deleted)
include
    (val);

create unique index
    [unique_nonclustered_example]
on #test_index
    (is_deleted, dt desc, id desc)
include
    (val);

Uniqueキーをインデックスの最後に追加する理由の例は、ファクトテーブルの1つにあります。 Identity列であるPrimary Keyがあります。ただし、Clustered Indexは、代わりにパーティションスキーム列であり、その後に一意性のない3つの外部キーディメンションが続きます。このテーブルの選択パフォーマンスはひどいです。Primary Keyを利用するよりも、キールックアップでClustered Indexを使用すると、シーク時間を改善できます。同様の設計に従うが、末尾にPrimary Keyが追加されている他のテーブルは、パフォーマンスが大幅に向上しています。

-- date_int is equivalent to convert(int, convert(varchar, current_timestamp, 112))
if not exists(select * from sys.partition_functions where [name] = N'pf_date_int')
    create partition function 
        pf_date_int (int) 
    as range right for values 
        (19000101, 20180101, 20180401, 20180701, 20181001, 20190101, 20190401, 20190701);
go

if not exists(select * from sys.partition_schemes where [name] = N'ps_date_int')
    create partition scheme 
        ps_date_int
    as partition 
        pf_date_int all 
    to 
        ([PRIMARY]);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.bad_fact_table'))
    create table dbo.bad_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        fk_id int not null,
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_bad_fact_table] clustered (date_int, group_id, group_entity_id, fk_id)
        )
    on ps_date_int(date_int);
go

if not exists(select * from sys.objects where [object_id] = OBJECT_ID(N'dbo.better_fact_table'))
    create table dbo.better_fact_table
        (
        id int not null, -- Identity implemented elsewhere, and CDC populates
        date_int int not null,
        dt date not null,
        group_id int not null,
        group_entity_id int not null, -- member of group
        -- tons of other columns
        primary key nonclustered(id, date_int),
        index [ci_better_fact_table] clustered(date_int, group_id, group_entity_id, id)
        )
    on ps_date_int(date_int);
go
14
Solonotix

私は開発チームのコードレビューに頻繁に関与しており、彼らが従うための一般的なガイドラインを提供できるようにする必要があります。

私が現在関わっている環境には、サーバーが250台、データベースが2500台あります。私は,000データベースのシステムで作業しました。インデックス作成のガイドラインは、名前付け規則などを中心に展開する必要があります。インデックスに含める列の「ルール」ではありません-every個々のインデックスは、その特定のビジネスルールまたはテーブルに触れるコード。

一意性は、一意でないインデックスを維持するコストと比較して、Insertのバックエンドに追加コストがありますか?次に、一意性を確保するためにテーブルの主キーをインデックスの最後に追加することの何が問題になっていますか?

一意でないインデックスの最後に主キー列を追加して、一意の外観をアンチパターンにする。ビジネスルールでデータを一意にする必要がある場合は、一意の制約を列に追加します。一意のインデックスが自動的に作成されます。列にインデックスを付ける場合パフォーマンスの場合、なぜ列をインデックスに追加するのですか?

一意性を強制してもオーバーヘッドが追加されないという仮定が正しいとしても(特定のケースではis n't)、インデックスを不必要に複雑にすることで何を解決していますか?

インデックス定義にUNIQUE修飾子を含めることができるようにインデックスキーの末尾に主キーを追加する特定のインスタンスでは、実際にはディスク上の物理インデックス構造に違いはありません。これは、常に一意である必要があるという点で、Bツリーインデックスキーの構造の性質によるものです。

David Browne がコメントで言及されているように:

すべての非クラスター化インデックスは一意のインデックスとして保存されるため、一意のインデックスへの挿入にextraのコストはかかりません。実際、唯一の追加コストはfailingで候補キーを一意のインデックスとして宣言することであり、これによりクラスター化インデックスキーがインデックスキーに追加されます。

次の 最低限完全で検証可能な例 を例にとります。

USE tempdb;

DROP TABLE IF EXISTS dbo.IndexTest;
CREATE TABLE dbo.IndexTest
(
    id int NOT NULL
        CONSTRAINT IndexTest_pk
        PRIMARY KEY
        CLUSTERED
        IDENTITY(1,1)
    , rowDate datetime NOT NULL
);

2番目のインデックスキー定義の末尾に主キーを追加することを除いて、同一の2つのインデックスを追加します。

CREATE INDEX IndexTest_rowDate_ix01
ON dbo.IndexTest(rowDate);

CREATE UNIQUE INDEX IndexTest_rowDate_ix02
ON dbo.IndexTest(rowDate, id);

次に、テーブルにいくつかの行を追加します。

INSERT INTO dbo.IndexTest (rowDate)
VALUES (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 0, GETDATE()))
     , (DATEADD(SECOND, 1, GETDATE()))
     , (DATEADD(SECOND, 2, GETDATE()));

上記のように、3つの行にはrowDate列の同じ値が含まれ、2つの行には一意の値が含まれています。

次に、ドキュメント化されていないDBCC PAGEコマンドを使用して、各インデックスの物理ページ構造を確認します。

DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE @indexid int;

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix01'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix01 *****************************************';

SELECT @fileid = ddpa.allocated_page_file_id
    , @pageid = ddpa.allocated_page_page_id
FROM sys.indexes i 
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), i.object_id, i.index_id, NULL, 'LIMITED') ddpa
WHERE i.name = N'IndexTest_rowDate_ix02'
    AND ddpa.is_allocated = 1
    AND ddpa.is_iam_page = 0;

PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';
DBCC TRACEON(3604);
DBCC PAGE (@dbid, @fileid, @pageid, 1);
DBCC TRACEON(3604);
PRINT N'*************************************** IndexTest_rowDate_ix02 *****************************************';

Beyond Compareを使用して出力を確認しましたが、割り当てページIDなどの明らかな違いを除いて、2つのインデックス構造は同じです。

enter image description here

上記は、すべてのインデックスに主キーを含めること、およびatを一意として定義することがA Good Thing™であることを意味している可能性があります。私はその仮定を行わず、実際にインデックス内の自然データが既に一意である場合にのみ、インデックスを一意として定義することをお勧めします。

Interwebzには、このトピックに関する優れたリソースがいくつかあります。

参考までに、identity列の単なる存在はnotが一意であることを保証します。列を主キーとして定義するか、一意の制約ensureを指定して、その列に格納されている値が実際に一意であるようにする必要があります。 SET IDENTITY_INSERT schema.table ON;ステートメントを使用すると、identityとして定義された列に一意でない値を挿入できます。

16
Max Vernon

Maxの素晴らしい答え へのアドオンです。

一意でないクラスター化インデックスを作成する場合、SQL ServerはとにかくUniquifierと呼ばれるものをバックグラウンドで作成します。

このUniquifierは4バイト(基本的な32ビット整数)しかないため、プラットフォームに多数のCRUD操作がある場合、このUniquifierは将来的に潜在的な問題を引き起こす可能性があります。したがって、システムに多数のCRUD操作がある場合、使用可能な一意の番号をすべて使い果たして、突然エラーが発生し、テーブルにデータを挿入できなくなります(それが原因で)新しく挿入された行に割り当てる一意の値がなくなりました)。

これが発生すると、次のエラーが表示されます。

The maximum system-generated unique value for a duplicate group 
was exceeded for index with partition ID (someID). 

Dropping and re-creating the index may resolve this;
otherwise, use another clustering key.

エラー666 (上記のエラー)は、一意でないキーの単一のセットのuniquifierが2,147,483,647行を超える場合に発生します。

したがって、このエラーを表示するには、単一のキー値に最大20億行を含めるか、単一のキー値を最大20億回変更する必要があります。そのため、この制限に遭遇する可能性はそれほど高くありません。

5
Chessbrain