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


10

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


4
テクニカルノートへの小さな返答:AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIすべての文字をリストする必要がなく、コードを読みにくくすることなく、必要なことを実行できます
Erik A

の最終条件WHEREが偽である行がありますか?特に、比較では大文字と小文字が区別される場合があることに注意してください。
jpmc26

@ErikvonAsmuthは素晴らしいポイントです。ただし、ちょっとした技術的な注記:SQL Server 2008および2008 R2の場合、バージョン "100"照合を使用するのが最適です(使用されているカルチャ/ロケールで利用可能な場合)。ですからLatin1_General_100_CI_AI。また、SQL Server 2012以降(少なくともSQL Server 2019を介して)の場合は、使用されているロケールの最高バージョンで補助文字対応の照合を使用するのが最善です。したがってLatin1_General_100_CI_AI_SC、この場合はそうなります。バージョン> 100(今のところ日本語のみ)にはありません(または必要ありません)_SC(例:)Japanese_XJIS_140_CI_AI
ソロモンルツキー

回答:


9

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

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

「最適化」による実行計画 ここに画像の説明を入力してください

実行計画

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

「最適化」なしの実行計画 ここに画像の説明を入力してください

実行計画

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

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

編集:

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

クエリは、大きなテーブルから5億5,000万行を読み取り、それらを除外しています。 ここに画像の説明を入力してください

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

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

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

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

ここに画像の説明を入力してください ここに画像の説明を入力してください

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

次の違い

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

概観

最適化されたクエリ ここに画像の説明を入力してください

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

非最適化クエリ ここに画像の説明を入力してください 非最適化クエリのプランには並列処理がなく、ネストされたループ結合を使用し、550Mレコードで残余IOフィルタリングを実行する必要があります。(また、更新が行われています)

最適化されていないクエリを改善するために何ができますか?

  • キー列リストに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)クエリにOPTION を追加する
  • ...

テストデータ+使用したクエリ

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;

8

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

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

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

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

最初の計画

ここに画像の説明を入力してください

セカンドプラン

ここに画像の説明を入力してください

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

場合は、その後の説明は、あなたがちょうどこの場合には幸運ということであるようにと同じように比較するとき、これは実際には再現性があります。

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', 'æ', 'ä', 'ø', 'å' ) 

このような誤ったカーディナリティーの推定は、通常、悪いことです。この場合、大規模な過小評価によって引き起こされたハッシュの流出にもかかわらず、明らかに(?)うまく機能した異なる形の(および並列の)計画が作成されました。

TRIMSQL Serverがなければ、これをベースカラムヒストグラムの範囲間隔に変換し、はるかに正確な推定値を得ることができますがTRIM、推測に頼るだけです。

推測の性質はさまざまですが、単一の述語の推定LEFT(TRIM(largeTbl.last_name), 1)は、状況によっては*推定されるだけtable_cardinality/estimated_number_of_distinct_column_valuesです。

正確にはどのような状況かわかりません-データのサイズが役割を果たすようです。ここのように広い固定長のデータ型でこれを再現できましたが、異なる、より高いvarchar推定値を得ました(これは、フラットな10%推定値と推定100,000行を使用しただけです)。@Solomon Rutzkyが指摘するように、varchar(100)末尾のスペースで埋め込みが行われるとchar、低い方の推定値が使用される

INリストはアウトに展開されているORとSQL Serverが使用する指数バックオフを考慮さ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)))
))
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.