これに似たクエリを最適化しようとしています:
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つのオプションが表示されます。
スパイク値のフィルターされた統計を作成します。たとえば、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つの大きなテーブルに対してこのフィルターされた統計を作成し、それらの統計を再作成する場合、たとえば、毎週、元のクエリの行推定で問題ないはずです。
以前の経験から、通常は一時テーブルを使用するアプローチを使用しましたが、この場合は、フィルタリングされた統計を使用するアプローチの方が魅力的です。私はまだそれを生産で使用していません、そして私は欠点が何であり得るか知りたいです。
だから私の質問は:
- オプティマイザーが高度に歪んだデータ分布に対処するのに役立つ他のアプローチはありますか?
フィルターされた統計と中間一時テーブルを使用してクエリを分割する(正しく!)が主なオプションですが、インデックス付きビューを使用してどのように役立つかを検討することもできます。適切に実装すると、インデックス付きビューは、ベーステーブルの追加の非クラスター化インデックスとほぼ同じ影響を与えるはずです。
自動マッチングに依存する代わりにWITH (NOEXPAND)
を使用すると(Enterprise Editionのみ)、オプティマイザはインデックス付きビューの統計も作成および使用できます。
より一般的には、「安全」または「安全でない」値を事前に特定できる場合、Kimberlyの Building High Performance Stored Procedures で説明されているハイブリッドアプローチ(動的SQLを含む)を検討できます。 L.トリップ.
また、OPTIMIZE FOR
などのヒントを含め、それぞれに適切なアプローチを使用して、さまざまなケースに最適化された複数の個別の手順を検討することもできます。
最後に、クエリストア(利用可能な場合)を介した計画ガイドや強制計画があります。
- 2番目のアプローチで手動で作成および処理されたフィルターされた統計の欠点は何ですか?
主に、フィルターされた統計が期待するほど頻繁に更新されないことに関する問題。これらの統計を手動で更新することで、これを回避できます。
あなたはすでに再コンパイルしているので、パラメーター化されたクエリでフィルターされた統計の使用に対処する必要があります。
マテリアライズドビューでアプローチを試しましたが、クエリのパフォーマンスは非常に優れており、実行時間は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);
私があなたの質問を読んでいたとき、私はすぐにフィルター統計について考えました。問題は、再コンパイルのヒントがない限り、クエリがパラメーター化されている場合は使用できないことです。
Erik Darlingによるこの素晴らしい記事をチェックしてください: https://www.brentozar.com/archive/2016/12/filtered-statistics-follow/