web-dev-qa-db-ja.com

varchar(max)、nvarchar(max)、varbinary(max)の列は選択クエリに影響しますか?

次の表を検討してください。

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クエリに影響します。

5
Saeed Neamati

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
);

クラスター化インデックススキャンが使用されます。このテストテーブルには他のインデックスはありません

enter image description here

216バイトのデータサイズ。

また、8つのレコードがある場合でも、ORフィルターがスタックしていることに注意してください。

このテーブルで発生した読み取り:

enter image description here

クレジット 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
);

再び、残余述語を使用したクラスター化インデックススキャン:

enter image description here

ただし、32KBのデータセットを使用します。

ほぼ1000のlob論理読み取りがあるため、

enter image description here

次に、問題の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です。

enter image description here

これを元のBooksテーブルのページと、Textのない新しいテーブルのページまで追跡できます。

元のBooksテーブル:

enter image description here

新しいBookUniqueTokensテーブル

enter image description here

したがって、すべてのページ+(2つのオーバーヘッドページ?)がクラスター化インデックスから読み取られます。

なぜ違いがあるのですか、なぜ違いがそれほど大きくないのですか?結局、データサイズの違いは巨大です(ロブデータ<>ロブデータなし)

Booksデータスペース

enter image description here

BooksWithTextデータスペース

enter image description here

この理由は、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 UniqueTokenUniqueToken + 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データのフィルタリングはサックです。

より大きなデータ型を使用すると、メモリ許可が屋根を通り抜け、パフォーマンスに影響を与える可能性があります。

6
Randi Vertongen

技術的には、データページでより多くのスペースを必要とし、データがより多くのデータページを必要とするものは、簡単に測定できないほど少量であっても、パフォーマンスを低下させます。しかし、より多くのデータページは、より多くのページを読み取るために必要なより多くの操作、より多くのデータページを保持するために必要なより多くのメモリなどを意味します。

したがって、ヒープまたはインデックスをスキャンする場合、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つの場所は次のとおりです。

  1. 明示的なスキーマ名:これはマイナーですが、それでもalwaysスキーマベースのオブジェクトの前にスキーマ名を付けます。したがって、Booksの代わりにdbo.Booksを使用する必要があります。これは、複数のスキーマが使用されていて、ユーザーごとに異なるデフォルトスキーマがある場合に役立つだけでなく、スキーマが明示的に指定されておらず、SQL Serverがいくつかの場所をチェックする必要がある場合に発生するロックを軽減します。

  2. 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]);
    
  3. 文字列比較:UniqueTokenに関して、大文字と小文字を区別しない比較やアクセント記号を区別しない比較を特に必要とせず、このテーブルを作成したデータベース(COLLATEを使用しない)を想定している場合[UniqueToken]列の句)にはデフォルトのバイナリ照合順序がないため、バイナリ比較を使用してUniqueToken値のマッチングのパフォーマンスを向上させることができます。非バイナリ比較では、値ごとに並べ替えキーを作成する必要があり、その並べ替えキーは特定のカルチャ(つまり、Latin1_GeneralFrenchHebrewSyriacなど)の言語規則に基づいています。値がexactlyと同じである必要がある場合、これは多くの追加処理です。したがって、次のようにします。

    1. UniqueTokenの非クラスター化インデックスを削除する
    2. UniqueToken colummnをVARCHAR(100) NOT NULL COLLATE Latin1_General_100_BIN2に変更します(上記の一時テーブルのように)
    3. UniqueTokenで非クラスター化インデックスを再作成します
1
Solomon Rutzky