要するに、非常に大きな人々のテーブルからの値で人々の小さなテーブルを更新しています。最近のテストでは、この更新の実行に約5分かかります。
私たちは可能な限り最も賢い最適化のように思われるものに偶然出会いました。同じクエリが2分未満で実行され、同じ結果が完全に生成されます。
これがクエリです。最後の行は「最適化」として追加されます。クエリ時間が大幅に減少するのはなぜですか?何かが足りませんか?これは将来的に問題を引き起こす可能性がありますか?
UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
テクニカルノート:テストする文字のリストにはさらに数文字が必要になる場合があることを認識しています。また、「DIFFERENCE」を使用した場合のエラーの明らかなマージンも認識しています。
クエリプラン(通常):https://www.brentozar.com/pastetheplan/?id=rypV84y7V
クエリプラン( "最適化"付き):https://www.brentozar.com/pastetheplan/?id = r1aC2my7E
これは、テーブル内のデータ、インデックスなどに依存します。実行プランとio +時間の統計を比較できないとは言いがたいです。
私が期待する違いは、2つのテーブル間のJOINの前に行われる追加のフィルタリングです。私の例では、更新をselectに変更して、テーブルを再利用します。
フィルター操作が発生していることがはっきりとわかります。私のテストデータでは、フィルターで除外されたレコードがないため、結果として改善はありません。
フィルターがなくなりました。つまり、不要なレコードをフィルターで除外するには、結合に依存する必要があります。
その他の理由クエリを変更したことによる別の理由/結果として、クエリの変更時に新しい実行プランが作成されたことが考えられます。もっと早く。これの例は、エンジンが別の結合演算子を選択することですが、これは現時点で推測しているだけです。
編集:
2つのクエリプランを取得した後の説明:
クエリは、大きなテーブルから5億5,000万行を読み取り、それらを除外しています。
つまり、述語は、シーク述語ではなく、フィルタリングのほとんどを実行するものです。その結果、データは読み取られますが、返されるデータははるかに少なくなります。
SQLサーバーに別のインデックス(クエリプラン)を使用させる/インデックスを追加すると、これを解決できます。
では、最適化クエリに同じ問題がないのはなぜですか?
シークではなくスキャンを使用して、別のクエリプランが使用されているため。
シークを行わずに、4M行のみを返します。
次の違い
更新の違いを無視して(最適化されたクエリでは何も更新されません)、ハッシュの一致が最適化されたクエリで使用されます。
非最適化でのネストされたループ結合の代わりに:
ネストされたループは、1つのテーブルが小さく、もう1つのテーブルが大きい場合に最適です。どちらも同じサイズに近いので、この場合はハッシュ一致の方が適していると主張します。
概要
最適化されたクエリのプランには並列処理があり、ハッシュ一致結合を使用し、残差を減らす必要がありますIOフィルタリング。また、ビットマップを使用して、結合行を生成できないキー値を排除します。(また、何もない更新中です)
非最適化クエリ 非最適化クエリのプランには並列処理がなく、ネストされたループ結合を使用し、残差を行う必要がありますIO 550Mレコードでフィルタリングします(更新も行われています)
最適化されていないクエリを改善するために何ができますか?
キー列リストにfirst_name&last_nameを含むようにインデックスを変更します。
CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name on dbo.largeTableOfPeople(birth_date、first_name、last_name)include(id)
しかし、関数の使用とこのテーブルが大きいため、これは最適なソリューションではない可能性があります。
(HASH JOIN, MERGE JOIN)
をクエリに追加するテストデータ+使用したクエリ
CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));
set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');
set @i += 1;
END
set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');
set @j += 1;
END
SET STATISTICS IO, TIME ON;
SELECT smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');
SELECT smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
ON largeTbl.birth_date = smallTbl.birthDate
AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')
drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
2番目のクエリが実際に改善されていることは明らかではありません。
実行プランにはQueryTimeStatsが含まれており、質問で述べられているほど劇的な違いはありません。
スロープランの経過時間は_257,556 ms
_(4分17秒)でした。高速計画では、並列度3で実行しているにもかかわらず、経過時間は_190,992 ms
_(3分11秒)でした。
さらに、2番目の計画は、結合後に行う必要のないデータベースで実行されていました。
余分な時間は、350万行を更新するために必要な作業で十分に説明できるように(これらの行を特定し、ページをラッチし、ページに更新を書き込み、トランザクションログを書き込むために更新演算子で必要な作業は無視できません)
Ifこれは、likeとlikeを比較したときに実際に再現可能です。説明は、この場合はラッキーになったということです。
37のIN
条件を含むフィルターは、テーブルの4,008,334のうち51行しか削除しませんでしたが、オプティマイザはさらに多くを削除すると考えました
_ LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
'c', 'd', 'e', 'è',
'é', 'f', 'g', 'h',
'i', 'j', 'k', 'l',
'm', 'n', 'o', 'ô',
'ö', 'p', 'q', 'r',
's', 't', 'u', 'ü',
'v', 'w', 'x', 'y',
'z', 'æ', 'ä', 'ø', 'å' )
_
このような誤ったカーディナリティーの推定は、通常は悪いことです。この場合、大規模な過小評価によって引き起こされたハッシュの流出にもかかわらず、明らかに(?)うまく機能した異なる形の(および並列の)計画が作成されました。
TRIM
がないと、SQL Serverはこれを基本列ヒストグラムの範囲間隔に変換し、より正確な推定を行うことができますが、TRIM
を使用すると、推測に頼るだけです。
推測の性質は変わる可能性がありますが、LEFT(TRIM(largeTbl.last_name), 1)
の単一の述部の見積もりは、状況によって異なります * ちょうど_table_cardinality/estimated_number_of_distinct_column_values
_と推定されます。
正確にはどのような状況かわかりません。データのサイズが関係しているようです。私はこれを広い固定長データ型 ここにあるように で再現できましたが、varchar
を使用して異なる、より高い推測を取得しました(これは、フラットな10%推測を使用し、推定100,000行です)。 @ Solomon Rutzky は、char
のようにvarchar(100)
に後続スペースが埋め込まれている場合、より低い推定値が使用されることを指摘します
IN
リストはOR
に展開され、SQL Serverは exponential backoff を使用し、最大4つの述語が考慮されます。したがって、_219.707
_の見積もりは次のようになります。
_DECLARE @TableCardinality FLOAT = 4008334,
@DistinctColumnValueEstimate FLOAT = 34207
DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)
SELECT @TableCardinality * ( 1 - (
@NotSelectivity *
SQRT(@NotSelectivity) *
SQRT(SQRT(@NotSelectivity)) *
SQRT(SQRT(SQRT(@NotSelectivity)))
))
_