ゲスト作成者:Andy Mallon(@AMtwo)
Microsoft Dynamics CRMの背後にあるデータベースのサポートに精通している場合は、それが最もパフォーマンスの高いデータベースではないことをおそらくご存知でしょう。正直なところ、これは驚くべきことではありません。高速データベースとして設計されているわけではありません。 柔軟になるように設計されています データベース。ほとんどの顧客関係管理(CRM)システムは、ビジネス要件が大きく異なる多くの業界の多くのビジネスのニーズを満たすことができるように、柔軟に設計されています。彼らはそれらの要件をデータベースのパフォーマンスよりも優先しています。それはおそらく賢いビジネスですが、私はビジネスパーソンではなく、データベースパーソンです。 Dynamics CRMでの私の経験は、人々が私のところに来て言うときです
アンディ、データベースは遅い
最近発生したのは、5分間のクエリタイムアウトが原因でレポートが失敗したことです。適切なインデックスを使用すると、数百行を非常に高速に取得できるはずです。 。クエリといくつかのサンプルパラメータを入手し、それをPlan Explorerにドロップして、テスト環境で数回実行しました(これはすべてテストで実行します。これは、後で重要になります)。ベンチマークに「最悪のベスト」を使用できるように、ウォームキャッシュを使用して実行していることを確認したかったのです。クエリは非常に厄介なSELECTでした CTE、および多数の結合を使用します。残念ながら、顧客固有のビジネスロジックが含まれていたため、正確なクエリを提供することはできません(申し訳ありません)。
7分、37秒で十分です。
すぐに、ここでは多くの悪いことが起こっています。 150万回の読み取りは、大量のI/Oです。 200行を返すのに457秒かかるのは遅いです。 Cardinality Estimatorは、200行ではなく2行を期待していました。また、このクエリはSELECTにすぎないため、多くの書き込みがありました。 ステートメント、これは私たちがTempDbにこぼれているに違いないことを意味します。運が良ければ、インデックスを作成してテーブルスキャンを排除し、この処理を高速化できるかもしれません。計画はどのようになっていますか?
アパトサウルス、またはキリンのように見えます。
クイックヒットはありません
Dynamics CRMについて説明するために、少し一時停止します。ビューを使用します。ネストされたビューを使用します。ネストされたビューを使用して、行レベルのセキュリティを適用します。ダイナミクスの用語では、これらの行レベルのセキュリティを適用するネストされたビューは、「フィルターされたビュー」と呼ばれます。アプリケーションからのすべてのクエリは、これらのフィルタリングされたビューを通過します。データアクセスを実行するための唯一の「サポートされている」方法は、これらのフィルタリングされたビューを使用することです。
このクエリは多数のテーブルを参照していると言ったことを思い出してください。まあ、それはフィルタリングされたビューの束を参照しています。したがって、私が渡された複雑なクエリは、実際にはいくつかのレイヤーがより複雑です。この時点で、私は淹れたてのコーヒーを手に入れ、より大きなモニターに切り替えました。
問題を解決するための優れた方法は、最初から始めることです。 SELECT演算子を拡大し、矢印に従って何が起こっているかを確認しました。
34インチの超ワイドモニターでも、ディスプレイをいじる必要がありましたプランの設定でこれだけ表示できます。プランエクスプローラーでは、プランを90度回転させて、「背の高い」プランを幅の広いモニターに収めることができます。
それらすべてのテーブル値関数呼び出しを見てください!すぐに非常に高価なハッシュマッチが続きます。私のスパイダーマンはうずき始めました。 fn_GetMaxPrivilegeDepthMaskとは 、なぜ30回呼び出されるのですか?これは問題だと思います。プランの演算子として「テーブル値関数」が表示されている場合、それは実際にはマルチステートメントテーブル値関数であることを意味します。 。それがインラインのテーブル値関数である場合、それはより大きな計画に組み込まれ、ブラックボックスではありません。マルチステートメントのテーブル値関数は悪です。それらを使用しないでください。 Cardinality Estimatorは、正確な推定を行うことができません。クエリオプティマイザは、より大きなクエリのコンテキストでそれらを最適化することはできません。パフォーマンスの観点からは、拡張性はありません。
このTVFはDynamicsCRMのすぐに使用できるコードですが、私のSpideySenseはそれが問題であると教えてくれます。大きな恐ろしい計画でこの大きな厄介なクエリを忘れてください。その機能に足を踏み入れて、何が起こっているのかを見てみましょう:
create function [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int)
returns @d table(PrivilegeDepthMask int)
-- It is by design that we return a table with only one row and column
as
begin
declare @UserId uniqueidentifier
select @UserId = dbo.fn_FindUserGuid()
declare @t table(depth int)
-- from user roles
insert into @t(depth)
select
--privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global)
-- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global)
-- do an AND with 0x0F ( =15) to get basic/local/deep/global
max(rp.PrivilegeDepthMask % 0x0F)
as PrivilegeDepthMask
from
PrivilegeBase priv
join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId)
join Role r on (rp.RoleId = r.ParentRootRoleId)
join SystemUserRoles ur on (r.RoleId = ur.RoleId and ur.SystemUserId = @UserId)
join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId)
where
potc.ObjectTypeCode = @ObjectTypeCode and
priv.AccessRight & 0x01 = 1
-- from user's teams roles
insert into @t(depth)
select
--privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global)
-- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global)
-- do an AND with 0x0F ( =15) to get basic/local/deep/global
max(rp.PrivilegeDepthMask % 0x0F)
as PrivilegeDepthMask
from
PrivilegeBase priv
join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId)
join Role r on (rp.RoleId = r.ParentRootRoleId)
join TeamRoles tr on (r.RoleId = tr.RoleId)
join SystemUserPrincipals sup on (sup.PrincipalId = tr.TeamId and sup.SystemUserId = @UserId)
join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId)
where
potc.ObjectTypeCode = @ObjectTypeCode and
priv.AccessRight & 0x01 = 1
insert into @d select max(depth) from @t
return
end
GO この関数は、マルチステートメントTVFの従来のパターンに従います。
- 定数として使用される変数を宣言します
- テーブル変数に挿入
- そのテーブル変数を返す
ここでは特別なことは何も起こっていません。これらの複数のステートメントを単一のSELECTとして書き直すことができます。 声明。単一のSELECTとして記述できる場合 ステートメント、これをインラインTVFとして書き直すことができます。
やってみましょう
はっきりしない場合は、ソフトウェアベンダーから提供されたコードを書き直そうとしています。これを「サポートされている」動作と見なすソフトウェアベンダーに会ったことはありません。すぐに使用できるアプリケーションコードを変更する場合は、ご自身で行ってください。 Microsoftは確かに、Dynamicsのこの「サポートされていない」動作を考慮しています。私はテスト環境を使用していて、本番環境で遊んでいないので、とにかくそれを行うつもりです。この関数の書き直しには数分しかかかりませんでした。それでは、試してみて、何が起こるか見てみませんか?私のバージョンの関数は次のようになります:
create function [dbo].[fn_GetMaxPrivilegeDepthMask](@ObjectTypeCode int)
returns table
-- It is by design that we return a table with only one row and column
as
RETURN
-- from user roles
select PrivilegeDepthMask = max(PrivilegeDepthMask)
from (
select
--privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global)
-- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global)
-- do an AND with 0x0F ( =15) to get basic/local/deep/global
max(rp.PrivilegeDepthMask % 0x0F)
as PrivilegeDepthMask
from
PrivilegeBase priv
join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId)
join Role r on (rp.RoleId = r.ParentRootRoleId)
join SystemUserRoles ur on (r.RoleId = ur.RoleId and ur.SystemUserId = dbo.fn_FindUserGuid())
join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId)
where
potc.ObjectTypeCode = @ObjectTypeCode and
priv.AccessRight & 0x01 = 1
UNION ALL
-- from user's teams roles
select
--privilege depth mask = 1(basic) 2(local) 4(deep) and 8(global)
-- 16(inherited read) 32(inherited local) 64(inherited deep) and 128(inherited global)
-- do an AND with 0x0F ( =15) to get basic/local/deep/global
max(rp.PrivilegeDepthMask % 0x0F)
as PrivilegeDepthMask
from
PrivilegeBase priv
join RolePrivileges rp on (rp.PrivilegeId = priv.PrivilegeId)
join Role r on (rp.RoleId = r.ParentRootRoleId)
join TeamRoles tr on (r.RoleId = tr.RoleId)
join SystemUserPrincipals sup on (sup.PrincipalId = tr.TeamId and sup.SystemUserId = dbo.fn_FindUserGuid())
join PrivilegeObjectTypeCodes potc on (potc.PrivilegeId = priv.PrivilegeId)
where
potc.ObjectTypeCode = @ObjectTypeCode and
priv.AccessRight & 0x01 = 1
)x
GO 元のテストクエリに戻り、キャッシュをダンプして、数回再実行しました。こちらが最も遅い 私のバージョンのTVFを使用する場合の実行時間:
それははるかに良く見えます!
それでも世界で最も効率的なクエリではありませんが、十分に高速です。これ以上高速にする必要はありません。ただし…それを実現するには、Microsoftのコードを変更する必要がありました。それは理想的ではありません。新しいTVFの完全な計画を見てみましょう:
さようならアパトサウルス、こんにちはPEZディスペンサー!
それはまだ本当に厄介な計画ですが、最初を見ると、これらのブラックボックスTVF呼び出しはすべてなくなっています。超高価なハッシュマッチはなくなりました。 SQL Serverは、TVF呼び出しの大きなボトルネックなしですぐに機能します(TVFの背後にある作業は、残りのSELECTとインラインになります。 ):
全体像への影響
このTVFは実際にどこで使用されていますか? Dynamics CRMのほぼすべてのフィルター処理されたビューは、この関数呼び出しを使用します。 246個のフィルター処理されたビューがあり、そのうち206個がこの関数を参照しています。これは、Dynamicsの行レベルのセキュリティ実装の一部として重要な機能です。 アプリケーションからデータベースへの実質的にすべてのクエリは、この関数を少なくとも1回、通常は数回呼び出します。 これは両面コインです。一方で、この機能を修正すると、アプリケーション全体のターボブーストとして機能する可能性があります。一方、この関数に関係するすべてのものに対して回帰テストを実行する方法はありません。
ちょっと待ってください。この関数呼び出しがパフォーマンスの核心であり、Dynamics CRMの核心である場合、Dynamicsを使用するすべての人がこのパフォーマンスのボトルネックに直面していることになります。私たちはマイクロソフトとの訴訟を起こし、私は何人かの人々に電話をかけて、このコードを担当するエンジニアリングチームにチケットをぶつけてもらいました。運が良ければ、この更新されたバージョンの関数は、Dynamics CRMの将来のリリースでボックス(およびクラウド)に組み込まれる予定です。
DynamicsCRMのマルチステートメントTVFはこれだけではありません。同じタイプの変更をfn_UserSharedAttributesAccessに加えました。 別のパフォーマンスの問題。また、問題が発生していないために触れていないTVFが他にもあります。
ダイナミクスを使用していない場合でも、すべての人にレッスン
私の後に繰り返してください:マルチステートメントテーブルの値の関数は悪です!
マルチステートメントTVFを使用しないように、コードをリファクタリングします。コードを調整しようとしていて、マルチステートメントTVFが表示されている場合は、それを批判的に見てください。コードを常に変更できるとは限りません(または、変更した場合はサポート契約に違反する可能性があります)が、コードを変更できる場合は変更してください。マルチステートメントTVFの使用を停止するようにソフトウェアベンダーに指示してください。データベースからこれらの厄介な機能のいくつかを排除することにより、世界をより良い場所にします。
作者について
Andy Mallonは、SQLServerDBAおよびMicrosoftDataPlatform MVPであり、ヘルスケア、金融、eのデータベースを管理しています。 -コマース、および非営利セクター。 2003年以来、Andyは、要求の厳しいパフォーマンスニーズを持つ大量の高可用性OLTP環境をサポートしてきました。 Andyは、BostonSQLの創設者であり、SQLSaturday Bostonの共同主催者であり、am2.coのブログです。