web-dev-qa-db-ja.com

なぜこれはより速く、安全に使用できますか? (最初の文字がアルファベットの場合)

要するに、非常に大きな人々のテーブルからの値で人々の小さなテーブルを更新しています。最近のテストでは、この更新の実行に約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

10
JohnF

これは、テーブル内のデータ、インデックスなどに依存します。実行プランとio +時間の統計を比較できないとは言いがたいです。

私が期待する違いは、2つのテーブル間のJOINの前に行われる追加のフィルタリングです。私の例では、更新をselectに変更して、テーブルを再利用します。

「最適化」を含む実行計画enter image description here

実行計画

フィルター操作が発生していることがはっきりとわかります。私のテストデータでは、フィルターで除外されたレコードがないため、結果として改善はありません。

「最適化」なしの実行プランenter image description here

実行計画

フィルターがなくなりました。つまり、不要なレコードをフィルターで除外するには、結合に依存する必要があります。

その他の理由クエリを変更したことによる別の理由/結果として、クエリの変更時に新しい実行プランが作成されたことが考えられます。もっと早く。これの例は、エンジンが別の結合演算子を選択することですが、これは現時点で推測しているだけです。

編集:

2つのクエリプランを取得した後の説明:

クエリは、大きなテーブルから5億5,000万行を読み取り、それらを除外しています。 enter image description here

つまり、述語は、シーク述語ではなく、フィルタリングのほとんどを実行するものです。その結果、データは読み取られますが、返されるデータははるかに少なくなります。

SQLサーバーに別のインデックス(クエリプラン)を使用させる/インデックスを追加すると、これを解決できます。

では、最適化クエリに同じ問題がないのはなぜですか?

シークではなくスキャンを使用して、別のクエリプランが使用されているため。

enter image description hereenter image description here

シークを行わずに、4M行のみを返します。

次の違い

更新の違いを無視して(最適化されたクエリでは何も更新されません)、ハッシュの一致が最適化されたクエリで使用されます。

enter image description here

非最適化でのネストされたループ結合の代わりに:

enter image description here

ネストされたループは、1つのテーブルが小さく、もう1つのテーブルが大きい場合に最適です。どちらも同じサイズに近いので、この場合はハッシュ一致の方が適していると主張します。

概要

最適化されたクエリenter image description here

最適化されたクエリのプランには並列処理があり、ハッシュ一致結合を使用し、残差を減らす必要がありますIOフィルタリング。また、ビットマップを使用して、結合行を生成できないキー値を排除します。(また、何もない更新中です)

非最適化クエリenter image description here 非最適化クエリのプランには並列処理がなく、ネストされたループ結合を使用し、残差を行う必要があります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)

しかし、関数の使用とこのテーブルが大きいため、これは最適なソリューションではない可能性があります。

  • 統計を更新し、再コンパイルを使用してより良い計画を試行して取得します。
  • OPTION(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;
9
Randi Vertongen

2番目のクエリが実際に改善されていることは明らかではありません。

実行プランにはQueryTimeStatsが含まれており、質問で述べられているほど劇的な違いはありません。

スロープランの経過時間は_257,556 ms_(4分17秒)でした。高速計画では、並列度3で実行しているにもかかわらず、経過時間は_190,992 ms_(3分11秒)でした。

さらに、2番目の計画は、結合後に行う必要のないデータベースで実行されていました。

最初の計画

enter image description here

セカンドプラン

enter image description here

余分な時間は、350万行を更新するために必要な作業で十分に説明できるように(これらの行を特定し、ページをラッチし、ページに更新を書き込み、トランザクションログを書き込むために更新演算子で必要な作業は無視できません)

Ifこれは、likeとlikeを比較したときに実際に再現可能です。説明は、この場合はラッキーになったということです。

37のIN条件を含むフィルターは、テーブルの4,008,334のうち51行しか削除しませんでしたが、オプティマイザはさらに多くを削除すると考えました

enter image description here

_   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)))
))
_
8
Martin Smith