web-dev-qa-db-ja.com

SQL Server-高度に歪んだデータ分散のためのクエリ最適化

これに似たクエリを最適化しようとしています:

select top(1)
    t1.Table1ID,
    t1.Column1,
    t1....
    ....
    t2.Table2ID,
    t2....
    ....
    c.FirstName,
    c.LastName,
    c....
from BigTable1 t1
join BigTable2 t2
    on t1.Table1ID = t2.Table1ID
join Customer c
    on t2.CustomerID = c.CustomerID
join Table4 t4
    on t4.Table4ID = t2.Table4ID
join Table5 t5
    on t5.Table5ID = t1.Table5ID
join Table6 t6
    on t6.Table6ID = t5.Table6ID
where 
    t4.Column1 = @p1
    and t1.Column1 = @p2
    and t3.FirstName = @FirstName
    and t3.LastName = @LastName
    and t6.Column1 = @p5
    and (@p6 is null or t2.Column6 = @p6)
order by t2.Table2ID desc
option(recompile);

BigTable1、BigTable2、Customer-大きなトランザクションテーブル(数億行)、Table4、Table5、Table6は比較的静的で小さなルックアップテーブルです。問題は、これらの大きなテーブルのデータの分布がかなりゆがんでいることです。そのため、このクエリのパフォーマンスは非常に低くなります(実行プランの推定行数は実際の行とは大きく異なります)。これらの大きなテーブルの統計を更新しても効果がありません(ヒストグラムの200ステップでは、データ分布のすべてのスキューをカバーするには不十分です)。たとえば、Customerテーブルには、約50万件のレコードに対応する(FirstName、LastName)の組み合わせがいくつかあります。

そのようなクエリのパフォーマンスを改善する2つのオプションが表示されます。

  1. このクエリを小さいクエリに分割し、中間結果を一時テーブルに保存します。中間結果を一時テーブルに具体化するこのアプローチを使用すると、オプティマイザにカーディナリティ推定の機会を提供できますが、tempdbにかなりの追加の負荷が追加されます(このクエリはかなり頻繁に、1秒に数回実行されるため)。もう1つの欠点は、すべてのパラメーター値の方が高速ではないことです。非定型パラメーター値(元のクエリに数分かかる可能性がある場合、これには数秒しかかかりません)の方がはるかに優れているように見えますが、一意の(またはまれな)行は、一時テーブルを使用したアプローチの方が低速です。
  2. スパイク値のフィルターされた統計を作成します。たとえば、Customerテーブルは次のようになります。

    declare @sql nvarchar(max) = N'', @i int, @N int;
    select top (1000)
    identity(int,1,1) as id,
    FirstName,
    LastName,
    count(*) as cnt
    into #FL
    from Customer
    where 
    FirstName is not null 
    and LastName is not null
    group by
    FirstName,
    LastName
    order by cnt desc;
    
    set @N = @@ROWCOUNT;
    set @i = 1;
    
    while @i <= @N
    begin
        select @sql = 'CREATE STATISTICS Customer_FN_LN_' + cast(id as varchar(10)) + ' ON dbo.Customer(CustomerID) WHERE FirstName = ''' + FirstName + ''' AND LastName = ''' + LastName + ''' WITH FULLSCAN, NORECOMPUTE'
    from #FL
    where id = @i;
    exec sp_executesql @sql;
    set @i = @i + 1;
    end;
    

したがって、これらの3つの大きなテーブルに対してこのフィルターされた統計を作成し、それらの統計を再作成する場合、たとえば、毎週、元のクエリの行推定で問題ないはずです。

以前の経験から、通常は一時テーブルを使用するアプローチを使用しましたが、この場合は、フィルタリングされた統計を使用するアプローチの方が魅力的です。私はまだそれを生産で使用していません、そして私は欠点が何であり得るか知りたいです。

だから私の質問は:

  1. オプティマイザーが高度に歪んだデータ分布に対処するのに役立つ他のアプローチはありますか?
  2. 2番目のアプローチで手動で作成および処理されたフィルターされた統計の欠点は何ですか?
5
Andrei
  1. オプティマイザーが高度に歪んだデータ分布に対処するのに役立つ他のアプローチはありますか?

フィルターされた統計と中間一時テーブルを使用してクエリを分割する(正しく!)が主なオプションですが、インデックス付きビューを使用してどのように役立つかを検討することもできます。適切に実装すると、インデックス付きビューは、ベーステーブルの追加の非クラスター化インデックスとほぼ同じ影響を与えるはずです。

自動マッチングに依存する代わりにWITH (NOEXPAND)を使用すると(Enterprise Editionのみ)、オプティマイザはインデックス付きビューの統計も作成および使用できます。

より一般的には、「安全」または「安全でない」値を事前に特定できる場合、Kimberlyの Building High Performance Stored Procedures で説明されているハイブリッドアプローチ(動的SQLを含む)を検討できます。 L.トリップ.

また、OPTIMIZE FORなどのヒントを含め、それぞれに適切なアプローチを使用して、さまざまなケースに最適化された複数の個別の手順を検討することもできます。

最後に、クエリストア(利用可能な場合)を介した計画ガイドや強制計画があります。

  1. 2番目のアプローチで手動で作成および処理されたフィルターされた統計の欠点は何ですか?

主に、フィルターされた統計が期待するほど頻繁に更新されないことに関する問題。これらの統計を手動で更新することで、これを回避できます。

あなたはすでに再コンパイルしているので、パラメーター化されたクエリでフィルターされた統計の使用に対処する必要があります。

4
Paul White 9

マテリアライズドビューでアプローチを試しましたが、クエリのパフォーマンスは非常に優れており、実行時間は20ミリ秒以内で一貫しており、これは大きな改善です。データ変更によるパフォーマンスの低下も(私の場合)かなり許容できるようです。ただし、ダウンタイムが発生するため、このソリューションを本番環境で使用できるとは思いません。残念ながら、(ONLINE = ON)を指定してビューにクラスター化インデックスを作成することはできません。そして、いくつかの巨大なテーブルを結合するビューにクラスター化インデックスを作成するには、実際にはかなり時間がかかります。私の場合は約30分でした。さらに、インデックス付きビューが既にあるが、基になるテーブルを変更する必要がある場合は、ビューを削除して再作成する必要があります。ここでは、ビューにクラスター化インデックスを最初に作成する問題に戻り、ダウンタイムが発生します。再び。それにもかかわらず、ダウンタイムが問題でなければ、ここでインデックスビューを使用するのが最もパフォーマンスの高いソリューションのように思われるので、ここでソリューションを提供する必要があると思います(もちろん、使用している正確なクエリではありませんが、それはアイデアを与えます)。

-- indexed view:
create view vBigView with schemabinding
as
select
    t2.Table2ID,
    t2.Table1ID,
    t2.CustomerID,
    t1.Column1,
    t2.Table4ID,
    t5.Table5ID,
    t5.Table6ID.
    t2.Column6,
    c.FirstName,
    c.LastName
from BigTable1 t1
join BigTable2 t2
    on t1.Table1ID = t2.Table1ID
join Customer c
    on t2.CustomerID = c.CustomerID
join Table5 t5
    on t5.Table5ID = t1.Table5ID
go
create unique clustered index UQ_vBigView on vBigView(
    Table4ID,
    Column1,
    Table6ID,
    FirstName,
    LastName,
    Column6,
    Table2ID
) with (sort_in_tempdb = on, online = on, maxdop = 4); -- this what I was hoping to do, unfortunately, ONLINE option does not work for clustered indexes on views, so this will throw an error!
go

-- modified query
declare
    @Table4ID tinyint,
    @Table6ID tinyint;

set @Table4ID = (select Table4ID from Table4 where Column1 = @p1); -- Column1 is unique
set @Table6ID = (select Table6ID from Table6 where Column1 = @p5); -- Column1 is unique

:with cte as(
select top (1)
    Table2ID,
    Table1ID,
    CustomerID,
    Column1,
    Table4ID,
    Table5ID,
    Table6ID.
    Column6,
    FirstName,
    LastName
from vBigView with(noexpand)
where 
    Table4ID = @Table4ID
    and Column1 = @p2
    and Table6ID = @Table6ID
    and FirstName = @FirstName
    and LastName = @LastName
    and (@p6 is null or Column6 = @p6)
order by Table2ID desc
)
select
    t.Table1ID,
    t.Column1,
    t....
    ....
    t.Table2ID,
    t2....
    ....
    t.FirstName,
    t.LastName,
    c....
from cte t
join BigTable2 t2
    on t2.Table2ID = t.Table2ID
join Customer c
    on c.CustomerID = t.CustomerID
join Table4 t4
    on t4.Table4ID = t.Table4ID
join Table5 t5
    on t5.Table5ID = t.Table5ID
join Table6 t6
    on t6.Table6ID = t5.Table6ID
option(recompile);
1
Andrei

私があなたの質問を読んでいたとき、私はすぐにフィルター統計について考えました。問題は、再コンパイルのヒントがない限り、クエリがパラメーター化されている場合は使用できないことです。

Erik Darlingによるこの素晴らしい記事をチェックしてください: https://www.brentozar.com/archive/2016/12/filtered-statistics-follow/

0
Mattia Nocerino