In de afgelopen maand heb ik contact gehad met tal van klanten die impliciete conversieproblemen aan de kolomzijde hadden in verband met hun OLTP-workloads. Bij twee gelegenheden was het geaccumuleerde effect van de impliciete conversies aan de kolomzijde de onderliggende oorzaak van het algehele prestatieprobleem voor de SQL Server die werd beoordeeld, en helaas is er geen magische instelling of configuratie-optie die we kunnen aanpassen om de situatie te verbeteren wanneer dit het geval is. Hoewel we suggesties kunnen doen om ander, lager hangend fruit te repareren dat de prestaties in het algemeen zou kunnen beïnvloeden, is het effect van de impliciete conversies aan de kolomzijde iets dat ofwel een wijziging in het schemaontwerp vereist om te corrigeren, of een codewijziging om te voorkomen dat de kolom- side-conversie niet volledig tegen het huidige databaseschema.
Impliciete conversies zijn het resultaat van de database-engine die waarden van verschillende gegevenstypen vergelijkt tijdens het uitvoeren van query's. Een lijst met mogelijke impliciete conversies die binnen de database-engine kunnen plaatsvinden, is te vinden in het Books Online-onderwerp Data Type Conversion (Database Engine). Impliciete conversies vinden altijd plaats op basis van de prioriteit van het gegevenstype voor de gegevenstypen die tijdens de bewerking worden vergeleken. De prioriteitsvolgorde van het gegevenstype is te vinden in het Books Online-onderwerp Gegevenstypevoorrang (Transact-SQL). Ik heb onlangs geblogd over de impliciete conversies die resulteren in een indexscan, en heb grafieken aangeleverd die ook kunnen worden gebruikt om de meest problematische impliciete conversies te bepalen.
De tests instellen
Om de prestatieoverhead te demonstreren die gepaard gaat met impliciete conversies aan de kolomzijde die resulteren in een indexscan, heb ik een reeks verschillende tests uitgevoerd op de AdventureWorks2012-database met behulp van de tabel Sales.SalesOrderDetail om testtabellen en datasets te bouwen. De meest voorkomende impliciete conversie aan de kolomzijde die ik als consultant zie, vindt plaats wanneer het kolomtype char of varchar is, en de applicatiecode een parameter doorgeeft die nchar of nvarchar is en filters op de char- of varchar-kolom. Om dit type scenario te simuleren, heb ik een kopie gemaakt van de SalesOrderDetail-tabel (genaamd SalesOrderDetail_ASCII) en de kolom CarrierTrackingNumber gewijzigd van nvarchar in varchar. Daarnaast heb ik een niet-geclusterde index op de CarrierTrackingNumber-kolom toegevoegd aan de originele SalesOrderDetail-tabel, evenals de nieuwe SalesOrderDetail_ASCII-tabel.
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
De nieuwe tabel SalesOrderDetail_ASCII heeft 121.317 rijen en is 17,5 MB groot en zal worden gebruikt om de overhead van een kleine tabel te evalueren. Ik heb ook een tabel gemaakt die tien keer groter is, met behulp van een aangepaste versie van het script Enlargeging the AdventureWorks Sample Databases van mijn blog, dat 1.334.487 rijen bevat en 190 MB groot is. De testserver hiervoor is dezelfde 4 vCPU-VM met 4 GB RAM, met Windows Server 2008 R2 en SQL Server 2012, met Service Pack 1 en Cumulatieve Update 3, die ik in eerdere artikelen heb gebruikt, dus de tabellen passen volledig in het geheugen , waardoor schijf-I/O-overhead geen invloed heeft op de tests die worden uitgevoerd.
De testwerkbelasting is gegenereerd met behulp van een reeks PowerShell-scripts die de lijst met CarrierTrackingNumbers selecteren uit de SalesOrderDetail-tabel die een ArrayList bouwt, en vervolgens willekeurig een CarrierTrackingNumber selecteren uit de ArrayList om de SalesOrderDetail_ASCII-tabel te doorzoeken met behulp van een varchar-parameter en vervolgens een nvarchar-parameter, en om vervolgens de tabel SalesOrderDetail te doorzoeken met behulp van een nvarchar-parameter om een vergelijking te maken voor waar de kolom en parameter beide nvarchar zijn. Elk van de individuele tests voert de instructie 10.000 keer uit om de prestatieoverhead over een aanhoudende werkbelasting te meten.
#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
Er is een tweede reeks tests uitgevoerd op de tabellen SalesOrderDetailEnlarged_ASCII en SalesOrderDetailEnlarged met dezelfde parametrisering als de eerste reeks tests om het overheadverschil aan te tonen naarmate de gegevens die in de tabel zijn opgeslagen in de loop van de tijd toenemen. Er is ook een laatste reeks tests uitgevoerd op de tabel SalesOrderDetail met behulp van de kolom ProductID als filterkolom met parametertypen int, bigint en vervolgens smallint om een vergelijking te maken van de overhead van impliciete conversies die niet resulteren in een indexscan ter vergelijking.
Opmerking:alle scripts zijn bij dit artikel gevoegd om reproductie van de impliciete conversietests voor verdere evaluatie en vergelijking mogelijk te maken.
Testresultaten
Tijdens elk van de testuitvoeringen is Performance Monitor geconfigureerd om een gegevensverzamelaarset uit te voeren die de Processor\% Processor Time en SQL Server:SQLStatisitics\Batch Requests/s-tellers bevat om de prestatieoverhead voor elk van de tests bij te houden. Bovendien is Extended Events geconfigureerd om de rpc_completed-gebeurtenis te volgen, zodat de gemiddelde duur, cpu_time en logische uitlezingen voor elk van de tests kunnen worden gevolgd.
Small Table CarrierTrackingNumber Resultaten
Figuur 1 – Prestatiemeterschema met tellers
Test-ID | Kolomgegevenstype | Parametergegevenstype | Gem % processortijd | Gem batchverzoeken/sec | Duur u: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 |
Tabel 2 – Gegevensgemiddelden Prestatiemeter
Uit de resultaten kunnen we zien dat de impliciete conversie aan de kolomzijde van varchar naar nvarchar en de resulterende indexscan een aanzienlijke invloed heeft op de prestaties van de werkbelasting. Het gemiddelde % processortijd voor de impliciete conversietest aan de kolomzijde (TestID =2) is bijna tien keer zoveel als de andere tests waarbij de impliciete conversie aan de kolomzijde, resulterend in een indexscan, niet plaatsvond. Bovendien was de gemiddelde batchverzoeken/sec voor de impliciete conversietest aan de kolomzijde iets minder dan 25% van de andere tests. De duur van de tests waarbij geen impliciete conversies plaatsvonden, duurden beide 51 seconden, ook al waren de gegevens opgeslagen als nvarchar in test nummer 3 met behulp van een nvarchar-gegevenstype, waardoor twee keer zoveel opslagruimte nodig was. Dit wordt verwacht omdat de tabel nog steeds kleiner is dan de bufferpool.
Test-ID | Gem cpu_time (µs) | Gemiddelde duur (µs) | Gem logische_lezen |
---|---|---|---|
1 | 40,7 | 154,9 | 51.6 |
2 | 15.640,8 | 15.760.0 | 385.6 |
3 | 45,3 | 169,7 | 52,7 |
Tabel 3 – Gemiddelden voor uitgebreide gebeurtenissen
De gegevens die zijn verzameld door de gebeurtenis rpc_completed in Extended Events, laten zien dat de gemiddelde cpu_time, duur en logische leesbewerkingen die zijn gekoppeld aan de query's die geen impliciete conversie aan de kolomzijde uitvoeren, ongeveer equivalent zijn, terwijl de impliciete conversie aan de kolomzijde een aanzienlijke CPU met zich meebrengt overhead, evenals een langere gemiddelde duur met aanzienlijk meer logische uitlezingen.
Vergrote tabel CarrierTrackingNumber-resultaten
Figuur 4 – Prestatiemeterschema met tellers
Test-ID | Kolomgegevenstype | Parametergegevenstype | Gem % processortijd | Gem batchverzoeken/sec | Duur u: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 |
Tabel 5 – Gegevensgemiddelden Prestatiemeter
Naarmate de gegevens groter worden, neemt ook de prestatieoverhead van de impliciete conversie aan de kolomzijde toe. Het gemiddelde % processortijd voor de impliciete conversietest aan de kolomzijde (TestID =2) is opnieuw bijna tien keer zoveel als de andere tests waarbij de impliciete conversie aan de kolomzijde die resulteerde in een indexscan, niet plaatsvond. Bovendien was de gemiddelde batchverzoeken/sec voor de impliciete conversietest aan de kolomzijde iets minder dan 10% van de andere tests. De duur van de tests waarbij geen impliciete conversies plaatsvonden, duurden beide één minuut, terwijl de impliciete conversietest aan de kolomzijde bijna elf minuten nodig had om uit te voeren.
Test-ID | Gem cpu_time (µs) | Gemiddelde duur (µs) | Gem logische_lezen |
---|---|---|---|
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 |
Tabel 6 – Gemiddelden voor uitgebreide gebeurtenissen
De resultaten van Extended Events beginnen echt de prestatieoverhead te tonen die wordt veroorzaakt door de impliciete conversies aan de kolomzijde voor de werkbelasting. De gemiddelde cpu_time per uitvoering springt naar meer dan 214 ms en is meer dan 200 keer de cpu_time voor de instructies die geen impliciete conversies aan de kolomzijde hebben. De duur is ook bijna 60 keer die van de instructies die geen impliciete conversies aan de kolomzijde hebben.
Samenvatting
Naarmate de omvang van de gegevens blijft toenemen, zal de overhead die gepaard gaat met impliciete conversies aan de kolomzijde die resulteren in een indexscan voor de werkbelasting, ook blijven groeien, en het belangrijkste om te onthouden is dat op een gegeven moment geen enkele hoeveelheid hardware meer nodig is. kunnen omgaan met de prestatieoverhead. Impliciete conversies zijn gemakkelijk te voorkomen als er een goed databaseschema-ontwerp bestaat en ontwikkelaars goede coderingstechnieken voor applicaties volgen. In situaties waarin de coderingspraktijken van de toepassing resulteren in parametrering die gebruikmaakt van nvarchar-parametrering, is het beter om het databaseschema-ontwerp af te stemmen op de queryparametrisering dan om varchar-kolommen in het databaseontwerp te gebruiken en de prestatieoverhead te veroorzaken van de impliciete conversie aan de kolomzijde.
Download de demoscripts:Implicit_Conversion_Tests.zip (5 KB)