Здесь мозг-твистер для парней 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, а один использует подзапрос; содержимое подзапроса в и 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 ( "SELECT is not Broken" ), и я обеспокоен тем, что хорошие результаты из функции A - это как-то локализованная случайность, что она "провалится" так же, как B на другом сервере.
Любые идеи?
ОБНОВЛЕНИЕ. Теперь я включаю полный автономный script для воспроизведения.
Функция 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
Вышеприведенный код / script может использоваться для создания схемы CLR/DB; используйте те же скрипты GoodFunction
и BadFunction
в оригинале.