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


14

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

バックグラウンド

操作はBツリーのどこに収まるかを暗黙的にチェックし、一意でないインデックスに重複が見つかった場合は、一意化を追加するため、インデックスUniqueを維持するための追加コストはないという説明を以前読んだ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つにあります。そこでPrimary KeyあることIdentity列。ただし、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

回答:


16

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

現在関与している環境には、2500のデータベースを備えた250のサーバーがあります。私は30,000個のデータベースを持つシステムに取り組んできました。インデックス作成のガイドラインは、インデックスに含める列の「ルール」ではなく、命名規則などを中心に展開する必要があります。個々のインデックスはすべて、テーブルに触れる特定のビジネスルールまたはコードの正しいインデックスになるように設計する必要があります。

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

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

一意性を強制しても余分なオーバーヘッドが追加されないという仮定が正しい場合(特定の場合はそうはありません)、インデックスを不必要に複雑にすることで何を解決しますか?

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

以下のようデイビット・ブラウンはコメントで述べました:

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

以下の最小限完全で検証可能な例を取り上げます。

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つの行には列の同じ値が含まれ、rowDate2つの行には一意の値が含まれます。

次に、ドキュメント化されていない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つのインデックス構造は同じです。

ここに画像の説明を入力してください

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

Interwebzには、このトピックに関する次のような優れたリソースがあります。

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


5

マックスの優れた答えへの単なるアドオンです。

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

これは、Uniquifierお使いのプラットフォームは、CRUD操作の多くを持っている場合、このことから、将来的には潜在的な問題を引き起こす可能性がUniquifier大きいだけで4バイト(基本的な32ビット整数を)です。したがって、システムに多くの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億回変更する必要があります。そのため、この制限に遭遇する可能性はほとんどありません。


隠されたuniquifierがキースペースを使い果たす可能性があることは知りませんでしたが、場合によってはすべてが制限されていると思います。方法CaseIf構造が10レベルに制限されているのと同様に、一意でないエンティティの解決にも制限があることは理にかなっています。あなたの声明によると、これはクラスタリングキーが一意でない場合にのみ適用されるように聞こえます。これはNonclustered Index、クラスタ化キーが問題である場合、またはインデックスにUnique問題がない場合Nonclusteredですか?
Solonotix

一意のインデックスは(私の知る限り)列型のサイズによって制限されます(したがって、BIGINT型の場合、8バイトで作業できます)。また、Microsoftの公式ドキュメントによると、クラスター化インデックスには最大900バイト、非クラスター化には1700バイトが許可されています(非クラスター化インデックスは複数持つことができ、テーブルごとにクラスター化インデックスは1つしか持てないため)。docs.microsoft.com/en-us/sql/sql-server/...
Chessbrain

1
@Solonotix- クラスター化インデックスの一意化子は、非クラスター化インデックスで使用されます。この例のコードを主キーなしで実行すると(代わりにクラスター化インデックスが作成されます)、出力は非一意のインデックスと一意のインデックスの両方で同じであることがわかります。
マックスヴァーノン

-2

インデックスが一意である必要があるかどうか、およびこのアプローチでオーバーヘッドが多いかどうかという問題については検討しません。しかし、いくつかのことがあなたの一般的なデザインに私を悩ませました

  1. dt datetime not null default(current_timestamp)。日時は古い形式またはこれであり、datetime2()およびsysdatetime()を使用することで、少なくともある程度のスペース節約を達成できる場合があります。
  2. #test_index(is_deleted)include(val)にインデックス[nonunique_nonclustered_example]を作成します。これは私を悩ます。どのようにデータにアクセスするのか(私はもっと多くのことを賭けているWHERE is_deleted = 0)、フィルターされたインデックスの使用を見てください。私も、2つのフィルタのインデックスを使用するための1つの検討するwhere is_deleted = 0とのために他のwhere is_deleted = 1

基本的に、これは実際の問題/解決策ではなく仮説をテストするために設計されたコーディング演習のように見えますが、これら2つのパターンは間違いなくコードレビューで探しているものです。


datetimeの代わりにdatetime2を使用して節約できる最大値は1バイトです。精度が3未満の場合、秒の小数部で精度が失われますが、これは常に実行可能な解決策ではありません。提供されたインデックスの例については、私の質問に焦点を当てるためにデザインをシンプルに保ちました。Nonclusteredインデックスは、内部キー検索のデータ列の最後に追加クラスタリング・キーを有することになります。そのため、2つのインデックスは物理的に同じであり、これが私の質問のポイントでした。
Solonotix

規模を大きくすると、1バイトまたは2バイトの保存をすばやく実行できます。また、不正確な日時を使用しているため、精度を下げることができると想定していました。インデックスについても、インデックスの先頭列としてのビット列は不適切な選択として扱うパターンであると述べます。すべてのものと同様に、走行距離は異なる場合があります。悲しいことに、近似モデルの欠点。
トビー

-4

単純にPKを使用して別の小さなインデックスを作成するように見えます。したがって、そのパフォーマンスは高速です。

これは、大規模なデータテーブル(例:マスターデータテーブル)を持つ企業で見られます。誰かが、さまざまなレポートグループのニーズを満たすことを期待して、1つの巨大なクラスター化インデックスを持つことにしました。

しかし、あるグループはそのインデックスの一部しか必要としないかもしれませんが、別のグループは他の部分を必要とするかもしれません。

一方、それを細分化して複数の小さなターゲットインデックスを作成すると、多くの場合、問題が解決します。

そして、それがあなたがしていることのようです。この巨大なクラスター化インデックスが非常にパフォーマンスに優れている場合、PKを使用して、パフォーマンスが向上した(驚くことではない)列の少ない別のインデックスを作成しています。

したがって、分析を実行して、単一のクラスター化インデックスを取得し、特定のジョブが必要とするより小さなターゲットインデックスに分割できるかどうかを把握してください。

インデックスの作成と更新にはオーバーヘッドがあるため、「単一インデックスと複数インデックス」の観点からパフォーマンスを分析する必要があります。しかし、全体的な観点からこれを分析する必要があります。

EG:1つの大規模なクラスター化されたインデックスよりもリソース集約度が低く、複数のより小さなターゲットインデックスを持つ方がリソース集約的です。しかし、その後、バックエンドでターゲットクエリをより迅速に実行し、そこで時間(およびお金)を節約できるなら、それだけの価値があるかもしれません。

そのため、エンドツーエンドの分析を行う必要があります。それが自分の世界に与える影響だけでなく、エンドユーザーに与える影響も調べます。

PK識別子を誤って使用しているように感じます。ただし、1つのインデックス(?)のみを許可するデータベースシステムを使用している可能性がありますが、PKを使用して別のデータベースシステムに忍び込むことができます(b / c最近のすべてのリレーショナルデータベースシステムでは、PKが自動的にインデックス付けされるようです)。ただし、最新のRDBMSでは、複数のインデックスを作成できます。作成できるインデックスの数に制限はありません(1 PKの制限とは対照的です)。

したがって、PKをaltインデックスのように機能させることにより、PKを使い果たします。これは、テーブルが後でその役割で展開される場合に必要になる可能性があります。

それはあなたのテーブルがPKを必要としないと言うことではありません。SOPDBの101は「すべてのテーブルはPKを持つべきです」と言います。しかし、データウェアハウスの状況などでは、テーブルにPKを設定するだけで余分なオーバーヘッドが発生する可能性があります。または、重複エントリを二重に追加しないことを保証するのは神からの送信かもしれません。それは本当にあなたが何をしているのか、なぜそれをしているのかという問題です。

しかし、大規模なテーブルは、インデックスを持つことで間違いなく恩恵を受けます。しかし、単一の大規模なクラスター化インデックスが最適であると仮定するのは最高です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.