Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Fonctions SQL Server définies par l'utilisateur

Les fonctions définies par l'utilisateur dans SQL Server (UDF) sont des objets clés que chaque développeur doit connaître. Bien qu'ils soient très utiles dans de nombreux scénarios (clauses WHERE, colonnes calculées et contraintes de vérification), ils présentent encore certaines limitations et mauvaises pratiques qui peuvent entraîner des problèmes de performances. Les UDF multi-instructions peuvent avoir des impacts significatifs sur les performances, et cet article traitera spécifiquement de ces scénarios.

Les fonctions ne sont pas implémentées de la même manière que dans les langages orientés objet, bien que les fonctions table inline puissent être utilisées dans des scénarios lorsque vous avez besoin de vues paramétrées, cela ne s'applique pas aux fonctions qui renvoient des scalaires ou des tables. Ces fonctions doivent être utilisées avec précaution car elles peuvent causer de nombreux problèmes de performances. Cependant, ils sont essentiels dans de nombreux cas, nous devrons donc accorder plus d'attention à leurs implémentations. Les fonctions sont utilisées dans les instructions SQL dans des lots, des procédures, des déclencheurs ou des vues, dans des requêtes SQL ad hoc ou dans le cadre de requêtes de rapport générées par des outils tels que PowerBI ou Tableau, dans des champs calculés et des contraintes de vérification. Alors que les fonctions scalaires peuvent être récursives jusqu'à 32 niveaux, les fonctions de table ne prennent pas en charge la récursivité.

Types de fonctions dans SQL Server

Dans SQL Server, nous avons trois types de fonctions :les fonctions scalaires définies par l'utilisateur (SF) qui renvoient une seule valeur scalaire, les fonctions table définies par l'utilisateur (TVF) qui renvoient une table et les fonctions table inline (ITVF) qui n'ont pas de corps fonctionnel. Les fonctions de table peuvent être en ligne ou multi-instructions. Les fonctions inline n'ont pas de variables de retour, elles ne font que retourner des fonctions de valeur. Les fonctions multi-instructions sont contenues dans les blocs de code BEGIN-END et peuvent avoir plusieurs instructions T-SQL qui ne créent aucun effet secondaire (comme la modification du contenu d'une table).

Nous allons montrer chaque type de fonction dans un exemple simple :

/**
inline table function
**/
CREATE FUNCTION dbo.fnInline( @P1 INT, @P2 VARCHAR(50) )
RETURNS TABLE
AS
RETURN ( SELECT @P1 AS P_OUT_1, @P2 AS P_OUT_2 )





/**
multi-statement table function
**/
CREATE FUNCTION dbo.fnMultiTable(  @P1 INT, @P2 VARCHAR(50)  )
RETURNS @r_table TABLE ( OUT_1 INT, OUT_2 VARCHAR(50) )
AS
  BEGIN
    INSERT @r_table SELECT @P1, @P2;
    RETURN;
  END;

/**
scalar function
**/
CREATE FUNCTION dbo.fnScalar(  @P1 INT, @P2 INT  )
RETURNS INT
AS
BEGIN
    RETURN @P1 + @P2
END

Limites des fonctions SQL Server

Comme mentionné dans l'introduction, il existe certaines limitations dans l'utilisation des fonctions, et j'en explorerai quelques-unes ci-dessous. Une liste complète peut être trouvée sur Microsoft Docs :

  • Il n'y a pas de concept de fonctions temporaires
  • Vous ne pouvez pas créer une fonction dans une autre base de données, mais, selon vos privilèges, vous pouvez y accéder
  • Avec les UDF, vous n'êtes pas autorisé à effectuer des actions qui modifient l'état de la base de données,
  • Dans UDF, vous ne pouvez pas appeler une procédure, à l'exception de la procédure stockée étendue
  • UDF ne peut pas renvoyer un ensemble de résultats, mais uniquement un type de données de table
  • Vous ne pouvez pas utiliser de SQL dynamique ni de tables temporaires dans les UDF
  • Les fonctions UDF sont limitées dans leurs capacités de gestion des erreurs :elles ne prennent pas en charge RAISERROR ni TRY…CATCH et vous ne pouvez pas obtenir de données à partir de la variable système @ERROR

Qu'est-ce qui est autorisé dans les fonctions multi-instruction ?

Seuls les éléments suivants sont autorisés :

  • Énoncés de devoir
  • Toutes les instructions de contrôle de flux, à l'exception du bloc TRY…CATCH
  • Appels DECLARE, utilisés pour créer des variables locales et des curseurs
  • Vous pouvez utiliser des requêtes SELECT comportant des listes avec des expressions et attribuer ces valeurs à des variables déclarées localement
  • Les curseurs ne peuvent référencer que des tables locales et doivent être ouverts et fermés dans le corps de la fonction. FETCH peut uniquement affecter ou modifier les valeurs des variables locales, pas récupérer ou modifier les données de la base de données

Qu'est-ce qui doit être évité dans les fonctions multi-instructions, bien qu'autorisé ?

  • Vous devez éviter les scénarios dans lesquels vous utilisez des colonnes calculées avec des fonctions scalaires ; cela entraînera des reconstructions d'index et des mises à jour lentes nécessitant des recalculs
  • Considérez que toute fonction multi-instructions a son plan d'exécution et son impact sur les performances
  • La fonction UDF table multi-instructions, si elle est utilisée dans une expression SQL ou une instruction de jointure, sera lente en raison du plan d'exécution non optimal
  • N'utilisez pas de fonctions scalaires dans les instructions WHERE et les clauses ON, sauf si vous êtes sûr qu'elles interrogeront un petit ensemble de données, et que cet ensemble de données restera petit à l'avenir

Noms et paramètres des fonctions

Comme tout autre nom d'objet, les noms de fonction doivent se conformer aux règles des identificateurs et doivent être uniques au sein de leur schéma. Si vous créez des fonctions scalaires, vous pouvez les exécuter à l'aide de l'instruction EXECUTE. Dans ce cas, vous n'avez pas à mettre le nom du schéma dans le nom de la fonction. Voir l'exemple de l'appel de la fonction EXECUTE ci-dessous (nous créons une fonction qui retourne l'occurrence du Nième jour d'un mois puis récupère cette donnée) :

CREATE FUNCTION dbo.fnGetDayofWeekInMonth 
(
  @YearInput          VARCHAR(50),
  @MonthInput       VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
  @WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
  @CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
 ) 
  RETURNS DATETIME  
  AS
  BEGIN
  RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, 
          CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
          -
          (DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, 
                         CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
          [email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
  END        


-- In SQL Server 2012 and later versions, you can use the EXECUTE command or the SELECT command to run a UDF, or use a standard approach
DECLARE @ret DateTime
EXEC @ret = fnGetDayofWeekInMonth '2020', 'Jan', 'Mon',2
SELECT @ret AS Third_Monday_In_January_2020

 SELECT dbo.fnGetDayofWeekInMonth('2020', 'Jan', DEFAULT, DEFAULT) 
               AS 'Using default',
               dbo.fnGetDayofWeekInMonth('2020', 'Jan', 'Mon', 2) AS 'explicit'

Nous pouvons définir des valeurs par défaut pour les paramètres de fonction, ils doivent être préfixés par "@" et conformes aux règles de nommage des identifiants. Les paramètres ne peuvent être que des valeurs constantes, ils ne peuvent pas être utilisés dans des requêtes SQL à la place de tables, de vues, de colonnes ou d'autres objets de base de données, et les valeurs ne peuvent pas être des expressions, même déterministes. Tous les types de données sont autorisés, à l'exception du type de données TIMESTAMP, et aucun type de données non scalaire ne peut être utilisé, à l'exception des paramètres de table. Dans les appels de fonction "standard", vous devez spécifier l'attribut DEFAULT si vous souhaitez donner à l'utilisateur final la possibilité de rendre un paramètre facultatif. Dans les nouvelles versions, en utilisant la syntaxe EXECUTE, ce n'est plus nécessaire, vous n'entrez simplement pas ce paramètre dans l'appel de fonction. Si nous utilisons des types de table personnalisés, ils doivent être marqués comme READONLY, ce qui signifie que nous ne pouvons pas modifier la valeur initiale dans la fonction, mais ils peuvent être utilisés dans les calculs et les définitions d'autres paramètres.

Performances des fonctions SQL Server

Le dernier sujet que nous aborderons dans cet article, en utilisant les fonctions du chapitre précédent, est la performance des fonctions. Nous allons étendre cette fonction et surveiller les délais d'exécution et la qualité des plans d'exécution. Nous commençons par créer d'autres versions de fonctions, et continuons avec leur comparaison :

CREATE FUNCTION dbo.fnGetDayofWeekInMonthBound 
(
  @YearInput    VARCHAR(50),
  @MonthInput   VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
  @WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
  @CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
  ) 
  RETURNS DATETIME
  WITH SCHEMABINDING
  AS
  BEGIN
  RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
          -(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
          [email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
  END        
GO

CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthInline (
  @YearInput    VARCHAR(50),
  @MonthInput   VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
  @WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
  @CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
  ) 
  RETURNS TABLE
  WITH SCHEMABINDING
  AS
  RETURN (SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
          -(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
          [email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate)
GO

CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthTVF (
  @YearInput    VARCHAR(50),
  @MonthInput   VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
  @WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
  @CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
  ) 
  RETURNS @When TABLE (TheDate DATETIME)
  WITH schemabinding
  AS
  Begin
  INSERT INTO @When(TheDate) 
    SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
          -(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
          [email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
  RETURN
  end   
  GO

Créer des appels de test et des scénarios de test

Nous commençons par les versions de table :

SELECT * FROM dbo.fnNthDayOfWeekOfMonthTVF('2020','Feb','Tue',2)

SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM    dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)),113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
 
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113)  FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
  OUTER apply dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)

Création de données de test :

IF EXISTS(SELECT * FROM tempdb.sys.tables WHERE name LIKE '#DataForTest%')
  DROP TABLE #DataForTest
GO
SELECT * 
INTO #DataForTest
 FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
  CROSS join (VALUES ('jan'),('feb'),('mar'),('apr'),('may'),('jun'),('jul'),('aug'),('sep'),('oct'),('nov'),('dec'))months(Themonth)
  CROSS join (VALUES ('Mon'),('Tue'),('Wed'),('Thu'),('Fri'),('Sat'),('Sun'))day(TheDay)
  CROSS join (VALUES (1),(2),(3),(4))nth(nth)

Tester les performances :

DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE())

Début du chronométrage :

INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start

Tout d'abord, nous n'utilisons aucun type de fonction pour obtenir une ligne de base :

SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)+ (7*Nth)-1
          -(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0))
		  [email protected]@DateFirst+(CHARINDEX(TheDay,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate
  INTO #Test0
  FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Using the code entirely unwrapped';

Nous utilisons maintenant une fonction inline Table-valued croisée :

SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
 INTO #Test1
  FROM #DataForTest
    CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'Inline function cross apply'

Nous utilisons une fonction inline Table-valued croisée :

SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsDate
  INTO #Test2
  FROM #DataForTest
 INSERT INTO @TableLog(Reason) SELECT 'Inline function Derived table'

Pour comparer les non fiables, nous utilisons une fonction scalaire avec la liaison de schéma :

SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonthBound(TheYear,TheMonth,TheDay,nth))itsdate
  INTO #Test3
  FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Trusted (Schemabound) scalar function'
 

Ensuite, nous utilisons une fonction scalaire sans liaison de schéma :

SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
  INTO #Test6
  FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'

Ensuite, la fonction de table multi-instructions a dérivé :

SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsdate
  INTO #Test4
  FROM #DataForTest 
INSERT INTO @TableLog(Reason) SELECT 'multi-statement table function derived'

Enfin, le tableau multi-instructions s'applique :

SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
  INTO #Test5
  FROM #DataForTest
    CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'multi-statement cross APPLY'--where the routine you want to time ends

Listez tous les horaires :

SELECT ending.Reason AS Test, DateDiff(ms, starting.TimeOfEvent,ending.TimeOfEvent) [AS Time (ms)] FROM @TableLog starting
INNER JOIN @TableLog ending ON ending.OrderVal=starting.OrderVal+1

 
DROP table #Test0
DROP table #Test1
DROP table #Test2
DROP table #Test3
DROP table #Test4
DROP table #Test5
DROP table #Test6
DROP TABLE #DataForTest

Le tableau ci-dessus montre clairement que vous devez considérer les performances par rapport aux fonctionnalités lorsque vous utilisez des fonctions définies par l'utilisateur.

Conclusion

Les fonctions sont appréciées par de nombreux développeurs, principalement parce qu'elles sont des "constructions logiques". Vous pouvez facilement créer des cas de test, ils sont déterministes et encapsulants, ils s'intègrent parfaitement au flux de code SQL et permettent une flexibilité dans le paramétrage. Ils constituent un bon choix lorsque vous devez implémenter une logique complexe qui doit être effectuée sur un ensemble de données plus petit ou déjà filtré que vous devrez réutiliser dans plusieurs scénarios. Les vues de table en ligne peuvent être utilisées dans les vues nécessitant des paramètres, en particulier à partir des couches supérieures (applications orientées client). D'autre part, les fonctions scalaires sont idéales pour travailler avec XML ou d'autres formats hiérarchiques, car elles peuvent être appelées de manière récursive.

Les fonctions multi-instructions définies par l'utilisateur sont un excellent ajout à votre pile d'outils de développement, mais vous devez comprendre comment elles fonctionnent et quelles sont leurs limites et leurs problèmes de performances. Leur mauvaise utilisation peut détruire les performances de n'importe quelle base de données, mais si vous savez comment utiliser ces fonctions, elles peuvent apporter de nombreux avantages à la réutilisation et à l'encapsulation du code.