web-dev-qa-db-ja.com

SQL Server:クラスター化インデックス、並べ替え、ページ付け

私のアプリケーションでは、何回か、いくつかのフィールドでページ分けされ、ソートされた結果を表示する必要があります。

たとえば、姓で並べ替えられた単純なユーザーリストです。そのため、また論理的な削除があり、それがマルチテナンシーアプリケーションであるため、通常は次のようなCLUSTERED INDEXを使用します。

_CREATE CLUSTERED INDEX [idx] ON [Users]
(
     IsDeleted ASC,
     [AccountId] ASC,
     [LastName] ASC
)
_

つまり、SELECT TOP(20) * FROM Users WHERE IsDeleted = 0 AND AccountId = xxxのようなページ分割されたクエリは、LastNameでソートされます。並べ替えが保証されているわけではありませんが、実際には常に並べ替えられています。

ただし、ここでキンバリートリップ クラスター化インデックスに関するブログ投稿 を読んで、そのようにするのは恐ろしいことだと彼女は言います。そして、IsDeleted(BIT)フィールドでは、

ただし、CLUSTERED INDEXを一意のIDのみに変更すると、_ORDER BY LastName_の使用を開始する必要があり、実際には非常に時間がかかります。

私のテーブルには数百万のレコード(多くても数千万)があり、一般的に次のように使用されます。

  • データのクエリ。ほとんどの時間。
  • 変更されたデータのみがサブセット_IsDeleted = 0, AccountId = xxxx_にある一括更新/挿入(単一アカウントの削除されていないデータのみが一括更新されます)。

質問:

これらの種類のテーブルに推奨されるインデックス(およびソート方法)は何ですか?

別の例これらの種類のテーブルは、次の列を含む調査結果テーブルですIsDeleted (BIT), AccountId (FK GUID), UserId (FK GUID), QuestionKey (NVARCHAR), AnswerValue (TEXT)、私のCLUSTERED KEYはおそらく_(IsDeleted, AccountId, UserId, QuestionKey)_であり、99%の時間でテーブルにクエリを実行するか、最初の3つのフィールドで一括更新します

_WHERE IsDeleted = 0 
AND AccountId = xxx 
AND UserId = yyy
_

または4つのフィールド:_... AND QuestionKey = 'country'_

編集:

これを行った主な理由の1つは、一括更新とクエリが常に1ページまたは少数のページに制限されているためです。 Identity列があると、ほとんどのページで読み取り/書き込みを行うためのクエリと更新が必要になります。

編集2:

Joe Obbishの例に従ってください。

このクエリ:

_SELECT TOP (20) * 
FROM Users2
WHERE IsDeleted = 0 
AND AccountId = '46FC5693-7446-415A-8626-8937365460D1'
ORDER BY [LastName];
_
  • _(IsDeleted, AccountId, LastName)_に1つのクラスターインデックスがあると、次のようになります。

CPU時間= 3 ms、経過時間= 3 ms。

テーブル 'Users2'。スキャンカウント1、論理読み取り5、物理読み取り4、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

  • 新しいPK ID(NEWID())列に1つのクラスターインデックスがあり(データが内部的にランダムにソートされる)、1つのクラスター化されていない_(IsDeleted, AccountId, LastName)_の結果は次のようになります。

CPU時間= 16 ms、経過時間= 18 ms。

テーブル 'Users2'。スキャンカウント1、論理読み取り533、物理読み取り5、先読み読み取り1240、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

IOと時間に注意してください。データが一緒に保存されていない場合、速度が遅くなり、さらに多くのIOが必要になります。より多くのスペースが必要になる場合がありますが、速度の違いは注目に値します。

5
Diego Jancic

まず、明示的なORDER BYを含めることについての彼の回答で RDFozz が言ったことを繰り返し述べる必要があります。 SQL Serverが 割り当て順序スキャン を実行すると、誤った結果が得られる可能性があります。クエリにORDER BYを含めても、パフォーマンスに影響はありません。やってみませんか?

クエリのパフォーマンスの観点から、必要なインデックスは、一度に返す行の数と、テーブルから実際に必要な列の数によって異なります。

最初に、6つのテナントを持つテーブルに約650万行をスローします。

CREATE TABLE dbo.Users2 (
    IsDeleted Bit NOT NULL,
    [AccountId] UNIQUEIDENTIFIER NOT NULL,
    [LastName] NVARCHAR(50) NOT NULL,
    [UsefulColumn] NVARCHAR(20) NOT NULL,
    [OtherColumns] NVARCHAR(100) NOT NULL
);

CREATE CLUSTERED INDEX [idx] ON [Users2]
(
     IsDeleted ASC,
     [AccountId] ASC,
     [LastName] ASC
);

CREATE TABLE #ids (id INT NOT NULL IDENTITY (0, 1), [AccountId] UNIQUEIDENTIFIER NOT NULL);
INSERT INTO #ids
SELECT TOP 6 NEWID()
FROM master..spt_values;

INSERT INTO [Users2] WITH (TABLOCK)
SELECT 
CASE WHEN t1.number % 10 = 1 THEN 1 ELSE 0 END
, #ids.[AccountId]
, LEFT(REPLACE(CONVERT(NVARCHAR(50), NEWID()), '-', ''), 12)
, REPLICATE(N'Z', 20)
, REPLICATE(N'Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
LEFT OUTER JOIN #ids ON ABS(t1.number % 6) = #ids.id;

DROP TABLE #ids;

-- get an ID: FFA7D6D8-63E8-422B-B5E7-F7020871CDB4
SELECT TOP 1 [AccountId] FROM Users2
WHERE IsDeleted = 0
ORDER BY [AccountId] DESC;

あなたのようなクエリを実行すると:

SELECT TOP (20) * 
FROM Users2
WHERE IsDeleted = 0 
AND AccountId = 'FFA7D6D8-63E8-422B-B5E7-F7020871CDB4'
ORDER BY [LastName];

期待どおりにクラスター化インデックスシークを取得します。

query plan

クラスタ化インデックスを使用するのが最善の選択肢ですか?場合によります。テーブルからすべての列を選択する必要がない場合は、明示的なソートなしで必要なデータを返す、より小さなカバリングインデックスを定義できます。カバリングインデックスを小さくすると、データをさらにページアウトするときのパフォーマンスが向上します。 UsefulColumnだけが必要で、OtherColumns列は必要ないとします。次のインデックスを定義できます。

CREATE NONCLUSTERED INDEX [idx_1] ON [Users2]
(
     [AccountId] ASC,
     [LastName] ASC
) 
INCLUDE ([UsefulColumn]) 
WHERE IsDeleted = 0;

かなり大きいです。私のテストケースでは、データサイズの約28%です。このインデックスの場合、テーブルのクラスター化されたキーを変更してもサイズに大きな影響はないことに注意することが重要です。 SQL Serverは、クラスター化されたキー列が既にインデックスに含まれていない限り、インデックスのリーフノードに格納します。これは簡単なテストで実証できます:

CREATE TABLE dbo.IX_TEST (
    COL1 BIGINT NOT NULL,
    COL2 BIGINT NOT NULL,
    FILLER VARCHAR(6) NOT NULL,
    PRIMARY KEY (COL1)
);

INSERT INTO dbo.IX_TEST WITH (TABLOCK)
SELECT TOP (1000000)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 6)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

EXEC sp_spaceused 'IX_TEST'; -- 96 KB

CREATE INDEX COL1 ON dbo.IX_TEST (COL1)
EXEC sp_spaceused 'IX_TEST'; -- 14032 KB

CREATE INDEX COL2 ON dbo.IX_TEST (COL2)
EXEC sp_spaceused 'IX_TEST'; --- 35920 KB

テーブルに戻ると、UsefulColumn列のみにインデックスを作成した場合、IsDeleted列に約1バイト、AccountIdに16バイト、2 * LastName列の平均長のオーバーヘッド(圧縮なし)が発生します。 、および内部一意名の場合は0または4バイト(姓が重複している場合にのみ発生)。私のテストデータでは、かなりのオーバーヘッドがあります:

1 + 16 + 2 * 12 + 0 = 41バイト

ただし、上記で定義したidx_1インデックスの場合、約1バイトです(IsDeletedの場合は1、一意名の場合は0、重複する姓があまり多くないと想定)。キー列として幅の広い列を使用しているため、インデックスは主に大きくなります。インデックスは現在のクラスタリングキーと同じサイズになりますが、テーブルのクラスタリングキーをより薄い列のセットに変更すると、UsefulColumnでのみ定義されているインデックスのサイズが大幅に減少します。

TOPの値については、カバーするインデックスでニースインデックスシークを取得する必要があります。このクエリ:

SELECT TOP (200000) [LastName], [UsefulColumn] 
FROM Users2
WHERE IsDeleted = 0 AND AccountId = 'FFA7D6D8-63E8-422B-B5E7-F7020871CDB4'
ORDER BY [LastName];

次の計画があります:

covering plan

テーブルのすべての列が必要な場合でも、上記で定義されたインデックスは十分なパフォーマンスを提供します。返されるすべての行のキールックアップを取得します。単一のエンドユーザーの場合、20行でキールックアップを実行しても、目立ちません。テストでは、3 msと18 msの実行時間を確認しました。ただし、並行性の高いワークロードがある場合は、違いが生じる可能性があります。正しく評価できるのはあなただけです。

上記の警告がない場合でも、多くの行を選択すると、パフォーマンスに大きな違いが生じることがあります。

clust compare

最初のクエリには、インデックスの使用を強制するインデックスヒントがあります。クエリオプティマイザーは、そのプランはクラスター化インデックスを使用するプランよりもはるかにコストがかかると考えています。

推測しなければならないのであれば、ページングを実行するためにクラスター化インデックスを使用する必要はおそらくないと思います。テーブルに多数のインデックスがある場合は、ページネーションクエリ用の新しいカバリングインデックスを作成し、より小さな一意の列セットにクラスター化インデックスを定義することでメリットが得られる可能性があります。

7
Joe Obbish

この質問はマルチテナントシステムに関連しているため、このDBA.SEの質問に対して提供した、より詳細な回答をご覧ください。

マルチテナントSQL Serverデータベースの複合主キー

しかし、いくつかの簡単なメモ:

  1. 非クラスター化インデックスがある場合(およびこのクラスター化インデックスの場合)、値を変更できるクラスター化インデックスに列をしないことがおそらく最善ですを指すFKを持つPKです)。クラスタ化インデックスキー列はすべての非クラスタ化インデックスにコピーされるため、これらの列のいずれかの値を変更するには、このテーブルのすべての非クラスタ化インデックスへの書き込み操作が必要です(これがPKである場合のリップル効果は言うまでもありません) FKは、更新する必要があるため、それを指しています)。これはI/Oの問題であるだけでなく、インデックスのデータページを、使用していなかった可能性のあるバッファプール(つまりメモリ)に取り込むことを意味します。また、この操作中にロックされる項目が増えるため、ブロックとデッドロックの範囲が広がります。

  2. マルチテナントの場合、すべてのアプリクエリでWHERE句にその列が必要であるため、最初にTenantIDを使用することをお勧めします。バックエンドメンテナンスレポートまたは社内(つまり、クライアント非依存)レポートではないクエリのみがあり、そこでは少し遅いクエリを処理できますOR処理する非クラスター化インデックスを作成するそれらと。

  3. Unique Clustered Index/PKを作成するための少数の非変更列がない場合は、IDENTITYタイプのINT列を追加するか、必要に応じてBIGINT。このような場合にUNIQUEIDENTIFIERを使用する必要はめったにありません。

2
Solomon Rutzky

まず、ワイドクラスタ化インデックスが不適切である理由は次のとおりです。

  • 他のインデックスはクラスター化インデックス値を含める必要があるため、大きくなります。そして
  • 挿入時にデータがテーブルの最後に追加されず、更新時にデータがテーブル内の位置を変更する可能性があるため、ページ分割が発生する可能性が高くなります。これにより、挿入と更新に時間が追加され、インデックスが断片化されます。

だから、純粋に実用的な用語で言えば、おそらく最初に自分自身に尋ねることは-これはあなたにとって問題なのでしょうか?

潜在的な問題が実際の問題になる前に解決することは常に最善ですが、DBは新しいものではないと思います。大幅な成長が期待されておらず、現在すべてが適切に機能している場合。ディスク領域が不足している状況に遭遇しておらず、それを増やすとコストがかかります。また、再設計によってパフォーマンスに問題が発生することが予想される場合は、現時点で変更が最善の解決策であるかどうかを検討してください。

変更する他のドライバーがある場合、特に一般的なリファクタリングの努力が意味をなすものは何でも、代替案を試すことは論理的なタスクであり、おそらく実行する必要があります。ただし、only変更の動機が「この方法で行うのは悪いこと」である場合、(ステートメントがどの程度正確であっても)リスクがあります。明らかなメリットがない変更を行うために多くの時間と労力を費やしている。あなたがこの上司でない限り、あなたの上司はそれを悪い考えだと思うでしょう。


それはさておき:変更に関する主な懸念は、現在クラスター化インデックスを使用して、基本的に最も便利な順序でデータを並べ替えることです。また、クエリにORDER BYを配置せず、クラスター化インデックスを使用して正しい順序でデータを返すため、現在は(かなりの)時間を節約できます。これは、データが格納される方法だからです。そして、これまでのところ、それはあなたのために働いています。

まず、これは変わる可能性があることに注意してください。 「クラスター化インデックスを含むテーブルから返されたデータはクラスター化インデックスの順序以外で返される可能性がある」という実際のMicrosoftページは見つかりませんでしたが、 multipleページ 説明する[〜#〜]ない[〜#〜]ORDER BYを使用してつまり、データの順序、クラスター化インデックスの保証はありません。現在、SQL Serverエンジンは、たまたまソートされた順序でデータを返すようにクエリを最適化しています。データ(新しいデータの大規模な流入)、サーバー(より多くのCPUを搭載したサーバーへのDBの移動)、またはメンテナンスルーチン(クラスター化インデックスの再構築の頻度が高い)への変更はすべて、この動作に変化をもたらす可能性があります。

ただし、ORDER BY LastNameのパフォーマンスが低い理由についてのヒントを提供する 1つの記事 も見つけました。一見すると、それはあなたが今やっていることをサポートしているように聞こえるかもしれません。そうではありません。特に、クラスター化インデックスはORDER BYwithを使用して並べ替えのパフォーマンスを向上できると述べています。

ただし、ORDER BY LastNameはクラスター化インデックスの順序と一致しません。既存のクエリにORDER BY Is_Deleted, AccountID, LastNameを追加してみることをお勧めします。 ORDER BYはインデックスと一致するため、これを含めてもクエリに著しいペナルティが課されることはありません。私は間違っているかもしれません-オプティマイザは、返されるすべての行でIs_DeletedAccountIDが同じであることを認識している可能性があり、過去のテストでインデックスの順序を使用していますが、インデックスの使用を保証する最善の方法すべての列を含めることを確認することです。これはここでも同じです。

次のステップ-クラスター化インデックスをより狭いインデックスに変更することを本当に検討したい場合は、これを試してみて、(テストシステムではもちろん)パフォーマンスを確認します。クラスター化インデックスを(UserIdに変更します。0と想定し、現在のクラスター化インデックスの定義を使用して非クラスター化インデックスを追加します。同じクエリをテストします。ORDER BYが列と一致するクエリ(および並べ替え)新しいインデックスの方向、それが重要です)次に、クエリが適切に実行されるかどうかを確認します。

結果はおそらく、ページ分割の方法によって異なります。それがクエリに最終的に組み込まれている場合(SQL 2012以降の場合、OFFSET/FETCHメソッドは絶賛されます)、オプティマイザはそれがより速くなることに気づく可能性が高くなります。他の方法よりも非クラスター化インデックスを使用して適切な行を見つけ、実際に必要な20〜30行の残りのデータを検索します。

クエリがインデックスを使用しない場合は、テーブルヒントを使用して強制的にインデックスを作成し、それがオプティマイザの最良の推測よりもパフォーマンスが良いかどうかを確認できます。ただし、オプティマイザの呼び出しをオーバーライドして、データを戻す最善の方法は何かです。それはあなたのデータがどのように変化しても物事を見るので、それは注意して行われるべきです...

一括更新への実際の影響も確認する必要があります。ただし、ほとんどの場合、ユーザーが待たなければならないクエリは、時間外に発生するクエリよりも重要です。それがそれほど速くない場合、通常の夜間の作業で使用可能なウィンドウを超えてプッシュアウトするように強制しない限り、通常はそれで問題ありません。

2
RDFozz