Вот крутой вопрос для парней из SQL - кто-нибудь может подумать о причине, по которой первая из этих функций работает нормально, а вторая работает очень медленно?
Функция 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
Ниже я объясню, что делает эта функция, но прежде чем углубиться в это, я хочу отметить, что я не думаю, что это важно, потому что, насколько я могу судить, эти две функции точно то же самое! Разница лишь в том, что один использует CTE, а другой - подзапрос; содержимое подзапроса в A и CTE в B идентичны .
В случае, если кто-то решит, что это имеет значение: Цель этой функции - просто выделить всех возможных предков (родитель, дедушка и т. Д.) Из произвольного числа мест в иерархии. Столбец Node
- это hierarchyid
, а dbo.GetAncestors
- это функция CLR, которая просто идет по пути, она не делает никакого доступа к данным.
UniqueIntTable
- это то, что он подразумевает - это определенный пользователем тип таблицы с одним столбцом, Value int NOT NULL PRIMARY KEY
. Все, что здесь должно быть проиндексировано, индексируется - план выполнения функции A - это, по сути, всего два поиска индекса и совпадение хеша, как и должно быть с функцией B.
Некоторые странные аспекты этой странной проблемы:
Я даже не могу получить примерный план выполнения для простого запроса с использованием функции B. Похоже, что проблема производительности связана с компиляцией этого простого функция поиска.
Если я возьму «тело» из функции B и просто вставлю его во встроенный запрос, он будет работать нормально, с той же производительностью, что и функция A. Так что, похоже, проблема только в CTE внутри UDF или, наоборот, только с UDF, использующим CTE.
Загрузка процессора на одном ядре на тестовой машине резко возрастает до 100%, когда я пытаюсь запустить B. Кажется, что количество операций ввода-вывода невелико.
Я хочу просто проигнорировать это как ошибку SQL Server и использовать версию A, но я всегда стараюсь помнить Правило № 1 ( «ВЫБРАТЬ НЕ Сломано» ), и я ' Я обеспокоен тем, что хорошие результаты от функции A являются каким-то образом локализованной случайностью, что она "потерпит неудачу" так же, как B на другом сервере.
Есть идеи?
ОБНОВЛЕНИЕ - Сейчас я включаю полный автономный скрипт для воспроизведения.
Функция GetAncestors
[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 / схемы БД; используйте те же самые сценарии GoodFunction
и BadFunction
в оригинале.