web-dev-qa-db-ja.com

パフォーマンスのリファクタリング:テーブルスキャンを回避する

いくつかのテーブルを結合するクエリを含むプロシージャがあり、パフォーマンスの問題があります。

メインテーブル(巨大なテーブル)には、PKといくつかのNCインデックスがあります。

CREATE TABLE [dbo].[TableA]
(
    [TableAID] [bigint] NOT NULL,
    [UserID] [int] NOT NULL,
    [IP1] [tinyint] NOT NULL,
    [IP2] [tinyint] NOT NULL,
    [IP3] [tinyint] NOT NULL,
    [IP4] [tinyint] NOT NULL
    CONSTRAINT [PK_TableA] 
        PRIMARY KEY CLUSTERED  ([TableAID] ASC)
) ON [PRIMARY]


CREATE NONCLUSTERED INDEX [idx_1] ON [dbo].[TableA] 
(
    [UserID] ASC
)


CREATE NONCLUSTERED INDEX [idx_2] ON [dbo].[TableA] 
(
    [IP1] ASC,
    [IP2] ASC,
    [IP3] ASC,
    [IP4] ASC
)

これは、パフォーマンスの低いクエリです。

SELECT DISTINCT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
    FROM [dbo].[TableA] a WITH (NOLOCK) 
    JOIN [dbo].[TableB] b WITH (NOLOCK) ON b.UserID = a.UserID 
    JOIN [dbo].[Tablec] c WITH (NOLOCK) ON b.CountryID = c.CountryID
    JOIN ( 
        SELECT 
            IP1, IP2, IP3, IP4          
            from 
            @IPs 
            ) as ip ON 
                ((ip.IP1 is NULL) OR (ip.IP1=a.IP1)) AND 
                ((ip.IP2 is NULL) OR (ip.IP2=a.IP2)) AND
                ((ip.IP3 is NULL) OR (ip.IP3=a.IP3)) AND
                ((ip.IP4 is NULL) OR (ip.IP4=a.IP4)) 

@IPsテーブルの定義:

DECLARE @IPs TABLE (
    IP1 int,
    IP2 int,
    IP3 int,
    IP4 int
    )


INSERT INTO @IPs(IP1,IP2,IP3,IP4)
SELECT      T.v.value('(IP1/node())[1]', 'int'),
            T.v.value('(IP2/node())[1]', 'int'),
            T.v.value('(IP3/node())[1]', 'int'),
            T.v.value('(IP4/node())[1]', 'int')
FROM @IPAddresses.nodes('//IPAddresses/IPAddress') T(v)

@IPAddressesはxmlです。 xmlがより多くのIPを送信できることがわかりました。これは、IPテーブルの複数行を意味します。

問題は、TableAでの読み取り数です。 IP列のNCインデックスがある場合でも、その結合条件はテーブルスキャンを強制しています...

パフォーマンスを向上させるにはどうすればよいですか?このテーブル/クエリをリファクタリングするにはどうすればよいですか?

このコードを書き換えるためのより簡単でより良い方法があるかどうか私はまだ考えています:

SELECT 
        IP1, IP2, IP3, IP4          
        from 
        @IPs 
        ) as ip ON 
            ((ip.IP1 is NULL) OR (ip.IP1=a.IP1)) AND 
            ((ip.IP2 is NULL) OR (ip.IP2=a.IP2)) AND
            ((ip.IP3 is NULL) OR (ip.IP3=a.IP3)) AND
            ((ip.IP4 is NULL) OR (ip.IP4=a.IP4))

... IPの数が多い場合。

5

これを困難にするいくつかのことがあります。注意しないと、NULLチェックによりインデックスシークが妨げられる可能性があります。また、列がNULLの場合、明らかにそれらに対して検索することはできません。したがって、IP1NULLの場合、4列のインデックスidx_2はあまり役に立ちません。 NULL変数の任意の組み合わせに対して選択的な1つのインデックスを定義することは実際には可能ではないようです。また、SQL Server インデックスシークを続行できません は、不等式述語をシークした後:

同様に、2つの列にインデックスがある場合、最初の列に等価述語がある場合にのみ、インデックスを使用して2番目の列の述語を満たすことができます。

つまり、TINYINTデータ型の境界を使用するトリックは、次のように効果的ではない可能性があります。

a.IP1 >= NULLIF(ip.IP1, 0) AND a.IP1 <= NULLIF(ip.IP1, 255)

それに加えて、私が使用している戦略は、SQL Server 2014で導入された新しいカーディナリティエスティメーターでよりうまく機能するようで、質問にはSQL Server 2008のタグが付けられています。

テーブル変数を行に分割し、一度に1行ずつ処理することを強くお勧めします。これは主にNULLの値を処理するためであり、コメントはほとんどの場合1行しか取得しないことを示唆しているためです。 1行で十分なパフォーマンスが得られ、行が多すぎない場合は問題ありません。これが受け入れられない場合は、一時テーブル(テーブル変数を使用しないでください)の行がNULLであるかどうかを確認し、その場合はコードを分岐できます。

以上のことをすべて踏まえると、IPアドレスの2つがNULLでない限り、クラスター化インデックススキャンを行わなくても非常に優れたパフォーマンスを得ることができるようです。それらの3つがNULLの場合、ほとんどのテーブルが返されます。その時点で、クラスター化インデックススキャンを実行することはおそらく意味があります。

以下は、さまざまなソリューションをテストするためにモックアップしたデータです。 1個ずつ0〜255の整数をランダムに選択して、1億個のIPアドレスを生成しました。実際のIPアドレスの分布はそれほどランダムではありませんが、より良いデータを生成する方法がありませんでした。

CREATE TABLE [dbo].[TableA](
    [TableAID] [bigint] NOT NULL,
    [UserID] [int] NOT NULL,
    [IP1] [tinyint] NOT NULL,
    [IP2] [tinyint] NOT NULL,
    [IP3] [tinyint] NOT NULL,
    [IP4] [tinyint] NOT NULL
CONSTRAINT [PK_TableA] PRIMARY KEY CLUSTERED 
( [TableAID] ASC )
);

-- insert 100 million random IP addresses
INSERT INTO [dbo].[TableA] WITH (TABLOCK)
SELECT TOP (100000000)
      ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    , ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 10000
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
    , ABS(BINARY_CHECKSUM(NEWID()) % 256)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3;

CREATE TABLE [dbo].[TableB] (
    [UserID] [int] NOT NULL,
    FILLER VARCHAR(100),
    PRIMARY KEY (UserId)
);

-- insert 10k users
INSERT INTO [dbo].[TableB] WITH (TABLOCK)
SELECT TOP (10000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

既に持っている非クラスター化インデックスのいずれも作成していないことに注意してください。代わりに、IPピースごとに1つのインデックスを作成します。

CREATE NONCLUSTERED INDEX [idx_IP1] ON [dbo].[TableA] ([IP1] ASC);
CREATE NONCLUSTERED INDEX [idx_IP2] ON [dbo].[TableA] ([IP2] ASC);
CREATE NONCLUSTERED INDEX [idx_IP3] ON [dbo].[TableA] ([IP3] ASC);
CREATE NONCLUSTERED INDEX [idx_IP4] ON [dbo].[TableA] ([IP4] ASC);

インデックスに必要なスペースは重要ではありません。ここに比較があります:

╔═══════════╦═════════════╗
║ IndexName ║ IndexSizeKB ║
╠═══════════╬═════════════╣
║ idx_1     ║     1786488 ║
║ idx_2     ║     1786480 ║
║ idx_IP1   ║     1487616 ║
║ idx_IP2   ║     1487616 ║
║ idx_IP3   ║     1487632 ║
║ idx_IP4   ║     1487608 ║
║ PK_TableA ║     2482056 ║
╚═══════════╩═════════════╝

必要に応じて、行またはページの圧縮を使用することの長所と短所を比較検討して、インデックスのサイズを小さくすることができます。ただし、どのIPピースがNULLになるかがわからず、クラスター化インデックススキャンを回避する必要がある場合は、4つのインデックスに代わる他の方法はありません。私が採用する戦略は、インデックス結合と呼ばれます。非クラスター化インデックスには、クラスター化されたキーTableAIDが含まれており、インデックスを結合することができます。各インデックスの選択性は約0.4%であり、非クラスター化インデックスシークでこれらの行を見つけるのは比較的安価です。すべてのインデックスを結合すると、結果セットが大幅に減少するはずです。その時点で、テーブルに対してクラスター化インデックスシークを実行して、UserIDなどの他の必要な列値を取得できます。

これがクエリです:

DECLARE @ip1 TINYINT = ?;
DECLARE @ip2 TINYINT = ?;
DECLARE @ip3 TINYINT = ?;
DECLARE @ip4 TINYINT = ?;

SELECT DISTINCT a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
FROM [dbo].[TableA] a 
JOIN [dbo].[TableB] b ON b.UserID = a.UserID 
WHERE
((@ip1 is NULL) OR (@ip1=a.IP1)) AND 
((@ip2 is NULL) OR (@ip2=a.IP2)) AND
((@ip3 is NULL) OR (@ip3=a.IP3)) AND
((@ip4 is NULL) OR (@ip4=a.IP4)) 
OPTION (RECOMPILE, QUERYTRACEON 9481);

RECOMPILEヒントを使用して、 パラメータ埋め込み最適化 を利用しています。この最適化は特定のサービスパック(SP4?)でのみ利用できるため、パッチが適用されていることを確認してください。クエリオプティマイザーは、TableAに対する単一のテーブルアクセスを、適切と思われる場合はインデックス結合に分割できます。推定された計画は、ここでは誤解を招く可能性が非常に高いことに注意してください。実際の計画に注目したい。

QUERYTRACEON 9481ヒントは、クエリのバージョンに含めないでください。 SQL Server 2016に対してテストしているため、SQL ServerがレガシーCEを使用するように強制するために使用していますが、これは必要なだけです。

いくつかのテストを実行してみましょう。次のパラメータを使用します。

DECLARE @ip1 TINYINT = 1;
DECLARE @ip2 TINYINT = 102;
DECLARE @ip3 TINYINT = 234;
DECLARE @ip4 TINYINT = 172;

マージ結合とループ結合ができます。

no null query

次のパラメータを使用します。

DECLARE @ip1 TINYINT = NULL;
DECLARE @ip2 TINYINT = 102;
DECLARE @ip3 TINYINT = 234;
DECLARE @ip4 TINYINT = 172;

IP1のインデックスがクエリで使用されていないことを除いて、非常によく似たプランを取得します。クエリはまだ約125ミリ秒で終了します。

次のパラメータを使用します。

DECLARE @ip1 TINYINT = 88;
DECLARE @ip2 TINYINT = NULL;
DECLARE @ip3 TINYINT = NULL;
DECLARE @ip4 TINYINT = NULL;

クエリは約5秒で終了します。クラスタ化インデックススキャンと共にハッシュ結合を取得します。これは必ずしも悪いことではありませんが、多少の努力で回避できます(次の段落を参照)。

3 NULL plan

インデックスの結合を効果的に強制する必要がある場合(おそらく、SQL Server 2008には、私が利用している最適化が欠けている可能性があります)、それは可能ですが、 はるかに複雑 です。

9
Joe Obbish

パフォーマンスの問題について-#tempTableを使用するテーブル変数のサイズに応じて、代わりに選択することをお勧めします。クエリオプティマイザーは、テーブル変数が常に1行(SQL Server 2016では100)を返すことを想定しているため、ネストされたループ結合を作成する可能性があり、追加のオーバーヘッドが追加されます。一時テーブルを使用すると、有効な統計とカーディナリティの推定=より適切な計画が得られます。

また、テーブル変数から行を返さないので、これは検証のみを目的としています。この場合、クエリを次のように変更します。

CREATE table #IPs  (IP1 tinyint,IP2 tinyint, IP3 tinyint, IP4 tinyint)
insert into #IPs values (1,2,3,4),(null,2,3,null)
SELECT  a.UserID, a.IP1, a.IP2, a.IP3, a.IP4
    FROM [dbo].[TableA] a WITH (NOLOCK) 
   WHERE EXISTS (Select 1 from #IPs as b where a.IP1 = b.IP1 or a.IP2 = b.IP2 OR a.IP3 = b.IP3 OR a.IP4 = b.IP4) 

検証目的であれば、結合の代わりにEXISTSを使用します。また、tempdb(統計情報の作業テーブル)にオーバーヘッドを追加する可能性がある個別の値を選択し、それらを並べ替えて、それぞれを選択する手間も省けます。

インデックス

4つの列を含めて作成したものは、クエリでこれらの列をIP1、IP2、IP3のいずれかとしてオプションで使用しているため機能しません。その後、要求された他の残りの列を取得するためにルックアップを実行する必要があるため、クエリオプティマイザーの見積もり代わりにテーブル全体を検索する方が簡単です。

実行できることは、非クラスター化インデックスを変更し、UserID列を次のように含めることです。

CREATE NONCLUSTERED INDEX [idx_2] ON [dbo].[TableA] 
(
    [IP1] ASC,
    [IP2] ASC,
    [IP3] ASC,
    [IP4] ASC
) INCLUDE ([UserID])
3
S4V1N