Remarque :Cet article a été initialement publié uniquement dans notre eBook, Techniques hautes performances pour SQL Server, Volume 2. Vous pouvez en savoir plus sur nos eBooks ici. Notez également que certaines de ces choses peuvent changer avec les améliorations prévues de l'OLTP en mémoire dans SQL Server 2016.
Il existe certaines habitudes et bonnes pratiques que beaucoup d'entre nous développent au fil du temps en ce qui concerne le code Transact-SQL. Avec les procédures stockées en particulier, nous nous efforçons de transmettre des valeurs de paramètre du type de données correct et de nommer nos paramètres explicitement plutôt que de nous fier uniquement à la position ordinale. Parfois, cependant, nous pouvons devenir paresseux à ce sujet :nous pouvons oublier de préfixer une chaîne Unicode avec N
, ou répertoriez simplement les constantes ou les variables dans l'ordre au lieu de spécifier les noms des paramètres. Ou les deux.
Dans SQL Server 2014, si vous utilisez OLTP en mémoire ("Hekaton") et des procédures compilées en mode natif, vous souhaiterez peut-être ajuster un peu votre réflexion sur ces choses. Je vais démontrer avec du code l'exemple OLTP en mémoire RTM SQL Server 2014 sur CodePlex, qui étend l'exemple de base de données AdventureWorks2012. (Si vous allez configurer cela à partir de zéro pour suivre, veuillez jeter un coup d'œil rapide à mes observations dans un post précédent.)
Examinons la signature de la procédure stockée Sales.usp_InsertSpecialOffer_inmem
:
CREATE PROCEDURE [Sales].[usp_InsertSpecialOffer_inmem] @Description NVARCHAR(255) NOT NULL, @DiscountPct SMALLMONEY NOT NULL = 0, @Type NVARCHAR(50) NOT NULL, @Category NVARCHAR(50) NOT NULL, @StartDate DATETIME2 NOT NULL, @EndDate DATETIME2 NOT NULL, @MinQty INT NOT NULL = 0, @MaxQty INT = NULL, @SpecialOfferID INT OUTPUT WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER AS BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL=SNAPSHOT, LANGUAGE=N'us_english') DECLARE @msg nvarchar(256) -- validation removed for brevity INSERT Sales.SpecialOffer_inmem (Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty, MaxQty) VALUES (@Description, @DiscountPct, @Type, @Category, @StartDate, @EndDate, @MinQty, @MaxQty) SET @SpecialOfferID = SCOPE_IDENTITY() END GO
J'étais curieux de savoir s'il importait que les paramètres soient nommés ou si les procédures compilées en mode natif géraient mieux les conversions implicites en tant qu'arguments des procédures stockées que les procédures stockées traditionnelles. J'ai d'abord créé une copie Sales.usp_InsertSpecialOffer_inmem
comme une procédure stockée traditionnelle - cela impliquait simplement de supprimer le ATOMIC
bloquer et supprimer le NOT NULL
déclarations à partir des paramètres d'entrée :
CREATE PROCEDURE [Sales].[usp_InsertSpecialOffer] @Description NVARCHAR(255), @DiscountPct SMALLMONEY = 0, @Type NVARCHAR(50), @Category NVARCHAR(50), @StartDate DATETIME2, @EndDate DATETIME2, @MinQty INT = 0, @MaxQty INT = NULL, @SpecialOfferID INT OUTPUT AS BEGIN DECLARE @msg nvarchar(256) -- validation removed for brevity INSERT Sales.SpecialOffer_inmem (Description, DiscountPct, Type, Category, StartDate, EndDate, MinQty, MaxQty) VALUES (@Description, @DiscountPct, @Type, @Category, @StartDate, @EndDate, @MinQty, @MaxQty) SET @SpecialOfferID = SCOPE_IDENTITY() END GO
Pour minimiser les critères de décalage, la procédure s'insère toujours dans la version en mémoire de la table, Sales.SpecialOffer_inmem.
Ensuite, j'ai voulu chronométrer 100 000 appels vers les deux copies de la procédure stockée avec ces critères :
Paramètres explicitement nommés | Paramètres non nommés | |
---|---|---|
Tous les paramètres du type de données correct | x | x |
Certains paramètres de type de données incorrect | x | x |
En utilisant le batch suivant, copié pour la version traditionnelle de la procédure stockée (en supprimant simplement _inmem
à partir des quatre EXEC
appels):
SET NOCOUNT ON; CREATE TABLE #x ( i INT IDENTITY(1,1), d VARCHAR(32), s DATETIME2(7) NOT NULL DEFAULT SYSDATETIME(), e DATETIME2(7) ); GO INSERT #x(d) VALUES('Named, proper types'); GO /* this uses named parameters, and uses correct data types */ DECLARE @p1 NVARCHAR(255) = N'Product 1', @p2 SMALLMONEY = 10, @p3 NVARCHAR(50) = N'Volume Discount', @p4 NVARCHAR(50) = N'Reseller', @p5 DATETIME2 = '20140615', @p6 DATETIME2 = '20140620', @p7 INT = 10, @p8 INT = 20, @p9 INT; EXEC Sales.usp_InsertSpecialOffer_inmem @Description = @p1, @DiscountPct = @p2, @Type = @p3, @Category = @p4, @StartDate = @p5, @EndDate = @p6, @MinQty = @p7, @MaxQty = @p8, @SpecialOfferID = @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 1; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO INSERT #x(d) VALUES('Not named, proper types'); GO /* this does not use named parameters, but uses correct data types */ DECLARE @p1 NVARCHAR(255) = N'Product 1', @p2 SMALLMONEY = 10, @p3 NVARCHAR(50) = N'Volume Discount', @p4 NVARCHAR(50) = N'Reseller', @p5 DATETIME2 = '20140615', @p6 DATETIME2 = '20140620', @p7 INT = 10, @p8 INT = 20, @p9 INT; EXEC Sales.usp_InsertSpecialOffer_inmem @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 2; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO INSERT #x(d) VALUES('Named, improper types'); GO /* this uses named parameters, but incorrect data types */ DECLARE @p1 VARCHAR(255) = 'Product 1', @p2 DECIMAL(10,2) = 10, @p3 VARCHAR(255) = 'Volume Discount', @p4 VARCHAR(32) = 'Reseller', @p5 DATETIME = '20140615', @p6 CHAR(8) = '20140620', @p7 TINYINT = 10, @p8 DECIMAL(10,2) = 20, @p9 BIGINT; EXEC Sales.usp_InsertSpecialOffer_inmem @Description = @p1, @DiscountPct = @p2, @Type = @p3, @Category = @p4, @StartDate = @p5, @EndDate = @p6, @MinQty = '10', @MaxQty = @p8, @SpecialOfferID = @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 3; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO INSERT #x(d) VALUES('Not named, improper types'); GO /* this does not use named parameters, and uses incorrect data types */ DECLARE @p1 VARCHAR(255) = 'Product 1', @p2 DECIMAL(10,2) = 10, @p3 VARCHAR(255) = 'Volume Discount', @p4 VARCHAR(32) = 'Reseller', @p5 DATETIME = '20140615', @p6 CHAR(8) = '20140620', @p7 TINYINT = 10, @p8 DECIMAL(10,2) = 20, @p9 BIGINT; EXEC Sales.usp_InsertSpecialOffer_inmem @p1, @p2, @p3, @p4, @p5, @p6, '10', @p8, @p9 OUTPUT; GO 100000 UPDATE #x SET e = SYSDATETIME() WHERE i = 4; GO DELETE Sales.SpecialOffer_inmem WHERE Description = N'Product 1'; GO SELECT d, duration_ms = DATEDIFF(MILLISECOND, s, e) FROM #x; GO DROP TABLE #x; GO
J'ai exécuté chaque test 10 fois, et voici les durées moyennes, en millisecondes :
Procédure stockée traditionnelle | |
---|---|
Paramètres | Durée moyenne (millisecondes) |
Types nommés et appropriés | 72 132 |
Non nommé, types appropriés | 72 846 |
Types nommés et incorrects | 76 154 |
Non nommé, types incorrects | 76 902 |
Procédure stockée compilée nativement | |
Paramètres | Durée moyenne (millisecondes) |
Types nommés et appropriés | 63 202 |
Non nommé, types appropriés | 61 297 |
Types nommés et incorrects | 64 560 |
Non nommé, types incorrects | 64 288 |
Durée moyenne, en millisecondes, des différentes méthodes d'appel
Avec la procédure stockée traditionnelle, il est clair que l'utilisation des mauvais types de données a un impact substantiel sur les performances (environ une différence de 4 secondes), tandis que le fait de ne pas nommer les paramètres a un effet beaucoup moins dramatique (ajoutant environ 700 ms). J'ai toujours essayé de suivre les meilleures pratiques et d'utiliser les bons types de données ainsi que de nommer tous les paramètres, et ce petit test semble confirmer que cela peut être bénéfique.
Avec la procédure stockée compilée en mode natif, l'utilisation des mauvais types de données entraînait toujours une baisse des performances similaire à celle de la procédure stockée traditionnelle. Cette fois, cependant, nommer les paramètres n'a pas tellement aidé; en fait, cela a eu un impact négatif, ajoutant près de deux secondes à la durée totale. Pour être honnête, il s'agit d'un grand nombre d'appels en un temps assez court, mais si vous essayez de tirer le maximum de performances de pointe de cette fonctionnalité, chaque nanoseconde compte.
Découverte du problème
Comment savoir si vos procédures stockées compilées en mode natif sont appelées avec l'une de ces méthodes "lente" ? Il y a un XEvent pour ça ! L'événement s'appelle natively_compiled_proc_slow_parameter_passing
, et il ne semble pas être documenté dans la documentation en ligne pour le moment. Vous pouvez créer la session d'événements étendus suivante pour surveiller cet événement :
CREATE EVENT SESSION [XTP_Parameter_Events] ON SERVER ADD EVENT sqlserver.natively_compiled_proc_slow_parameter_passing ( ACTION(sqlserver.sql_text) ) ADD TARGET package0.event_file(SET filename=N'C:\temp\XTPParams.xel'); GO ALTER EVENT SESSION [XTP_Parameter_Events] ON SERVER STATE = START;
Une fois la session en cours d'exécution, vous pouvez essayer l'un des quatre appels ci-dessus individuellement, puis exécuter cette requête :
;WITH x([timestamp], db, [object_id], reason, batch) AS ( SELECT xe.d.value(N'(event/@timestamp)[1]',N'datetime2(0)'), DB_NAME(xe.d.value(N'(event/data[@name="database_id"]/value)[1]',N'int')), xe.d.value(N'(event/data[@name="object_id"]/value)[1]',N'int'), xe.d.value(N'(event/data[@name="reason"]/text)[1]',N'sysname'), xe.d.value(N'(event/action[@name="sql_text"]/value)[1]',N'nvarchar(max)') FROM sys.fn_xe_file_target_read_file(N'C:\temp\XTPParams*.xel',NULL,NULL,NULL) AS ft CROSS APPLY (SELECT CONVERT(XML, ft.event_data)) AS xe(d) ) SELECT [timestamp], db, [object_id], reason, batch FROM x;
En fonction de ce que vous avez exécuté, vous devriez voir des résultats similaires à celui-ci :
horodatage | db | object_id | raison | lot |
---|---|---|---|---|
2014-07-01 16:23:14 | AdventureWorks2012 | 2087678485 | named_parameters | DECLARE @p1 NVARCHAR(255) = N'Product 1', @p2 SMALLMONEY = 10, @p3 NVARCHAR(50) = N'Volume Discount', @p4 NVARCHAR(50) = N'Reseller', @p5 DATETIME2 = '20140615', @p6 DATETIME2 = '20140620', @p7 INT = 10, @p8 INT = 20, @p9 INT; EXEC Sales.usp_InsertSpecialOffer_inmem @Description = @p1, @DiscountPct = @p2, @Type = @p3, @Category = @p4, @StartDate = @p5, @EndDate = @p6, @MinQty = @p7, @MaxQty = @p8, @SpecialOfferID = @p9 OUTPUT; |
2014-07-01 16:23:22 | AdventureWorks2012 | 2087678485 | parameter_conversion | DECLARE @p1 VARCHAR(255) = 'Product 1', @p2 DECIMAL(10,2) = 10, @p3 VARCHAR(255) = 'Volume Discount', @p4 VARCHAR(32) = 'Reseller', @p5 DATETIME = '20140615', @p6 CHAR(8) = '20140620', @p7 TINYINT = 10, @p8 DECIMAL(10,2) = 20, @p9 BIGINT; EXEC Sales.usp_InsertSpecialOffer_inmem @p1, @p2, @p3, @p4, @p5, @p6, '10', @p8, @p9 OUTPUT; |
Exemple de résultats d'événements étendus
Espérons que le batch
est suffisante pour identifier le coupable, mais si vous avez de gros lots contenant plusieurs appels à des procédures compilées en mode natif et que vous devez rechercher les objets qui déclenchent spécifiquement ce problème, vous pouvez simplement les rechercher par object_id
dans leurs bases de données respectives.
Maintenant, je ne recommande pas d'exécuter les 400 000 appels dans le texte pendant que la session est active, ou d'activer cette session dans un environnement de production hautement simultané - si vous le faites souvent, cela peut entraîner des frais généraux importants. Vous feriez bien mieux de vérifier ce type d'activité dans votre environnement de développement ou de préproduction, tant que vous pouvez le soumettre à une charge de travail appropriée couvrant un cycle d'activité complet.
Conclusion
J'ai été définitivement surpris par le fait que la dénomination des paramètres - longtemps considérée comme une bonne pratique - est devenue une pire pratique avec des procédures stockées compilées en mode natif. Et Microsoft sait qu'il s'agit d'un problème potentiel suffisant pour créer un événement étendu conçu spécifiquement pour le suivre. Si vous utilisez l'OLTP en mémoire, c'est une chose que vous devez garder à l'esprit lorsque vous développez des procédures stockées de prise en charge. Je sais que je vais certainement devoir désentraîner ma mémoire musculaire en utilisant des paramètres nommés.