Au cours du mois dernier, j'ai rencontré de nombreux clients qui ont eu des problèmes de conversion implicite côté colonne associés à leurs charges de travail OLTP. À deux reprises, l'effet cumulé des conversions implicites côté colonne a été la cause sous-jacente du problème de performances global pour le serveur SQL en cours d'examen, et malheureusement, il n'y a pas de paramètre magique ou d'option de configuration que nous pouvons modifier pour améliorer la situation. quand c'est le cas. Bien que nous puissions proposer des suggestions pour corriger d'autres fruits moins susceptibles d'affecter les performances globales, l'effet des conversions implicites côté colonne est quelque chose qui nécessite soit une modification de la conception du schéma à corriger, soit une modification du code pour empêcher la colonne- la conversion latérale ne se produise pas complètement par rapport au schéma de base de données actuel.
Les conversions implicites sont le résultat de la comparaison par le moteur de base de données des valeurs de différents types de données lors de l'exécution de la requête. Une liste des conversions implicites possibles qui pourraient se produire à l'intérieur du moteur de base de données se trouve dans la rubrique de la documentation en ligne Conversion de type de données (moteur de base de données). Les conversions implicites se produisent toujours en fonction de la priorité des types de données pour les types de données qui sont comparés au cours de l'opération. L'ordre de priorité des types de données se trouve dans la rubrique de la documentation en ligne Priorité des types de données (Transact-SQL). J'ai récemment publié un blog sur les conversions implicites qui entraînent une analyse d'index et fourni des graphiques qui peuvent également être utilisés pour déterminer les conversions implicites les plus problématiques.
Configuration des tests
Pour démontrer la surcharge de performances associée aux conversions implicites côté colonne qui entraînent une analyse d'index, j'ai exécuté une série de tests différents sur la base de données AdventureWorks2012 à l'aide de la table Sales.SalesOrderDetail pour créer des tables de test et des ensembles de données. La conversion implicite côté colonne la plus courante que je vois en tant que consultant se produit lorsque le type de colonne est char ou varchar, et que le code d'application passe un paramètre qui est nchar ou nvarchar et filtre sur la colonne char ou varchar. Pour simuler ce type de scénario, j'ai créé une copie de la table SalesOrderDetail (nommée SalesOrderDetail_ASCII) et changé la colonne CarrierTrackingNumber de nvarchar en varchar. De plus, j'ai ajouté un index non clusterisé sur la colonne CarrierTrackingNumber à la table SalesOrderDetail d'origine, ainsi qu'à la nouvelle table SalesOrderDetail_ASCII.
USE [AdventureWorks2012] GO -- Add CarrierTrackingNumber index to original Sales.SalesOrderDetail table IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE [object_id] = OBJECT_ID(N'Sales.SalesOrderDetail') AND name=N'IX_SalesOrderDetail_CarrierTrackingNumber' ) BEGIN CREATE INDEX IX_SalesOrderDetail_CarrierTrackingNumber ON Sales.SalesOrderDetail (CarrierTrackingNumber); END GO IF OBJECT_ID('Sales.SalesOrderDetail_ASCII') IS NOT NULL BEGIN DROP TABLE Sales.SalesOrderDetail_ASCII; END GO CREATE TABLE Sales.SalesOrderDetail_ASCII ( SalesOrderID int NOT NULL, SalesOrderDetailID int NOT NULL IDENTITY (1, 1), CarrierTrackingNumber varchar(25) NULL, OrderQty smallint NOT NULL, ProductID int NOT NULL, SpecialOfferID int NOT NULL, UnitPrice money NOT NULL, UnitPriceDiscount money NOT NULL, LineTotal AS (isnull(([UnitPrice]*((1.0)-[UnitPriceDiscount]))*[OrderQty],(0.0))), rowguid uniqueidentifier NOT NULL ROWGUIDCOL, ModifiedDate datetime NOT NULL ); GO SET IDENTITY_INSERT Sales.SalesOrderDetail_ASCII ON; GO INSERT INTO Sales.SalesOrderDetail_ASCII ( SalesOrderID, SalesOrderDetailID, CarrierTrackingNumber, OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate ) SELECT SalesOrderID, SalesOrderDetailID, CONVERT(varchar(25), CarrierTrackingNumber), OrderQty, ProductID, SpecialOfferID, UnitPrice, UnitPriceDiscount, rowguid, ModifiedDate FROM Sales.SalesOrderDetail WITH (HOLDLOCK TABLOCKX); GO SET IDENTITY_INSERT Sales.SalesOrderDetail_ASCII OFF; GO ALTER TABLE Sales.SalesOrderDetail_ASCII ADD CONSTRAINT PK_SalesOrderDetail_ASCII_SalesOrderID_SalesOrderDetailID PRIMARY KEY CLUSTERED (SalesOrderID, SalesOrderDetailID); CREATE UNIQUE NONCLUSTERED INDEX AK_SalesOrderDetail_ASCII_rowguid ON Sales.SalesOrderDetail_ASCII (rowguid); CREATE NONCLUSTERED INDEX IX_SalesOrderDetail_ASCII_ProductID ON Sales.SalesOrderDetail_ASCII (ProductID); CREATE INDEX IX_SalesOrderDetail_ASCII_CarrierTrackingNumber ON Sales.SalesOrderDetail_ASCII (CarrierTrackingNumber); GO
La nouvelle table SalesOrderDetail_ASCII a 121 317 lignes et une taille de 17,5 Mo, et sera utilisée pour évaluer la surcharge d'une petite table. J'ai également créé une table dix fois plus grande, en utilisant une version modifiée du script Enlarging the AdventureWorks Sample Databases de mon blog, qui contient 1 334 487 lignes et une taille de 190 Mo. Le serveur de test pour cela est le même VM 4 vCPU avec 4 Go de RAM, exécutant Windows Server 2008 R2 et SQL Server 2012, avec Service Pack 1 et mise à jour cumulative 3, que j'ai utilisé dans les articles précédents, de sorte que les tables tiendront entièrement en mémoire , éliminant ainsi la surcharge d'E/S disque affectant les tests en cours d'exécution.
La charge de travail de test a été générée à l'aide d'une série de scripts PowerShell qui sélectionnent la liste des CarrierTrackingNumbers dans la table SalesOrderDetail en créant une ArrayList, puis sélectionnent au hasard un CarrierTrackingNumber dans la ArrayList pour interroger la table SalesOrderDetail_ASCII à l'aide d'un paramètre varchar, puis d'un paramètre nvarchar, et puis pour interroger la table SalesOrderDetail à l'aide d'un paramètre nvarchar afin de fournir une comparaison pour savoir où la colonne et le paramètre sont tous deux nvarchar. Chacun des tests individuels exécute l'instruction 10 000 fois pour permettre de mesurer la surcharge de performances sur une charge de travail soutenue.
#No Implicit Conversions $loop = 10000; Write-Host "Small table no conversion start time:" [DateTime]::Now $query = @"SELECT * FROM Sales.SalesOrderDetail_ASCII " "WHERE CarrierTrackingNumber = @CTNumber;"; while($loop -gt 0) { $Value = Get-Random -InputObject $Results; $SqlCmd = $SqlConn.CreateCommand(); $SqlCmd.CommandText = $query; $SqlCmd.CommandType = [System.Data.CommandType]::Text; $SqlParameter = $SqlCmd.Parameters.AddWithValue("@CTNumber", $Value); $SqlParameter.SqlDbType = [System.Data.SqlDbType]::VarChar; $SqlParameter.Size = 30; $SqlCmd.ExecuteNonQuery() | Out-Null; $loop--; } Write-Host "Small table no conversion end time:" [DateTime]::Now Sleep -Seconds 10; #Small table implicit conversions $loop = 10000; Write-Host "Small table implicit conversions start time:" [DateTime]::Now $query = @"SELECT * FROM Sales.SalesOrderDetail_ASCII " "WHERE CarrierTrackingNumber = @CTNumber;"; while($loop -gt 0) { $Value = Get-Random -InputObject $Results; $SqlCmd = $SqlConn.CreateCommand(); $SqlCmd.CommandText = $query; $SqlCmd.CommandType = [System.Data.CommandType]::Text; $SqlParameter = $SqlCmd.Parameters.AddWithValue("@CTNumber", $Value); $SqlParameter.SqlDbType = [System.Data.SqlDbType]::NVarChar; $SqlParameter.Size = 30; $SqlCmd.ExecuteNonQuery() | Out-Null; $loop--; } Write-Host "Small table implicit conversions end time:" [DateTime]::Now Sleep -Seconds 10; #Small table unicode no implicit conversions $loop = 10000; Write-Host "Small table unicode no implicit conversion start time:" [DateTime]::Now $query = @"SELECT * FROM Sales.SalesOrderDetail " "WHERE CarrierTrackingNumber = @CTNumber;" while($loop -gt 0) { $Value = Get-Random -InputObject $Results; $SqlCmd = $SqlConn.CreateCommand(); $SqlCmd.CommandText = $query; $SqlCmd.CommandType = [System.Data.CommandType]::Text; $SqlParameter = $SqlCmd.Parameters.AddWithValue("@CTNumber", $Value); $SqlParameter.SqlDbType = [System.Data.SqlDbType]::NVarChar; $SqlParameter.Size = 30; $SqlCmd.ExecuteNonQuery() | Out-Null; $loop--; } Write-Host "Small table unicode no implicit conversion end time:" [DateTime]::Now
Un deuxième ensemble de tests a été exécuté sur les tables SalesOrderDetailEnlarged_ASCII et SalesOrderDetailEnlarged en utilisant le même paramétrage que le premier ensemble de tests pour montrer la différence de surcharge lorsque la taille des données stockées dans la table augmente avec le temps. Un dernier ensemble de tests a également été exécuté sur la table SalesOrderDetail en utilisant la colonne ProductID comme colonne de filtre avec les types de paramètres int, bigint, puis smallint pour fournir une comparaison de la surcharge des conversions implicites qui ne se traduisent pas par une analyse d'index. pour comparaison.
Remarque :Tous les scripts sont joints à cet article pour permettre la reproduction des tests de conversion implicites pour une évaluation et une comparaison plus approfondies.
Résultats des tests
Au cours de chacune des exécutions de test, l'Analyseur de performances a été configuré pour exécuter un ensemble de collecteurs de données qui comprenait les compteurs Processor\% Processor Time et SQL Server:SQLStatisitics\Batch Requests/sec pour suivre la surcharge de performances pour chacun des tests. De plus, les événements étendus ont été configurés pour suivre l'événement rpc_completed afin de permettre le suivi de la durée moyenne, du temps cpu et des lectures logiques pour chacun des tests.
Résultats de la petite table CarrierTrackingNumber
Figure 1 – Graphique des compteurs du moniteur de performances
ID de test | Type de données de colonne | Type de données de paramètre | % moyen du temps processeur | Nombre moyen de requêtes par lot/s | Durée h:mm:ss |
---|---|---|---|---|---|
1 | Varchar | Varchar | 2.5 | 192.3 | 0:00:51 |
2 | Varchar | Nvarchar | 19.4 | 46.7 | 0:03:33 |
3 | Nvarchar | Nvarchar | 2.6 | 192.3 | 0:00:51 |
Tableau 2 – Moyennes des données du moniteur de performances
D'après les résultats, nous pouvons voir que la conversion implicite côté colonne de varchar en nvarchar et l'analyse d'index qui en résulte ont un impact significatif sur les performances de la charge de travail. Le pourcentage moyen de temps processeur pour le test de conversion implicite côté colonne (TestID =2) est presque dix fois supérieur à celui des autres tests où la conversion implicite côté colonne, entraînant une analyse d'index, ne s'est pas produite. De plus, la moyenne des requêtes par lot/s pour le test de conversion implicite côté colonne était légèrement inférieure à 25 % des autres tests. La durée des tests où les conversions implicites ne se sont pas produites a pris 51 secondes, même si les données ont été stockées en tant que nvarchar dans le test numéro 3 en utilisant un type de données nvarchar, nécessitant deux fois plus d'espace de stockage. Ceci est normal car la table est encore plus petite que le pool de mémoire tampon.
ID de test | Temps cpu_moyen (µs) | Durée moyenne (µs) | Avg logical_reads |
---|---|---|---|
1 | 40.7 | 154.9 | 51.6 |
2 | 15 640,8 | 15 760,0 | 385.6 |
3 | 45.3 | 169.7 | 52.7 |
Tableau 3 – Moyennes des événements étendus
Les données collectées par l'événement rpc_completed dans les événements étendus montrent que la moyenne cpu_time, la durée et les lectures logiques associées aux requêtes qui n'effectuent pas de conversion implicite côté colonne sont à peu près équivalentes, où la conversion implicite côté colonne engage une CPU importante surcharge, ainsi qu'une durée moyenne plus longue avec beaucoup plus de lectures logiques.
Résultats du tableau agrandi CarrierTrackingNumber
Figure 4 – Graphique des compteurs du moniteur de performances
ID de test | Type de données de colonne | Type de données de paramètre | % moyen du temps processeur | Nombre moyen de requêtes par lot/s | Durée h:mm:ss |
---|---|---|---|---|---|
1 | Varchar | Varchar | 7.2 | 164.0 | 0:01:00 |
2 | Varchar | Nvarchar | 83.8 | 15.4 | 0:10:49 |
3 | Nvarchar | Nvarchar | 7.0 | 166.7 | 0:01:00 |
Tableau 5 – Moyennes des données du moniteur de performances
À mesure que la taille des données augmente, la surcharge de performances de la conversion implicite côté colonne augmente également. Le pourcentage moyen de temps processeur pour le test de conversion implicite côté colonne (TestID =2) est, encore une fois, près de dix fois supérieur à celui des autres tests où la conversion implicite côté colonne résultant en un parcours d'index ne s'est pas produite. De plus, la moyenne des requêtes par lot/s pour le test de conversion implicite côté colonne était légèrement inférieure à 10 % des autres tests. La durée des tests où les conversions implicites ne se produisaient pas prenait toutes deux une minute, alors que le test de conversion implicite côté colonne nécessitait près de onze minutes pour s'exécuter.
ID de test | Temps cpu_moyen (µs) | Durée moyenne (µs) | Avg logical_reads |
---|---|---|---|
1 | 728.5 | 1 036,5 | 569.6 |
2 | 214 174,6 | 59 519,1 | 4 358,2 |
3 | 821.5 | 1 032,4 | 553.5 |
Tableau 6 – Moyennes des événements étendus
Les résultats des événements étendus commencent vraiment à montrer la surcharge de performances causée par les conversions implicites côté colonne pour la charge de travail. Le cpu_time moyen par exécution passe à plus de 214 ms et est supérieur à 200 fois le cpu_time pour les instructions qui n'ont pas les conversions implicites côté colonne. La durée est également près de 60 fois celle des instructions qui n'ont pas de conversions implicites côté colonne.
Résumé
À mesure que la taille des données continue d'augmenter, la surcharge associée aux conversions implicites côté colonne qui entraînent une analyse d'index pour la charge de travail continuera également d'augmenter, et la chose importante à retenir est qu'à un moment donné, aucune quantité de matériel sera en mesure de faire face à la surcharge de performance. Les conversions implicites sont faciles à empêcher lorsqu'une bonne conception de schéma de base de données existe et que les développeurs suivent de bonnes techniques de codage d'application. Dans les situations où les pratiques de codage d'application entraînent un paramétrage qui exploite le paramétrage nvarchar, il est préférable de faire correspondre la conception du schéma de base de données au paramétrage de la requête plutôt que d'utiliser des colonnes varchar dans la conception de la base de données et d'encourir la surcharge de performances de la conversion implicite côté colonne.
Téléchargez les scripts de démonstration :Implicit_Conversion_Tests.zip (5 Ko)