次の表を検討してください。
create table Books
(
Id bigint not null primary key identity(1, 1),
UniqueToken varchar(100) not null,
[Text] nvarchar(max) not null
)
この表に100,000冊以上の本があるとしましょう。
これで、このテーブルに挿入する10,000冊の本のデータが与えられました。その一部は重複しています。したがって、最初に重複をフィルタリングしてから、新しい本を挿入する必要があります。
重複をチェックする1つの方法は次のとおりです。
select UniqueToken
from Books
where UniqueToken in
(
'first unique token',
'second unique token'
-- 10,000 items here
)
Text
列の存在はこのクエリのパフォーマンスに影響しますか?もしそうなら、それをどのように最適化できますか?
PS他のデータについても同じ構造です。そして、それはうまくいっていません。友人から、次のようにテーブルを2つのテーブルに分割する必要があると言われました。
create table BookUniqueTokens
(
Id bigint not null primary key identity(1, 1),
UniqueToken varchar(100)
)
create table Books
(
Id bigint not null primary key,
[Text] nvarchar(max)
)
そして、最初のテーブルのみで重複検索アルゴリズムを実行し、両方にデータを挿入する必要があります。このように、テーブルが物理的に分離されているため、パフォーマンスが大幅に向上すると主張しました。彼は[Text]
列は、select
列に対するすべてのUniqueToken
クエリに影響します。
10KレコードのデータセットのIN
句で8つのフィルター述語を使用してクエリを検討してください。
select UniqueToken
from Books
where UniqueToken in
(
'Unique token 1',
'Unique token 2',
'Unique token 3',
'Unique token 4',
'Unique token 5',
'Unique token 6',
'Unique token 9999',
'Unique token 5000'
-- 10,000 items here
);
クラスター化インデックススキャンが使用されます。このテストテーブルには他のインデックスはありません
216バイトのデータサイズ。
また、8つのレコードがある場合でも、OR
フィルターがスタックしていることに注意してください。
このテーブルで発生した読み取り:
クレジット statisticsparser。
クエリの選択部分にText
列を含めると、実際のデータサイズが大幅に変化します:
select UniqueToken,Text
from Books
where UniqueToken in
(
'Unique token 1',
'Unique token 2',
'Unique token 3',
'Unique token 4',
'Unique token 5',
'Unique token 6',
'Unique token 9999',
'Unique token 5000'
-- 10,000 items here
);
再び、残余述語を使用したクラスター化インデックススキャン:
ただし、32KBのデータセットを使用します。
ほぼ1000のlob論理読み取りがあるため、
次に、問題の2つのテーブルを作成し、それらに同じ10kレコードを入力します
Text
なしで同じ選択を実行します。 Books
テーブルを使用すると、99回の論理読み取りがあったことを思い出してください。
select UniqueToken
from BookUniqueTokens
where UniqueToken in
(
'Unique token 1',
'Unique token 2',
'Unique token 3',
'Unique token 4',
'Unique token 5',
'Unique token 6',
'Unique token 9999',
'Unique token 5000'
-- 10,000 items here
)
BookUniqueTokens
の読み取りは、99ではなく67です。
これを元のBooks
テーブルのページと、Text
のない新しいテーブルのページまで追跡できます。
元のBooks
テーブル:
新しいBookUniqueTokens
テーブル
したがって、すべてのページ+(2つのオーバーヘッドページ?)がクラスター化インデックスから読み取られます。
なぜ違いがあるのですか、なぜ違いがそれほど大きくないのですか?結局、データサイズの違いは巨大です(ロブデータ<>ロブデータなし)
Books
データスペース
BooksWithTextデータスペース
この理由は、ROW_OVERFLOW_DATAです。
データが8kbを超えると、データは別のページにROW_OVERFLOW_DATAとして保存されます。
さて、LOBデータが異なるページに格納されている場合、これら2つのクラスター化インデックスのページサイズが異なるのはなぜですか?
これらの各ページを追跡するためにクラスター化インデックスに追加された24バイトのポインターが原因です。結局のところ、SQLサーバーは、LOBデータがどこにあるかを知る必要があります。
彼は、[Text]列がUniqueToken列のすべての選択クエリに影響を与えると主張しました。
そして
テキスト列の存在はこのクエリのパフォーマンスに影響しますか?もしそうなら、それをどのように最適化できますか?
保存されたデータが実際にLob Dataであり、回答で提供されたクエリが使用されている場合
24バイトのポインタにより、オーバーヘッドが発生します。
実行回数/分が非常に高くないことにもよりますが、これは100Kレコードでも無視できると言えます。
このオーバーヘッドは、クラスター化インデックスなど、Text
を含むインデックスが使用された場合にのみ発生することに注意してください。
しかし、クラスター化インデックススキャンが使用され、LOBデータが8kbを超えない場合はどうなりますか?
データが8kbを超えず、UniqueToken
にインデックスがない場合、オーバーヘッドが大きくなる可能性があります。 Text
列を選択しない場合でも。
テキストが137文字しかない場合(すべてのレコード)の10kレコードの論理読み取り:
テーブル 'Books2'。スキャンカウント1、論理読み取り419
この余分なデータがすべてクラスター化インデックスページにあるためです。
繰り返しますが、UniqueToken
のインデックス(Text
列を含めない)は、この問題を解決します。
@David Browne-Microsoftによって指摘されたように、このテキスト列を選択しない場合にクラスター化インデックスにこのオーバーヘッドを追加しないように、データを行外に格納することもできます。
また、テキストを行外に保存したい場合は、別のテーブルを使用せずに強制的に実行できます。 sp_tableoptionで 'row value out of row'オプションを設定するだけです。 docs.Microsoft.com/en-us/sql/relational-databases
TL; DR
指定されたクエリに基づいて、UniqueToken
を含めずにTEXT
にインデックスを付けると、問題が解決するはずです。さらに、IN
ステートメントの代わりに、一時テーブルまたはテーブルタイプを使用してフィルタリングを行います。
編集:
はい、UniqueTokenに非クラスター化インデックスがあります
サンプルクエリはText
列に触れておらず、クエリに基づいて、これはカバリングインデックスになるはずです。
これを以前に使用した3つのテーブルでテストすると(UniqueToken
+ Lob data、Solely UniqueToken
、UniqueToken
+ 137 Char data in nvarchar(max)column)
CREATE INDEX [IX_Books_UniqueToken] ON Books(UniqueToken);
CREATE INDEX [IX_BookUniqueTokens_UniqueToken] ON BookUniqueTokens(UniqueToken);
CREATE INDEX [IX_Books2_UniqueToken] ON Books2(UniqueToken);
非クラスター化インデックスが使用されるため、これらの3つのテーブルの読み取りは同じままです。
Table 'Books'. Scan count 8, logical reads 16, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'BookUniqueTokens'. Scan count 8, logical reads 16, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Books2'. Scan count 8, logical reads 16, physical reads 5, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
追加の詳細
@David Browne-Microsoft
また、テキストを行外に保存したい場合は、別のテーブルを使用せずに強制的に実行できます。 sp_tableoptionで 'row value out of row'オプションを設定するだけです。 docs.Microsoft.com/en-us/sql/relational-databases/
既に作成されたデータに影響を与えるためには、インデックスを再構築する必要があることに注意してください。
@Erik Darlingより
オン
Lobデータのフィルタリングはサックです。
より大きなデータ型を使用すると、メモリ許可が屋根を通り抜け、パフォーマンスに影響を与える可能性があります。
技術的には、データページでより多くのスペースを必要とし、データがより多くのデータページを必要とするものは、簡単に測定できないほど少量であっても、パフォーマンスを低下させます。しかし、より多くのデータページは、より多くのページを読み取るために必要なより多くの操作、より多くのデータページを保持するために必要なより多くのメモリなどを意味します。
したがって、ヒープまたはインデックスをスキャンする場合、NVARCHAR(MAX)
列の存在canを選択しなくても、パフォーマンスに影響します。たとえば、行ごとに5000〜7000バイトがある場合、質問に示されているスキーマでは、それが行内に格納され、より多くのデータページが必要になります。しかし、8100バイト(おおよそ)以上であれば、データはLOBページへのポインタだけで行外に格納されることが保証されるため、それほど悪くはありません。
しかし、あなたの場合、UniqueToken
に非クラスター化インデックスがあることを述べたので、5000〜7000のNVARCHAR(MAX)
列があれば、それはほとんど(またはまったく)重要ではありません。クエリはId
列とUniqueToken
列のみが含まれるインデックスを参照する必要があるため、バイト(行ごとに1ページが発生)。そして、操作すべきスキャンの代わりにシークを行うので、インデックス内のすべてのデータページを読み取るわけではありません。
最後の考慮事項:本当に古いハードウェアがない場合(つまり、RAMおよび/またはディスク/ CPU/RAMを占有する他のプロセス。この場合、このクエリだけでなく、ほとんどのクエリが影響を受けます)がない場合)、その場合、100,000行は多くの行ではありません。実際には、それは多くの行にさえ近くありません。100万行は、ここで大きな違いを生むための多くの行ではありません。
したがって、クエリが実際に非クラスター化インデックスを使用していると仮定すると、問題のNVARCHAR(MAX)
列以外の場所を調べる必要があると思います。これは、-ときどきテーブルを2つのテーブルに分割することが最良の選択ではないということではありません。
私が改善のために検討する3つの場所は次のとおりです。
明示的なスキーマ名:これはマイナーですが、それでもalwaysスキーマベースのオブジェクトの前にスキーマ名を付けます。したがって、Books
の代わりにdbo.Books
を使用する必要があります。これは、複数のスキーマが使用されていて、ユーザーごとに異なるデフォルトスキーマがある場合に役立つだけでなく、スキーマが明示的に指定されておらず、SQL Serverがいくつかの場所をチェックする必要がある場合に発生するロックを軽減します。
IN
リスト:これらは便利ですが、スケーラビリティについては知られていません。 IN
リストは、リスト内の各項目のOR
条件に展開されます。意味:
where UniqueToken in
(
'first unique token',
'second unique token'
-- 10,000 items here
)
になる:
where UniqueToken = 'first unique token'
OR UniqueToken = 'second unique token'
-- 10,000 items here (9,998 more OR conditions)
リストに項目を追加すると、OR
条件が増えます。
IN
リストを動的に作成する代わりに、ローカル一時テーブルを作成し、INSERT
ステートメントのリストを作成します。また、それらすべてをトランザクションにラップして、INSERT
ごとに発生するトランザクションオーバーヘッドを回避します(したがって、10,000トランザクションを1に減らします)。
CREATE TABLE #UniqueTokens
(
UniqueToken VARCHAR(100) NOT NULL
COLLATE Latin1_General_100_BIN2
PRIMARY KEY
);
BEGIN TRAN;
..dynamically generated INSERT INTO #UniqueTokens (UniqueToken) VALUES ('...');
COMMIT TRAN;
これでリストが読み込まれたので、次のように使用して、同じ一連の重複トークンを取得できます。
SELECT bk.[UniqueToken]
FROM dbo.Books bk
INNER JOIN #UniqueTokens tmp
ON tmp.[UniqueToken] = bk.[UniqueToken];
または、10,000の新しいエントリのどれをロードできるかを知りたい場合、それらを挿入できるようにnon-duplicateトークンのリストが本当に必要ですか?その場合は、次のようにします。
SELECT tmp.[UniqueToken]
FROM #UniqueTokens tmp
WHERE NOT EXISTS(SELECT *
FROM dbo.Books bk
WHERE bk.[UniqueToken] = tmp.[UniqueToken]);
文字列比較:UniqueToken
に関して、大文字と小文字を区別しない比較やアクセント記号を区別しない比較を特に必要とせず、このテーブルを作成したデータベース(COLLATE
を使用しない)を想定している場合[UniqueToken]
列の句)にはデフォルトのバイナリ照合順序がないため、バイナリ比較を使用してUniqueToken
値のマッチングのパフォーマンスを向上させることができます。非バイナリ比較では、値ごとに並べ替えキーを作成する必要があり、その並べ替えキーは特定のカルチャ(つまり、Latin1_General
、French
、Hebrew
、Syriac
など)の言語規則に基づいています。値がexactlyと同じである必要がある場合、これは多くの追加処理です。したがって、次のようにします。
UniqueToken
の非クラスター化インデックスを削除するUniqueToken
colummnをVARCHAR(100) NOT NULL COLLATE Latin1_General_100_BIN2
に変更します(上記の一時テーブルのように)UniqueToken
で非クラスター化インデックスを再作成します