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

Comment ne pas appeler les procédures stockées Hekaton compilées nativement

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.