ここで、SQL 担当者向けの頭の体操をします。これらの関数の最初の関数が正常に実行され、2 番目の関数の実行速度が非常に遅い理由を誰か思いつきますか?
機能 A - 通常は 5 ミリ秒以内に終了します
CREATE FUNCTION dbo.GoodFunction
(
@IDs UniqueIntTable READONLY
)
RETURNS TABLE
AS RETURN
SELECT p.ID, p.Node, p.Name, p.Level
FROM
(
SELECT DISTINCT a.Ancestor AS Node
FROM Hierarchy h
CROSS APPLY dbo.GetAncestors(h.Node.GetAncestor(1)) a
WHERE h.ID IN (SELECT Value FROM @IDs)
) np
INNER JOIN Hierarchy p
ON p.Node = np.Node
機能 B - 実行速度が非常に遅い - 5 分後にあきらめた
CREATE FUNCTION dbo.BadFunction
(
@IDs UniqueIntTable READONLY
)
RETURNS TABLE
AS RETURN
WITH Ancestors_CTE AS
(
SELECT DISTINCT a.Ancestor AS Node
FROM Hierarchy c
CROSS APPLY dbo.GetAncestors(c.Node.GetAncestor(1)) a
WHERE c.ID IN (SELECT Value FROM @IDs)
)
SELECT p.ID, p.Node, p.Name, p.Level
FROM Ancestors_CTE ac
INNER JOIN Hierarchy p
ON p.Node = ac.Node
この関数が何をするかを以下で説明しますが、その前に、私が知る限り、これら 2 つの関数はまったく同じであるため、重要ではないと考えていることを指摘したいと思います! 唯一の違いは、一方が CTE を使用し、他方がサブクエリを使用することです。A のサブクエリの内容と B の CTE は同一です。
誰かがこれを問題だと判断した場合: この関数の目的は、階層内の任意の数の場所のすべての可能な祖先 (親、祖父母など) を選択することです。Node
列は でありhierarchyid
、パスをたどるだけのdbo.GetAncestors
CLR 関数であり、データ アクセスは行いません。
UniqueIntTable
これは、1 つの列を持つユーザー定義のテーブル タイプValue int NOT NULL PRIMARY KEY
です。ここでインデックスを作成する必要があるものはすべてインデックスが作成されます。関数 A の実行計画は、関数 B の場合と同様に、基本的には 2 つのインデックス シークと 1 つのハッシュ マッチだけです。
この奇妙な問題にはさらに奇妙な側面があります。
関数 B を使用した単純なクエリの推定実行計画を取得することさえできません。パフォーマンスの問題は、この単純に見える関数のコンパイルに関係しているようです。
関数 B から「本体」を取り出してインライン クエリに貼り付けると、関数 A と同じパフォーマンスで正常に実行されます。そのため、UDF 内のCTE に問題があるように見えるか、逆に、 CTE を使用する UDF。
B を実行しようとすると、テスト マシンの 1 つのコアの CPU 使用率が 100% にまで跳ね上がります。I/O はそれほど多くないようです。
SQL Server のバグとして肩をすくめてバージョン A を使用したいのですが、ルール 1 ( 「SELECT Ain't Broken」 ) を常に念頭に置いており、関数 A からの良い結果が得られることを懸念しています。 Bが別のサーバーで行うのと同じように「失敗」するという、どういうわけかローカライズされたまぐれです。
何か案は?
更新- 再現する完全な自己完結型スクリプトを含めています。
GetAncestor 関数
[SqlFunction(FillRowMethodName = "FillAncestor",
TableDefinition = "Ancestor hierarchyid", IsDeterministic = true,
IsPrecise = true, DataAccess = DataAccessKind.None)]
public static IEnumerable GetAncestors(SqlHierarchyId h)
{
while (!h.IsNull)
{
yield return h;
h = h.GetAncestor(1);
}
}
スキーマの作成
BEGIN TRAN
CREATE TABLE Hierarchy
(
ID int NOT NULL IDENTITY(1, 1)
CONSTRAINT PK_Hierarchy PRIMARY KEY CLUSTERED,
Node hierarchyid NOT NULL,
[Level] as Node.GetLevel(),
Name varchar(50) NOT NULL
)
CREATE INDEX IX_Hierarchy_Node
ON Hierarchy (Node)
INCLUDE (Name)
CREATE INDEX IX_Hierarchy_NodeBF
ON Hierarchy ([Level], Node)
GO
INSERT Hierarchy (Node, Name)
SELECT CAST('/1/' AS hierarchyid), 'Alice' UNION ALL
SELECT CAST('/1/1/' AS hierarchyid), 'Bob' UNION ALL
SELECT CAST('/1/1/1/' AS hierarchyid), 'Charles' UNION ALL
SELECT CAST('/1/1/2/' AS hierarchyid), 'Dave' UNION ALL
SELECT CAST('/1/1/3/' AS hierarchyid), 'Ellen' UNION ALL
SELECT CAST('/1/2/' AS hierarchyid), 'Fred' UNION ALL
SELECT CAST('/1/3/' AS hierarchyid), 'Graham' UNION ALL
SELECT CAST('/1/3/1/' AS hierarchyid), 'Harold' UNION ALL
SELECT CAST('/1/3/2/' AS hierarchyid), 'Isabelle' UNION ALL
SELECT CAST('/1/4/' AS hierarchyid), 'John' UNION ALL
SELECT CAST('/2/' AS hierarchyid), 'Karen' UNION ALL
SELECT CAST('/2/1/' AS hierarchyid), 'Liam' UNION ALL
SELECT CAST('/2/2/' AS hierarchyid), 'Mary' UNION ALL
SELECT CAST('/2/2/1/' AS hierarchyid), 'Nigel' UNION ALL
SELECT CAST('/2/2/2/' AS hierarchyid), 'Oliver' UNION ALL
SELECT CAST('/2/3/' AS hierarchyid), 'Peter' UNION ALL
SELECT CAST('/2/3/1/' AS hierarchyid), 'Quinn'
GO
CREATE TYPE UniqueIntTable AS TABLE
(
Value int NOT NULL,
PRIMARY KEY (Value)
)
GO
COMMIT
GO
上記のコード/スクリプトを使用して、CLR 関数/DB スキーマを作成できます。元のスクリプトと同じものを使用しGoodFunction
ます。BadFunction