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

Principes de base des expressions de table, partie 12 - Fonctions de table en ligne

Cet article est la douzième partie d'une série sur les expressions de table nommées. Jusqu'à présent, j'ai couvert les tables dérivées et les CTE, qui sont des expressions de table nommées à portée d'instruction, et les vues, qui sont des expressions de table nommées réutilisables. Ce mois-ci, je présente les fonctions de table en ligne, ou iTVF, et décris leurs avantages par rapport aux autres expressions de table nommées. Je les compare également aux procédures stockées, en me concentrant principalement sur les différences en termes de stratégie d'optimisation par défaut, et de plan de mise en cache et de comportement de réutilisation. Il y a beaucoup à couvrir en termes d'optimisation, donc je vais commencer la discussion ce mois-ci et la poursuivre le mois prochain.

Dans mes exemples, j'utiliserai un exemple de base de données appelé TSQLV5. Vous pouvez trouver le script qui le crée et le remplit ici et son diagramme ER ici.

Qu'est-ce qu'une fonction table en ligne ?

Par rapport aux expressions de table nommées précédemment couvertes, les iTVF ressemblent principalement à des vues. Comme les vues, les iTVF sont créés en tant qu'objet permanent dans la base de données et sont donc réutilisables par les utilisateurs autorisés à interagir avec eux. Le principal avantage des iTVF par rapport aux vues est le fait qu'elles prennent en charge les paramètres d'entrée. Ainsi, la façon la plus simple de décrire une iTVF est une vue paramétrée, bien que techniquement vous la créiez avec une instruction CREATE FUNCTION et non avec une instruction CREATE VIEW.

Il est important de ne pas confondre les iTVF avec les fonctions table multi-instructions (MSTVF). La première est une expression de table nommée inlinable basée sur une seule requête similaire à une vue et est au centre de cet article. Ce dernier est un module de programmation qui renvoie une variable de table en sortie, avec un flux multi-instructions dans son corps dont le but est de remplir la variable de table renvoyée avec des données.

Syntaxe

Voici la syntaxe T-SQL pour créer un iTVF :

CREATE [ OR ALTER ] FUNCTION [ . ]

[ () ]

TABLEAU DES RETOURS

[ AVEC ]

AS

RETOUR

[; ]

Observez dans la syntaxe la possibilité de définir des paramètres d'entrée.

Le but de l'attribut SCHEMABIDNING est le même que pour les vues et doit être évalué sur la base de considérations similaires. Pour plus de détails, consultez la partie 10 de la série.

Un exemple

À titre d'exemple pour un iTVF, supposons que vous deviez créer une expression de table nommée réutilisable qui accepte comme entrées un ID client (@custid) et un numéro (@n) et renvoie le nombre demandé de commandes les plus récentes à partir de la table Sales.Orders pour le client d'entrée.

Vous ne pouvez pas implémenter cette tâche avec une vue car les vues ne prennent pas en charge les paramètres d'entrée. Comme mentionné, vous pouvez considérer un iTVF comme une vue paramétrée, et en tant que tel, c'est le bon outil pour cette tâche.

Avant d'implémenter la fonction elle-même, voici le code pour créer un index de support sur la table Sales.Orders :

USE TSQLV5;
GO
 
CREATE INDEX idx_nc_cid_odD_oidD_i_eid
  ON Sales.Orders(custid, orderdate DESC, orderid DESC)
  INCLUDE(empid);

Et voici le code pour créer la fonction, nommée Sales.GetTopCustOrders :

CREATE OR ALTER FUNCTION Sales.GetTopCustOrders
  ( @custid AS INT, @n AS BIGINT )
RETURNS TABLE
AS
RETURN
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC;
GO

Tout comme avec les tables et les vues de base, lorsque vous récupérez des données, vous spécifiez les iTVF dans la clause FROM d'une instruction SELECT. Voici un exemple demandant les trois commandes les plus récentes pour le client 1 :

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(1, 3);

J'appellerai cet exemple la requête 1. Le plan de la requête 1 est illustré à la figure 1.

Figure 1 :Plan pour la requête 1

Qu'y a-t-il en ligne à propos des iTVF ?

Si vous vous interrogez sur la source du terme inline dans les fonctions table en ligne, cela a à voir avec la façon dont elles sont optimisées. Le concept d'inlining est applicable aux quatre types d'expressions de table nommées prises en charge par T-SQL, et implique en partie ce que j'ai décrit dans la partie 4 de la série comme désimbrication/substitution. Assurez-vous de revoir la section pertinente de la partie 4 si vous avez besoin d'un rappel.

Comme vous pouvez le voir sur la figure 1, grâce au fait que la fonction a été intégrée, SQL Server a pu créer un plan optimal qui interagit directement avec les index de la table de base sous-jacente. Dans notre cas, le plan effectue une recherche dans l'index de support que vous avez créé précédemment.

Les iTVF poussent le concept d'inlining un peu plus loin en appliquant l'optimisation de l'intégration des paramètres par défaut. Paul White décrit l'optimisation de l'intégration des paramètres dans son excellent article Parameter Sniffing, Embedding, and the RECOMPILE Options. Avec l'optimisation de l'intégration des paramètres, les références des paramètres de requête sont remplacées par les valeurs constantes littérales de l'exécution en cours, puis le code avec les constantes est optimisé.

Observez dans le plan de la figure 1 que le prédicat de recherche de l'opérateur Index Seek et l'expression supérieure de l'opérateur Top affichent les valeurs constantes littérales intégrées 1 et 3 de l'exécution de la requête actuelle. Ils n'affichent pas les paramètres @custid et @n, respectivement.

Avec les iTVF, l'optimisation de l'intégration des paramètres est utilisée par défaut. Avec les procédures stockées, les requêtes paramétrées sont optimisées par défaut. Vous devez ajouter OPTION(RECOMPILE) à la requête d'une procédure stockée pour demander l'optimisation de l'intégration des paramètres. Plus de détails sur l'optimisation des iTVF par rapport aux procédures stockées, y compris les implications, sous peu.

Modification des données via les iTVF

Rappelez-vous de la partie 11 de la série que tant que certaines exigences sont remplies, les expressions de table nommées peuvent être la cible d'instructions de modification. Cette capacité s'applique aux iTVF de la même manière qu'elle s'applique aux vues. Par exemple, voici le code que vous pouvez utiliser pour supprimer les trois commandes les plus récentes du client 1 (ne l'exécutez pas réellement) :

DELETE FROM Sales.GetTopCustOrders(1, 3);

Plus précisément dans notre base de données, tenter d'exécuter ce code échouerait en raison de l'application de l'intégrité référentielle (les commandes concernées ont des lignes de commande associées dans la table Sales.OrderDetails), mais il s'agit d'un code valide et pris en charge.

iTVF par rapport aux procédures stockées

Comme mentionné précédemment, la stratégie d'optimisation des requêtes par défaut pour les iTVF est différente de celle des procédures stockées. Avec les iTVF, la valeur par défaut consiste à utiliser l'optimisation de l'intégration des paramètres. Avec les procédures stockées, la valeur par défaut consiste à optimiser les requêtes paramétrées tout en appliquant le reniflage des paramètres. Pour obtenir l'incorporation de paramètres pour une requête de procédure stockée, vous devez ajouter OPTION(RECOMPILE).

Comme pour de nombreuses stratégies et techniques d'optimisation, l'intégration de paramètres a ses avantages et ses inconvénients.

Le principal avantage est qu'il permet des simplifications de requêtes qui peuvent parfois se traduire par des plans plus efficaces. Certaines de ces simplifications sont vraiment fascinantes. Paul le démontre avec des procédures stockées dans son article, et je le démontrerai avec les iTVF le mois prochain.

Le principal inconvénient de l'optimisation de l'intégration des paramètres est que vous n'obtenez pas un comportement efficace de mise en cache et de réutilisation des plans comme vous le faites pour les plans paramétrés. Avec chaque combinaison distincte de valeurs de paramètre, vous obtenez une chaîne de requête distincte, et donc une compilation distincte qui se traduit par un plan en cache distinct. Avec les iTVF à entrées constantes, vous pouvez obtenir un comportement de réutilisation du plan, mais uniquement si les mêmes valeurs de paramètre sont répétées. Évidemment, une requête de procédure stockée avec OPTION(RECOMPILE) ne réutilisera pas un plan même en répétant les mêmes valeurs de paramètre, sur demande.

Je vais démontrer trois cas :

  1. Plans réutilisables avec des constantes résultant du paramètre par défaut intégrant l'optimisation pour les requêtes iTVF avec des constantes
  2. Plans paramétrés réutilisables résultant de l'optimisation par défaut des requêtes de procédures stockées paramétrées
  3.  Plans non réutilisables avec des constantes résultant de l'optimisation de l'intégration des paramètres pour les requêtes de procédure stockée avec OPTION(RECOMPILE)

Commençons par le cas n°1.

Utilisez le code suivant pour interroger notre iTVF avec @custid =1 et @n =3 :

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(1, 3);

Pour rappel, il s'agirait de la deuxième exécution du même code puisque vous l'avez déjà exécuté une fois avec les mêmes valeurs de paramètres plus tôt, ce qui a donné le plan illustré à la figure 1.

Utilisez le code suivant pour interroger l'iTVF avec @custid =2 et @n =3 une fois :

SELECT orderid, orderdate, empid
FROM Sales.GetTopCustOrders(2, 3);

Je ferai référence à ce code en tant que requête 2. Le plan de la requête 2 est illustré à la figure 2.

Figure 2 :Plan pour la requête 2

Rappelez-vous que le plan de la figure 1 pour la requête 1 fait référence à l'ID client constant 1 dans le prédicat de recherche, alors que ce plan fait référence à l'ID client constant 2.

Utilisez le code suivant pour examiner les statistiques d'exécution des requêtes :

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders(%';

Ce code génère la sortie suivante :

plan_handle         execution_count text                                           query_plan
------------------- --------------- ---------------------------------------------- ----------------
0x06000B00FD9A1...  1               SELECT ... FROM Sales.GetTopCustOrders(2, 3);  <ShowPlanXML...>
0x06000B00F5C34...  2               SELECT ... FROM Sales.GetTopCustOrders(1, 3);  <ShowPlanXML...>

(2 rows affected)

Deux plans distincts sont créés ici :un pour la requête avec l'ID client 1, qui a été utilisé deux fois, et un autre pour la requête avec l'ID client 2, qui a été utilisé une fois. Avec un très grand nombre de combinaisons distinctes de valeurs de paramètres, vous vous retrouverez avec un grand nombre de compilations et de plans en cache.

Passons au cas n°2 :la stratégie d'optimisation par défaut des requêtes de procédures stockées paramétrées. Utilisez le code suivant pour encapsuler notre requête dans une procédure stockée appelée Sales.GetTopCustOrders2 :

CREATE OR ALTER PROC Sales.GetTopCustOrders2
  ( @custid AS INT, @n AS BIGINT )
AS
  SET NOCOUNT ON;
 
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC;
GO

Utilisez le code suivant pour exécuter la procédure stockée avec @custid =1 et @n =3 deux fois :

EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;

La première exécution déclenche l'optimisation de la requête, aboutissant au plan paramétré illustré à la figure 3 :

Figure 3 :Planifier la procédure Sales.GetTopCustOrders2

Observez la référence au paramètre @custid dans le prédicat de recherche et au paramètre @n dans l'expression supérieure.

Utilisez le code suivant pour exécuter la procédure stockée avec @custid =2 et @n =3 une fois :

EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;

Le plan paramétré mis en cache illustré à la figure 3 est réutilisé.

Utilisez le code suivant pour examiner les statistiques d'exécution des requêtes :

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';

Ce code génère la sortie suivante :

plan_handle         execution_count text                                            query_plan
------------------- --------------- ----------------------------------------------- ----------------
0x05000B00F1604...  3               ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...>

(1 row affected)

Un seul plan paramétré a été créé et mis en cache, et utilisé trois fois, malgré l'évolution des valeurs d'ID client.

Passons au cas n°3. Comme mentionné, avec les requêtes de procédure stockée, vous pouvez obtenir une optimisation de l'intégration des paramètres lors de l'utilisation de OPTION(RECOMPILE). Utilisez le code suivant pour modifier la requête de procédure afin d'inclure cette option :

CREATE OR ALTER PROC Sales.GetTopCustOrders2
  ( @custid AS INT, @n AS BIGINT )
AS
  SET NOCOUNT ON;
 
  SELECT TOP (@n) orderid, orderdate, empid
  FROM Sales.Orders
  WHERE custid = @custid
  ORDER BY orderdate DESC, orderid DESC
  OPTION(RECOMPILE);
GO

Exécutez la proc avec @custid =1 et @n =3 deux fois :

EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;
EXEC Sales.GetTopCustOrders2 @custid = 1, @n = 3;

Vous obtenez le même plan illustré précédemment dans la figure 1 avec les constantes intégrées.

Exécutez le proc avec @custid =2 et @n =3 une fois :

EXEC Sales.GetTopCustOrders2 @custid = 2, @n = 3;

Vous obtenez le même plan illustré précédemment dans la figure 2 avec les constantes intégrées.

Examinez les statistiques d'exécution des requêtes :

SELECT Q.plan_handle, Q.execution_count, T.text, P.query_plan
FROM sys.dm_exec_query_stats AS Q
  CROSS APPLY sys.dm_exec_sql_text(Q.plan_handle) AS T
  CROSS APPLY sys.dm_exec_query_plan(Q.plan_handle) AS P
WHERE T.text LIKE '%Sales.' + 'GetTopCustOrders2%';

Ce code génère la sortie suivante :

plan_handle         execution_count text                                            query_plan
------------------- --------------- ----------------------------------------------- ----------------
0x05000B00F1604...  1               ...SELECT TOP (@n)...WHERE custid = @custid...; <ShowPlanXML...>

(1 row affected)

Le nombre d'exécutions affiche 1, reflétant uniquement la dernière exécution. SQL Server met en cache le dernier plan exécuté, afin qu'il puisse afficher des statistiques pour cette exécution, mais sur demande, il ne réutilise pas le plan. Si vous vérifiez le plan affiché sous l'attribut query_plan, vous constaterez qu'il s'agit de celui créé pour les constantes lors de la dernière exécution, illustré précédemment à la figure 2.

Si vous recherchez moins de compilations et un comportement efficace de mise en cache et de réutilisation des plans, l'approche d'optimisation des procédures stockées par défaut des requêtes paramétrées est la solution.

Une implémentation basée sur iTVF présente un gros avantage par rapport à une implémentation basée sur une procédure stockée, lorsque vous devez appliquer la fonction à chaque ligne d'une table et transmettre des colonnes de la table en tant qu'entrées. Par exemple, supposons que vous deviez renvoyer les trois commandes les plus récentes pour chaque client dans la table Sales.Customers. Aucune construction de requête ne vous permet d'appliquer une procédure stockée par ligne dans une table. Vous pouvez implémenter une solution itérative avec un curseur, mais c'est toujours un bon jour où vous pouvez éviter les curseurs. En combinant l'opérateur APPLY avec un appel iTVF, vous pouvez réaliser la tâche proprement et proprement, comme ceci :

SELECT C.custid, O.orderid, O.orderdate, O.empid
FROM Sales.Customers AS C
  CROSS APPLY Sales.GetTopCustOrders( C.custid, 3 ) AS O;

Ce code génère la sortie suivante (abrégé) :

custid      orderid     orderdate  empid
----------- ----------- ---------- -----------
1           11011       2019-04-09 3
1           10952       2019-03-16 1
1           10835       2019-01-15 1
2           10926       2019-03-04 4
2           10759       2018-11-28 3
2           10625       2018-08-08 3
...

(263 rows affected)

L'appel de fonction est intégré et la référence au paramètre @custid est remplacée par la corrélation C.custid. Il en résulte le plan illustré à la figure 4.

Figure 4 :Planifier la requête avec APPLY et Sales.GetTopCustOrders iTVF

Le plan analyse certains index de la table Sales.Customers pour obtenir l'ensemble d'ID client et applique une recherche dans l'index de prise en charge que vous avez créé précédemment sur Sales.Orders par client. Il n'y a qu'un seul plan depuis que la fonction a été intégrée dans la requête externe, se transformant en une jointure corrélée ou latérale. Ce plan est très efficace, en particulier lorsque la colonne custid dans Sales.Orders est très dense, c'est-à-dire lorsqu'il existe un petit nombre d'ID client distincts.

Bien sûr, il existe d'autres façons d'implémenter cette tâche, comme l'utilisation d'un CTE avec la fonction ROW_NUMBER. Une telle solution a tendance à mieux fonctionner que celle basée sur APPLY lorsque la colonne custid dans la table Sales.Orders a une faible densité. Quoi qu'il en soit, la tâche spécifique que j'ai utilisée dans mes exemples n'est pas si importante pour les besoins de notre discussion. Mon but était d'expliquer les différentes stratégies d'optimisation que SQL Server utilise avec les différents outils.

Lorsque vous avez terminé, utilisez le code suivant pour le nettoyage :

DROP INDEX IF EXISTS idx_nc_cid_odD_oidD_i_eid ON Sales.Orders;

Résumé et suite

Alors, qu'avons-nous appris de cela ?

Un iTVF est une expression de table nommée paramétrée réutilisable.

SQL Server utilise une stratégie d'optimisation d'intégration de paramètres avec des iTVF par défaut et une stratégie d'optimisation de requête paramétrée avec des requêtes de procédure stockée. L'ajout de OPTION(RECOMPILE) à une requête de procédure stockée peut entraîner une optimisation de l'intégration des paramètres.

Si vous souhaitez obtenir moins de compilations et un comportement efficace de mise en cache et de réutilisation des plans, les plans de requête de procédure paramétrés sont la solution.

Les plans pour les requêtes iTVF sont mis en cache et peuvent être réutilisés, tant que les mêmes valeurs de paramètre sont répétées.

Vous pouvez facilement combiner l'utilisation de l'opérateur APPLY et d'un iTVF pour appliquer l'iTVF à chaque ligne du tableau de gauche, en transmettant les colonnes du tableau de gauche comme entrées à l'iTVF.

Comme mentionné, il y a beaucoup à couvrir sur l'optimisation des iTVF. Ce mois-ci, j'ai comparé les iTVF et les procédures stockées en termes de stratégie d'optimisation par défaut et de comportement de mise en cache et de réutilisation du plan. Le mois prochain, j'approfondirai les simplifications résultant de l'optimisation de l'intégration des paramètres.


No