SQL Server 2008 R2のレポートクエリの1つでパフォーマンスの問題を修正しようとしています。
見積もりを低くしているクエリの部分を含めました。この部分はさらに他のテーブルと結合されます。この推定値は非常に低いため、さらに結合するとネストされたループになり、クエリが永久に実行されます。
select n.Transactionid
from nath n
WHERE StatusId = 3 and
Date IS NOT NULL and
NOT EXISTS (SELECT 1 FROM nath
WHERE Transactionid= n.Transactionid
AND StatusId = 3
AND HistoryId < n.HistoryId)
ハッシュ一致の推定値は1.17ですが、実際には550Kレコードが出ています。統計はフルスキャンで更新されました。
SQL Server 2014インスタンスの1つでまったく同じクエリを実行したところ、より良い結果が得られました。ハッシュ一致演算子の推定値は557Kでした。その後、トレースフラグ9481を使用して2014年に古いカーディナリティエスティメータを強制的に実行し、推定は1に戻りました。したがって、問題は古いCEが自己結合を推定することと関係があると思います。
SQL Server 2008 R2でトレースフラグ4199を試しましたが、役に立ちませんでした。
実際のテーブル名を表示したくないので、列数を減らし、テーブル名と列名を変えて、同様のテーブルを作成しました。推定値は上記の値よりわずかにずれていますが、さらに大きな問題が解決されません。
SQL Server 2014とTF 9481
(SQL Server 2008 R2テスト環境を持っていません):
SQL Server 2014
この間違った見積もりを修正する方法がありましたらお知らせください。
この問題は、次のスクリプトでシミュレートできます。
create table nat ( c1 int identity(1,1) primary key,c2 int)
declare @a int=1
declare @b int =1
while @a<10000
begin
set @b=1
while @b<=5
begin
insert into nat select @a
set @b=@b+1
end
set @a=@a+1
end
select * from nat a where not exists (select 1 from nat b where b.c2=a.c2
and b.c1<a.c1)
OPTION(QUERYTRACEON 9481); -- estimated no of rows from hash match 1
select * from nat a where not exists (select 1 from nat b where b.c2=a.c2
and b.c1<a.c1) -- estimated no of rows from hash match 49995
SQL Server 2012で上記のクエリを使用していくつかのテストを実行しましたが、トレースフラグ4199を使用して新しいカーディナリティエスティメータの動作を強制することができません。
現在のテスト結果:
2014年に古いカーディナリティの動作を再現できますが、2012年には新しいCEの見積もりを取得できないのはなぜですか。
変更はトレースフラグ4199の一部ではなく、2014年にのみ行われたのですか?
NOT EXISTS
を左結合に変更すると、より適切な推定が得られるようです。
この場合、1つのカーディナリティー推定モデルが他のモデルよりも近い結果を生成する理由は、実際にはそれほど興味深いものではありません。元のCEでは、一致する行が見つからない可能性は非常に小さいと推定されていました。新しいCEは、ほぼ確実であると計算します。どちらも「正しい」ものであり、異なるモデリングの仮定に基づいています。基本的に、複数列の準結合は、単一列の統計情報に基づいて評価するのが難しいです。
クエリが何をしようとしているのか、SQL Serverで利用できる統計情報との互換性を高める方法でクエリを作成する方法について考えるのは、はるかに興味深いことです。
重要な観察は、クエリはグループごとに1つの値を持つ行を返すということです。元のクエリの場合、それは各HistoryId
の最小のTransactionid
値を持つ行です。リプロでは、c1
の異なる値ごとに最小のc2
値を持つ行です。 NOT EXISTS
クエリは、その要件を表す1つの方法にすぎません。
SQL Serverには個別の値(密度)に関する優れた統計情報があるため、グループごとに1つの値が必要であることを明確にするような方法でクエリを記述するだけで済みます。これを行うには、たとえば(再現を使用して)多くの方法があります。
SELECT *
FROM dbo.nat AS N
WHERE N.c1 =
(
SELECT MIN(N2.c1)
FROM dbo.nat AS N2
WHERE N2.c2 = N.c2
);
または、同等に:
SELECT N.*
FROM dbo.nat AS N
JOIN
(
SELECT
N.c2,
MIN(N.c1) AS c1
FROM dbo.nat AS N
GROUP BY
N.c2
) AS J
ON J.c2 = N.c2
AND J.c1 = N.c1;
これにより、2008 R2、2012、および2014(両方のCEモデル)で正確に正しい9999行の見積もりが生成されます。
自然なインデックスを使用して(おそらく一意でもあります):
CREATE INDEX i ON dbo.nat (c2, c1);
計画はさらに簡単です:
インデックスやその他の要因によっては、この非常に単純な計画フォームを常に取得できるとは限りません。基本的なグループ化と結合の操作を使用すると、オプティマイザー(およびそのカーディナリティー推定コンポーネント)から、より複雑な代替手段よりも多くの場合、より良い結果が得られるというのが私の指摘です。
質問のいくつかの誤解を解消するための最後のメモ:「新しいCE」は2014年に導入されました。TF4199は、プランに影響するオプティマイザーの修正を有効にします。 TF 9481は元の(「レガシー」)CEを指定し、2014以降のバージョンでのみ有効です。
1つのアプローチは、クエリを分割することです。主キーを使用して一時テーブルにデータを挿入し、他のクエリでそれに結合するだけです。 2番目のクエリは、一時テーブルから正しい見積もりを取得します。このようなもの:
CREATE TABLE #tmp ( Transactionid INT PRIMARY KEY )
INSERT INTO #tmp ( Transactionid )
SELECT DISTINCT n.Transactionid
FROM dbo.nath n
WHERE n.StatusId = 3
AND [Date] IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM dbo.nath x
WHERE n.Transactionid = x.Transactionid
AND n.StatusId = x.StatusId
AND x.HistoryId < n.HistoryId
)
フィルター処理されたインデックスまたは複数列の統計を試して、違いがあるかどうかを確認することもできますが、推測できるのは列の統計(dbcc show_statistics
):
いくつかの例:
CREATE NONCLUSTERED INDEX _idx
ON [dbo].[nath] ([TransactionId],[HistoryId])
WHERE ([StatusId]) = 3
AND [Date] IS NOT NULL
CREATE STATISTICS _stat_tran ON dbo.nath ( Transactionid, HistoryId, StatusId, [Date] )