En fin de compte, il n'est pas possible de forcer SQL Server à évaluer une UDF scalaire une seule fois dans une requête. Cependant, certaines mesures peuvent être prises pour l'encourager. Avec les tests, je pense que vous pouvez obtenir quelque chose qui fonctionne avec la version actuelle de SQL Server, mais il est possible que de futures modifications vous obligent à revoir votre code.
S'il est possible d'éditer le code, une bonne première chose à essayer est de rendre la fonction déterministe si possible. Paul White souligne ici que la fonction doit être créée avec l' SCHEMABINDING
option et que le code de fonction lui-même doit être déterministe.
Après avoir effectué la modification suivante:
CREATE OR ALTER FUNCTION dbo.EXPENSIVE_UDF () RETURNS INT
WITH SCHEMABINDING
AS
BEGIN
DECLARE @tbl TABLE (VAL VARCHAR(5));
-- make the function expensive to call
INSERT INTO @tbl
SELECT [VALUE]
FROM STRING_SPLIT(REPLICATE(CAST('Z ' AS VARCHAR(MAX)), 20000), ' ');
RETURN 1;
END;
La requête de la question est exécutée en 64 ms:
SELECT x1.ID
FROM dbo.X_100_INTEGERS x1
WHERE x1.ID >= dbo.EXPENSIVE_UDF();
Le plan de requête n'a plus l'opérateur de filtre:
Pour être sûr qu'il ne s'est exécuté qu'une fois, nous pouvons utiliser le nouveau DMV sys.dm_exec_function_stats publié dans SQL Server 2016:
SELECT execution_count
FROM sys.dm_exec_function_stats
WHERE object_id = OBJECT_ID('EXPENSIVE_UDF', 'FN');
L'émission d'un ALTER
contre la fonction réinitialisera le execution_count
pour cet objet. La requête ci-dessus renvoie 1, ce qui signifie que la fonction n'a été exécutée qu'une seule fois.
Notez que ce n'est pas parce que la fonction est déterministe qu'elle ne sera évaluée qu'une seule fois pour n'importe quelle requête. En fait, pour certaines requêtes, l'ajout SCHEMABINDING
peut dégrader les performances. Considérez la requête suivante:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
Le superflu a DISTINCT
été ajouté pour se débarrasser d'un opérateur Filter. Le plan semble prometteur:
Sur la base de cela, on s'attendrait à ce que l'UDF soit évalué une fois et utilisé comme table externe dans la jointure de boucle imbriquée. Cependant, la requête prend 6446 ms pour s'exécuter sur ma machine. Selon sys.dm_exec_function_stats
la fonction a été exécutée 100 fois. Comment est-ce possible? Dans " Compute Scalars, Expressions and Execution Plan Performance ", Paul White souligne que l'opérateur Compute Scalar peut être différé:
Le plus souvent, un calcul scalaire définit simplement une expression; le calcul réel est différé jusqu'à ce que quelque chose plus tard dans le plan d'exécution ait besoin du résultat.
Pour cette requête, il semble que l'appel UDF a été différé jusqu'à ce qu'il soit nécessaire, moment auquel il a été évalué 100 fois.
Fait intéressant, l'exemple CTE s'exécute en 71 ms sur ma machine lorsque l'UDF n'est pas défini avec SCHEMABINDING
, comme dans la question d'origine. La fonction n'est exécutée qu'une seule fois lorsque la requête est exécutée. Voici le plan de requête pour cela:
On ne sait pas pourquoi le calcul scalaire n'est pas différé. Cela peut être dû au fait que le non-déterminisme de la fonction limite le réarrangement des opérateurs que l'optimiseur de requête peut effectuer.
Une autre approche consiste à ajouter une petite table au CTE et à interroger la seule ligne de cette table. N'importe quelle petite table fera l'affaire, mais utilisons ce qui suit:
CREATE TABLE dbo.X_ONE_ROW_TABLE (ID INT NOT NULL);
INSERT INTO dbo.X_ONE_ROW_TABLE VALUES (1);
La requête devient alors:
WITH cte (UDF_VALUE) AS
(
SELECT DISTINCT dbo.EXPENSIVE_UDF() UDF_VALUE
FROM dbo.X_ONE_ROW_TABLE
)
SELECT ID
FROM dbo.X_100_INTEGERS
INNER JOIN cte ON ID >= cte.UDF_VALUE;
L'ajout de l' dbo.X_ONE_ROW_TABLE
incertitude ajoute à l'optimiseur. Si la table n'a aucune ligne, le CTE renverra 0 ligne. Dans tous les cas, l'optimiseur ne peut garantir que le CTE retournera une ligne si l'UDF n'est pas déterministe, il semble donc probable que l'UDF sera évalué avant la jointure. Je m'attendrais à ce que l'optimiseur analyse dbo.X_ONE_ROW_TABLE
, utilise un agrégat de flux pour obtenir la valeur maximale d'une ligne retournée (ce qui nécessite l'évaluation de la fonction), et l'utiliser comme table externe pour une jointure de boucle imbriquée dbo.X_100_INTEGERS
dans la requête principale . Cela semble être ce qui se passe :
La requête s'exécute en 110 ms environ sur ma machine et l'UDF n'est évalué qu'une seule fois selon sys.dm_exec_function_stats
. Il serait incorrect de dire que l'optimiseur de requêtes est obligé d'évaluer une seule fois l'UDF. Cependant, il est difficile d'imaginer une réécriture de l'optimiseur qui conduirait à une requête à moindre coût, même avec les limitations concernant UDF et le calcul des coûts scalaires.
En résumé, pour les fonctions déterministes (qui doivent inclure l' SCHEMABINDING
option), essayez d'écrire la requête de la manière la plus simple possible. Si sur SQL Server 2016 ou une version ultérieure, confirmez que la fonction n'a été exécutée qu'une seule fois à l'aide de sys.dm_exec_function_stats
. Les plans d'exécution peuvent être trompeurs à cet égard.
Pour les fonctions qui ne sont pas considérées par SQL Server comme déterministes, y compris tout ce qui manque SCHEMABINDING
, une approche consiste à placer l'UDF dans un CTE ou une table dérivée soigneusement conçue. Cela nécessite un peu de soin mais le même CTE peut fonctionner à la fois pour les fonctions déterministes et non déterministes.