Where句にこのようないくつかの本当に醜い条件が含まれる、非常に重要で非常に遅いビューがあります。また、結合が整数IDフィールドではなくvarchar(13)
での大まかな低速結合であることも認識していますが、このビューを使用する以下の簡単なクエリを改善したいと思います。
_CREATE VIEW [dbo].[vwReallySlowView] AS
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo, B.HourBooked AS HBooked,
B.MinBooked AS MBooked, B.SecBooked AS SBooked,
I.prep_on AS Pon, I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
(CASE I.prep_on WHEN 'Y' THEN I.PDate ELSE I.FirstDate END) AS PrDate, I.PTimeH AS PrTimeH, I.PTimeM AS PrTimeM,
(CASE WHEN I.RetnDate < I.FirstDate THEN I.FirstDate ELSE I.RetnDate END) AS RDatev, I.bit_field_v41 AS bitField, I.FirstDate AS FDatev, I.BookDate AS DBooked,
I.TimeBookedH AS TBookH, I.TimeBookedM AS TBookM, I.TimeBookedS AS TBookS, I.del_time_hour AS dth, I.del_time_min AS dtm, I.return_to_locn AS rtlocn,
I.return_time_hour AS rth, I.return_time_min AS rtm, (CASE WHEN I.Trans_type_v41 IN (6, 7) AND (I.Trans_qty < I.QtyCheckedOut)
THEN 0 WHEN I.Trans_type_v41 IN (6, 7) AND (I.Trans_qty >= I.QtyCheckedOut) THEN I.Trans_Qty - I.QtyCheckedOut ELSE I.trans_qty END) AS trqty,
(CASE WHEN I.Trans_type_v41 IN (6, 7) THEN 0 ELSE I.QtyCheckedOut END) AS MyQtycheckedout, (CASE WHEN I.Trans_type_v41 IN (6, 7)
THEN 0 ELSE I.QtyReturned END) AS retqty, I.ID, B.BookingProgressStatus AS bkProg, I.product_code_v42, I.return_to_locn, I.AssignTo, I.AssignType,
I.QtyReserved, B.DeprepOn,
(CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END) AS DeprepDateTime, I.InRack
FROM dbo.tblItemtran AS I
INNER JOIN -- booking_no = varchar(13)
dbo.tblbookings AS B ON B.booking_no = I.booking_no_v32 -- string inner-join
INNER JOIN -- product_code = varchar(13)
dbo.tblInvmas AS M ON I.product_code_v42 = M.product_code -- string inner-join
WHERE (I.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)) AND (I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) OR
(I.trans_type_v41 NOT IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (B.BookingProgressStatus = 1) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut = 0) OR
(I.trans_type_v41 IN (6, 7)) AND (I.bit_field_v41 & 4 = 0) AND (I.QtyCheckedOut > 0) AND (I.trans_qty - (I.QtyCheckedOut - I.QtyReturned) > 0)
_
このビューは通常、次のように使用されます。
_select * from vwReallySlowView
where product_code_v42 = 'LIGHTBULB100W' -- find "100 watt lightbulb" rows
_
実行すると、バッチの総コストの20〜80%を占めるこの実行プランアイテムが得られます。述語CONVERT_IMPLICIT( .... &(4))
は、これらの_bitwise boolean tests
_を実行するのが非常に遅いように見えます_(I.ibitfield & 4 = 0)
_。
私はほとんどの場合SQL以外のソフトウェア開発者なので、MS SQLやDBAタイプの作業全般の専門家ではありません。しかし、私はそのようなビットごとの組み合わせは悪い考えであり、離散ブールフィールドを持つほうがよかったのではないかと思います。
スキーマを変更せずにこのビューをより適切に処理するために、このインデックスを何らかの方法で改善できますか(すでに数千の場所で運用されています)、または整数にパックされたいくつかのブール値を持つ基になるテーブルを変更する必要があります_bit_field_v41
_ 、この問題を修正するには?
これは、この実行プランでスキャンされているtblItemtran
の私のクラスター化インデックスです。
_-- goal: speed up select * from vwReallySlowView where productcode = 'X'
CREATE CLUSTERED INDEX [idxtblItemTranProductCodeAndTransType] ON [dbo].[tblItemtran]
(
[product_code_v42] ASC, -- varchar(13)
[trans_type_v41] ASC -- int
)WITH ( PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF,
IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON)
ON [PRIMARY]
_
以下は、この_CONVERT_IMPLICIT
_述語のコストが27%になる他の製品の1つに対する実行計画です。 updateこの場合、最悪のノードは_inner join
_の「ハッシュ一致」になり、コストは34%になると思いますこれは、現在取り除くことができない文字列の結合を回避できない限り、回避できないコストです。上記のビューの_INNER JOIN
_操作は両方ともvarchar(13)
フィールドに対して行われます。
右下隅にズームイン:
。sqlplanとしての全体の実行計画はskydriveで利用可能です。 この画像は視覚的な概要です。 こちら をクリックして、画像を単独で表示します。
Update実行プラン全体を投稿しました。 _product_code
_の値が病理学的に悪かったのかわかりませんが、そのための1つの方法は、単一の製品を実行する代わりにselect count(*) from view
を使用することです。しかし、基礎となるテーブルのレコードの5%以下でのみ使用される製品は、_CONVERT_IMPLICIT
_オペレーションのコストが大幅に低くなるようです。ここでSQLを修正する場合は、ビューでWHERE
節をすべて使用して、巨大なwhere-clause-conditionの結果を計算し、「IncludeMeInTheView」ビットとして保存すると思います。基になるテーブルのフィールド。プレスト、問題は解決しましたよね?
実行計画では、コストの割合に過度に依存しないでください。これらは常に見積もられたコストであり、行数などの「実際の」数値を含む実行後の計画であってもです。推定コストは、オプティマイザが同じクエリの異なる候補実行プランから選択できるようにすることを目的として、非常にうまく機能するモデルに基づいています。コスト情報は興味深いものであり、考慮すべき要素ですが、クエリチューニングの主要な指標になることはめったにありません。実行計画情報を解釈するには、提示されたデータのより広い視野が必要です。
ItemTran Clustered Index Seek演算子
この演算子は、実際には1つの2つの操作です。最初にインデックスシーク操作は、述語_product_code_v42 = 'M10BOLT'
_に一致するすべての行を見つけ、次に各行に残余述語_bit_field_v41 & 4 = 0
_が適用されます。ベースタイプ(tinyint
またはsmallint
)からinteger
への_bit_field_v41
_の暗黙的な変換があります。
ビットごとのAND演算子 (&)は、両方のオペランドが同じ型である必要があるため、変換が行われます。定数値 '4'の暗黙のタイプは整数であり、 データタイプ優先規則 は優先順位の低い_bit_field_v41
_フィールド値が変換されることを意味します。
この問題は(そのような場合)、述語をbit_field_v41 & CONVERT(tinyint, 4) = 0
と書くことで簡単に修正できます。つまり、定数値は優先度が低く、列値ではなく(定数の折りたたみ時に)変換されます。 _bit_field_v41
_がtinyint
の場合、変換はまったく行われません。同様に、_bit_field_v41
_がsmallint
の場合、CONVERT(smallint, 4)
を使用できます。つまり、この場合、変換はパフォーマンスの問題ではありませんが、型を一致させ、可能な場合は暗黙的な変換を回避することをお勧めします。
このシークの推定コストの大部分は、ベーステーブルのサイズまでです。クラスタ化インデックスキー自体はかなり狭いですが、各行のサイズは大きくなります。テーブルの定義は与えられていませんが、ビューで使用される列だけが合計してかなりの行幅になります。クラスタ化インデックスにはすべての列が含まれるため、クラスタ化インデックスキー間の距離は行の幅であり、インデックスキーの幅ではありません。一部の列でバージョン接尾辞を使用することは、実際のテーブルに以前のバージョンの列がさらにあることを示唆しています。
シーク、残りの述語、出力列を見ると、同等のクエリを構築することで、この演算子のパフォーマンスを個別に確認できます(_1 <> 2
_は、自動パラメーター化を防ぐためのトリックであり、矛盾はオプティマイザーによって削除され、クエリプランには表示されません):
_SELECT
it.booking_no_v32,
it.QtyCheckedOut,
it.QtyReturned,
it.Trans_qty,
it.trans_type_v41
FROM dbo.tblItemTran AS it
WHERE
1 <> 2
AND it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0;
_
先読みはテーブル(クラスター化インデックス)の断片化の影響を受けるため、コールドデータキャッシュを使用したこのクエリのパフォーマンスは重要です。このテーブルのクラスタリングキーは断片化を招くため、このインデックスを定期的に維持(再編成または再構築)し、適切なFILLFACTOR
を使用して、インデックスメンテナンスウィンドウ間に新しい行のためのスペースを確保することが重要です。
SQL Data Generator を使用して生成されたサンプルデータを使用して、先読みに対する断片化の影響のテストを実行しました。質問のクエリプランに示されているのと同じテーブル行カウントを使用すると、高度に断片化されたクラスター化インデックスにより、_SELECT * FROM view
_が_DBCC DROPCLEANBUFFERS
_の後に15秒かかります。同じ条件で同じテストを行い、ItemTransテーブルで新しく再構築されたクラスター化インデックスを3秒で完了しました。
通常、テーブルデータが完全にキャッシュにある場合、断片化の問題はそれほど重要ではありません。ただし、断片化が少ない場合でも、テーブルの行が広いと、論理的および物理的読み取りの数が予想よりもはるかに多くなる可能性があります。また、明示的なCONVERT
を追加および削除して、暗黙的な変換の問題がここでは重要ではないという私のベストプラクティス違反を除いて、私の期待を検証することもできます。
さらに重要なのは、シーク演算子を離れる行の推定数です。最適化時間の見積もりは165行ですが、実行時に4,226行が生成されました。後でこの点に戻りますが、不一致の主な理由は、(ビットごとのANDを含む)残余述語の選択性がオプティマイザが予測するのが非常に難しいためです-実際には推測に頼っています。
フィルター演算子
ここでは、2つの_NOT IN
_リストがどのように組み合わされ、簡略化され、拡張されるかを示すため、および次のハッシュ一致の説明の参照を提供するために、ここでフィルター述語を示しています。 seekからのテストクエリを拡張して、その効果を組み込み、Filter演算子のパフォーマンスへの影響を判断できます。
_SELECT
it.booking_no_v32,
it.trans_type_v41,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut
FROM dbo.tblItemTran AS it
WHERE
it.product_code_v42 = 'M10BOLT'
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND
(
(
it.trans_type_v41 NOT IN (2, 3, 6, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
)
);
_
プランのCompute Scalar演算子は次の式を定義します(計算自体は、後の演算子で結果が必要になるまで延期されます)。
_[Expr1016] = (trans_qty - (QtyCheckedOut - QtyReturned))
_
ハッシュ一致演算子
文字データ型で結合を実行することは、この演算子の推定コストが高くなる理由ではありません。 SSMSツールチップにはハッシュキープローブエントリのみが表示されますが、重要な詳細は[SSMSプロパティ]ウィンドウにあります。
Hash Match演算子は、ItemTranテーブルの_booking_no_v32
_列(ハッシュキーのビルド)の値を使用してハッシュテーブルを作成し、Bookingsの_booking_no
_列(ハッシュキーのプローブ)を使用して一致をプローブします。テーブル。 SSMSツールチップも通常はプローブ残差を表示しますが、テキストはツールチップとしては長すぎて省略されます。
プローブ残差は、インデックスが以前にシークした後に表示される残差に似ています。ハッシュが一致するすべての行で残余述語が評価され、行を親演算子に渡すかどうかが決定されます。バランスのとれたハッシュテーブルでハッシュの一致を見つけるのは非常に高速ですが、比較する各行に複雑な残差述語を適用すると、比較するとかなり遅くなります。プランエクスプローラーのハッシュマッチツールチップに、プローブ残差式を含む詳細が表示されます。
残余述語は複雑で、予約表から列を利用できるようになったため、予約進行状況チェックが含まれています。ツールチップには、インデックスシークの前半で見られた推定行数と実際の行数の不一致も表示されます。フィルタリングの多くが2回実行されるのは奇妙に思えるかもしれませんが、これはオプティマイザが楽観的であるだけです。プローブの残差からプランを押し下げて行を削除できるフィルターの部分は想定されていません(行カウントの推定値はフィルターの前後で同じです)が、オプティマイザーはそれが間違っている可能性があることを認識しています。行を早期にフィルタリングする可能性(ハッシュ結合のコストを削減)は、追加のフィルターのわずかなコストに見合う価値があります。予約テーブルの列に対するテストが含まれているため、フィルター全体をプッシュダウンすることはできませんが、そのほとんどは可能です。完全性テストは、正確性のためにハッシュ残差テストでも必要です。
ハッシュテーブル用に予約されているメモリの量は推定される行数に基づいているため、行数の過小評価はハッシュ一致演算子の問題です。実行時に必要なハッシュテーブルのサイズに対してメモリが小さすぎる場合(行数が多いため)、ハッシュテーブルが再帰的に流出してtempdbストレージ。多くの場合、パフォーマンスが非常に低下します。最悪の場合、実行エンジンはハッシュバケットの再帰的な流出を停止し、非常に遅い救済アルゴリズムを使用します。ハッシュ流出(再帰的または救済)は、質問で概説されているパフォーマンスの問題の最も可能性の高い原因です(文字タイプの結合列や暗黙的な変換ではありません)。根本的な原因は、不正な行数(カーディナリティ)の見積もりに基づいて、サーバーがクエリ用に予約するメモリが少なすぎることです。
悲しいことに、SQL Server 2012より前のバージョンでは、ハッシュオペレーションがメモリ割り当てを超え(サーバーに大量の空きメモリがある場合でも、実行が開始される前に予約された後は動的に拡大できない)、実行計画に表示されません。 tempdb。プロファイラを使用して ハッシュ警告イベントクラス を監視することは可能ですが、警告を特定のクエリに関連付けるのは難しい場合があります。
問題の修正
3つの問題は、断片化、ハッシュ一致演算子に残された複雑なプローブ、およびインデックスシークでの推測から生じる誤ったカーディナリティの推定です。
推奨されるソリューション
断片化を確認し、必要に応じて修正します。メンテナンスをスケジュールして、インデックスが許容範囲内に収まるようにします。カーディナリティの見積もりを修正する通常の方法は、統計を提供することです。この場合、オプティマイザは、組み合わせ(_product_code_v42
_、_bitfield_v41 & 4 = 0
_)の統計を必要とします。式の統計を直接作成することはできないため、最初にビットフィールド式の計算列を作成してから、手動で複数列の統計を作成する必要があります。
_ALTER TABLE dbo.tblItemTran
ADD Bit3 AS bit_field_v41 & CONVERT(tinyint, 4);
CREATE STATISTICS [stats dbo.ItemTran (product_code_v42, Bit3)]
ON dbo.tblItemTran (product_code_v42, Bit3);
_
統計を使用するには、計算された列のテキスト定義がビュー定義のテキストとほぼ正確に一致する必要があるため、暗黙的な変換を排除するようにビューを修正すると同時に、テキストが一致するように注意する必要があります。
複数列の統計は、はるかに優れた推定をもたらすはずであり、ハッシュ一致演算子が再帰的スピルまたは救済アルゴリズムを使用する可能性を大幅に低減します。計算列を追加する(これはメタデータのみの操作であり、PERSISTED
とマークされていないため、テーブル内でスペースを取りません)。最初の解決策では、複数列の統計が最もよく推測されます。
クエリパフォーマンスの問題を解決するときは、経過時間、CPU使用率、論理読み取り、物理読み取り、待機タイプと期間などを測定することが重要です。上記のように、クエリの一部を個別に実行して、疑わしい原因を検証することも役立ちます。
データの最新のビューが重要でない一部の環境では、ビュー全体を頻繁にスナップショットテーブルに具体化するバックグラウンドプロセスを実行すると便利な場合があります。このテーブルは通常のベーステーブルにすぎず、更新パフォーマンスへの影響を心配することなく、読み取りクエリ用にインデックスを付けることができます。
View indexing
元のビューに直接インデックスを付けるように誘惑されないでください。読み取りパフォーマンスは驚くほど高速になりますが(ビューインデックスでの1回のシーク)、(この場合)既存のクエリプランのパフォーマンスの問題はすべて、ビューで参照されているテーブル列を変更するクエリに転送されます。ベーステーブルの行を変更するクエリは、実際に非常に深刻な影響を受けます。
部分的なインデックス付きビューを使用した高度なソリューション
カーディナリティの推定値を修正し、フィルターとプローブの残差を削除する、この特定のクエリには部分的なインデックス付きビューソリューションがありますが、これはデータに関するいくつかの仮定(主にスキーマでの推測)に基づいており、特に適切な実装に関してエキスパートの実装が必要ですインデックス付きビューのメンテナンスプランをサポートするためのインデックス。以下のコードを興味を持って共有します。非常に注意深い分析とテストなしに実装することは提案しません。
_-- Indexed view to optimize the main view
CREATE VIEW dbo.V1
WITH SCHEMABINDING
AS
SELECT
it.ID,
it.product_code_v42,
it.trans_type_v41,
it.booking_no_v32,
it.Trans_qty,
it.QtyReturned,
it.QtyCheckedOut,
it.QtyReserved,
it.bit_field_v41,
it.prep_on,
it.From_locn,
it.Trans_to_locn,
it.PDate,
it.FirstDate,
it.PTimeH,
it.PTimeM,
it.RetnDate,
it.BookDate,
it.TimeBookedH,
it.TimeBookedM,
it.TimeBookedS,
it.del_time_hour,
it.del_time_min,
it.return_to_locn,
it.return_time_hour,
it.return_time_min,
it.AssignTo,
it.AssignType,
it.InRack
FROM dbo.tblItemTran AS it
JOIN dbo.tblBookings AS tb ON
tb.booking_no = it.booking_no_v32
WHERE
(
it.trans_type_v41 NOT IN (2, 3, 7, 18, 19, 20, 21, 12, 13, 22)
AND it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
)
OR
(
it.trans_type_v41 NOT IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND tb.BookingProgressStatus = 1
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut = 0
)
OR
(
it.trans_type_v41 IN (6, 7)
AND it.bit_field_v41 & CONVERT(tinyint, 4) = 0
AND it.QtyCheckedOut > 0
AND it.trans_qty - (it.QtyCheckedOut - it.QtyReturned) > 0
);
GO
CREATE UNIQUE CLUSTERED INDEX cuq ON dbo.V1 (product_code_v42, ID);
GO
_
既存のビューは、上記のインデックス付きビューを使用するように調整されました。
_CREATE VIEW [dbo].[vwReallySlowView2]
AS
SELECT
I.booking_no_v32 AS bkno,
I.trans_type_v41 AS trantype,
B.Assigned_to_v61 AS Assignbk,
B.order_date AS dateo,
B.HourBooked AS HBooked,
B.MinBooked AS MBooked,
B.SecBooked AS SBooked,
I.prep_on AS Pon,
I.From_locn AS Flocn,
I.Trans_to_locn AS TTlocn,
CASE I.prep_on
WHEN 'Y' THEN I.PDate
ELSE I.FirstDate
END AS PrDate,
I.PTimeH AS PrTimeH,
I.PTimeM AS PrTimeM,
CASE
WHEN I.RetnDate < I.FirstDate
THEN I.FirstDate
ELSE I.RetnDate
END AS RDatev,
I.bit_field_v41 AS bitField,
I.FirstDate AS FDatev,
I.BookDate AS DBooked,
I.TimeBookedH AS TBookH,
I.TimeBookedM AS TBookM,
I.TimeBookedS AS TBookS,
I.del_time_hour AS dth,
I.del_time_min AS dtm,
I.return_to_locn AS rtlocn,
I.return_time_hour AS rth,
I.return_time_min AS rtm,
CASE
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty < I.QtyCheckedOut
THEN 0
WHEN
I.Trans_type_v41 IN (6, 7)
AND I.Trans_qty >= I.QtyCheckedOut
THEN I.Trans_Qty - I.QtyCheckedOut
ELSE
I.trans_qty
END AS trqty,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyCheckedOut
END AS MyQtycheckedout,
CASE
WHEN I.Trans_type_v41 IN (6, 7)
THEN 0
ELSE I.QtyReturned
END AS retqty,
I.ID,
B.BookingProgressStatus AS bkProg,
I.product_code_v42,
I.return_to_locn,
I.AssignTo,
I.AssignType,
I.QtyReserved,
B.DeprepOn,
CASE B.DeprepOn
WHEN 1 THEN B.DeprepDateTime
ELSE I.RetnDate
END AS DeprepDateTime,
I.InRack
FROM dbo.V1 AS I WITH (NOEXPAND)
JOIN dbo.tblbookings AS B ON
B.booking_no = I.booking_no_v32
JOIN dbo.tblInvmas AS M ON
I.product_code_v42 = M.product_code;
_
クエリと実行プランの例:
_SELECT
vrsv.*
FROM dbo.vwReallySlowView2 AS vrsv
WHERE vrsv.product_code_v42 = 'M10BOLT';
_
新しい計画では、ハッシュ一致には残余述語がない、複雑なフィルターがない、インデックス付きビューシークに残余述語がなく、カーディナリティ推定が正確である。
挿入/更新/削除の計画がどのように影響を受けるかの例として、これはItemTransテーブルへの挿入の計画です。
強調表示されたセクションは新しく、インデックス付きビューのメンテナンスに必要です。テーブルスプールは、インデックス付きビューのメンテナンスのために挿入されたベーステーブル行を再生します。各行は、クラスター化インデックスシークを使用して予約テーブルに結合され、フィルターは複雑なWHERE
句の述語を適用して、行をビューに追加する必要があるかどうかを確認します。その場合、ビューのクラスター化インデックスへの挿入が実行されます。
先に実行した同じ_SELECT * FROM view
_テストは、インデックス付きビューを配置して150ミリ秒で完了しました。
最後に、2008 R2サーバーはまだRTMのままです。パフォーマンスの問題は修正されませんが、 2008 R2のService Pack 2 は2012年7月から利用可能であり、Service Packをできるだけ最新の状態に保つには多くの理由があります。