Nell'ultimo mese ho interagito con numerosi clienti che hanno avuto problemi di conversione implicita lato colonna associati ai loro carichi di lavoro OLTP. In due occasioni, l'effetto accumulato delle conversioni implicite sul lato della colonna è stata la causa alla base del problema di prestazioni complessive per SQL Server in fase di revisione e sfortunatamente non esiste un'impostazione magica o un'opzione di configurazione che possiamo modificare per migliorare la situazione quando questo è il caso. Sebbene possiamo offrire suggerimenti per correggere altri frutti di basso livello che potrebbero influire sulle prestazioni complessive, l'effetto delle conversioni implicite sul lato della colonna è qualcosa che richiede una modifica alla progettazione dello schema da correggere o una modifica del codice per impedire la colonna- conversione laterale dal verificarsi completamente rispetto allo schema del database corrente.
Le conversioni implicite sono il risultato del motore di database che confronta i valori di diversi tipi di dati durante l'esecuzione della query. Un elenco delle possibili conversioni implicite che potrebbero verificarsi all'interno del motore di database è disponibile nell'argomento Conversione del tipo di dati (Motore di database) della documentazione in linea. Le conversioni implicite si verificano sempre in base alla precedenza del tipo di dati per i tipi di dati che vengono confrontati durante l'operazione. L'ordine di precedenza del tipo di dati è reperibile nell'argomento della documentazione in linea Precedenza del tipo di dati (Transact-SQL). Di recente ho scritto sul blog sulle conversioni implicite che si traducono in una scansione dell'indice e ho fornito grafici che possono essere utilizzati per determinare anche le conversioni implicite più problematiche.
Impostazione dei test
Per dimostrare l'overhead delle prestazioni associato alle conversioni implicite lato colonna che determinano un'analisi dell'indice, ho eseguito una serie di test diversi sul database AdventureWorks2012 utilizzando la tabella Sales.SalesOrderDetail per creare tabelle di test e set di dati. La conversione implicita lato colonna più comune che vedo come consulente si verifica quando il tipo di colonna è char o varchar e il codice dell'applicazione passa un parametro che è nchar o nvarchar e filtra sulla colonna char o varchar. Per simulare questo tipo di scenario, ho creato una copia della tabella SalesOrderDetail (denominata SalesOrderDetail_ASCII) e modificato la colonna CarrierTrackingNumber da nvarchar a varchar. Inoltre, ho aggiunto un indice non cluster nella colonna CarrierTrackingNumber alla tabella SalesOrderDetail originale, nonché alla nuova tabella 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 nuova tabella SalesOrderDetail_ASCII ha 121.317 righe e ha una dimensione di 17,5 MB e verrà utilizzata per valutare l'overhead di una piccola tabella. Ho anche creato una tabella dieci volte più grande, utilizzando una versione modificata dello script Enlarging the AdventureWorks Sample Databases del mio blog, che contiene 1.334.487 righe e ha una dimensione di 190 MB. Il server di prova per questo è lo stesso 4 vCPU VM con 4 GB di RAM, che esegue Windows Server 2008 R2 e SQL Server 2012, con Service Pack 1 e aggiornamento cumulativo 3, che ho usato negli articoli precedenti, quindi le tabelle si adatteranno completamente alla memoria , eliminando l'overhead di I/O del disco dall'influenzare i test in esecuzione.
Il carico di lavoro di prova è stato generato utilizzando una serie di script di PowerShell che selezionano l'elenco di CarrierTrackingNumbers dalla tabella SalesOrderDetail creando un ArrayList, quindi selezionano in modo casuale un CarrierTrackingNumber da ArrayList per interrogare la tabella SalesOrderDetail_ASCII utilizzando un parametro varchar e quindi un parametro nvarchar e quindi per interrogare la tabella SalesOrderDetail utilizzando un parametro nvarchar per fornire un confronto per dove la colonna e il parametro sono entrambi nvarchar. Ciascuno dei singoli test esegue l'istruzione 10.000 volte per consentire di misurare l'overhead delle prestazioni su un carico di lavoro sostenuto.
#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 secondo set di test è stato eseguito sulle tabelle SalesOrderDetailEnlarged_ASCII e SalesOrderDetailEnlarged utilizzando la stessa parametrizzazione del primo set di test per mostrare la differenza di sovraccarico quando la dimensione dei dati archiviati nella tabella aumenta nel tempo. Un'ultima serie di test è stata eseguita anche sulla tabella SalesOrderDetail utilizzando la colonna ProductID come colonna di filtro con i tipi di parametro int, bigint e quindi smallint per fornire un confronto dell'overhead delle conversioni implicite che non generano un'analisi dell'indice per confronto.
Nota:tutti gli script sono allegati a questo articolo per consentire la riproduzione dei test di conversione implicita per ulteriori valutazioni e confronti.
Risultati del test
Durante ciascuna delle esecuzioni di test, Performance Monitor è stato configurato per eseguire un set di agenti di raccolta dati che includeva i contatori Processor\% Processor Time e SQL Server:SQLStatisitics\Batch Requests/sec per tenere traccia del sovraccarico delle prestazioni per ciascuno dei test. Inoltre, gli eventi estesi sono stati configurati per tenere traccia dell'evento rpc_completed per consentire il monitoraggio della durata media, del tempo di CPU e delle letture logiche per ciascuno dei test.
Risultati di Small Table CarrierTrackingNumber
Figura 1 – Grafico dei contatori di Performance Monitor
TestID | Tipo di dati della colonna | Tipo di dati parametro | % media del tempo del processore | Richieste batch medie/sec | Durata 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 |
Tabella 2 – Medie dei dati di Performance Monitor
Dai risultati, possiamo vedere che la conversione implicita lato colonna da varchar a nvarchar e la scansione dell'indice risultante hanno un impatto significativo sulle prestazioni del carico di lavoro. La % media del tempo di elaborazione per il test di conversione implicita lato colonna (TestID =2) è quasi dieci volte superiore a quella degli altri test in cui non si è verificata la conversione implicita lato colonna, risultante in una scansione dell'indice. Inoltre, la media delle richieste batch/sec per il test di conversione implicita lato colonna era appena inferiore al 25% degli altri test. La durata dei test in cui non si verificavano conversioni implicite ha richiesto entrambi 51 secondi, anche se i dati sono stati archiviati come nvarchar nel test numero 3 utilizzando un tipo di dati nvarchar, richiedendo il doppio dello spazio di archiviazione. Ciò è previsto perché la tabella è ancora più piccola del pool di buffer.
TestID | Tempo_cpu medio (µs) | Durata media (µs) | Lettura_logica media |
---|---|---|---|
1 | 40.7 | 154,9 | 51.6 |
2 | 15.640,8 | 15.760,0 | 385.6 |
3 | 45.3 | 169.7 | 52.7 |
Tabella 3 – Medie degli eventi estesi
I dati raccolti dall'evento rpc_completed in Eventi estesi mostrano che la media cpu_time, la durata e le letture logiche associate alle query che non eseguono una conversione implicita lato colonna sono più o meno equivalenti, dove la conversione implicita lato colonna comporta una CPU significativa sovraccarico, nonché una durata media più lunga con letture significativamente più logiche.
Risultati di CarrierTrackingNumber tabella ingrandita
Figura 4 – Grafico dei contatori di Performance Monitor
TestID | Tipo di dati della colonna | Tipo di dati parametro | % media del tempo del processore | Richieste batch medie/sec | Durata 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 |
Tabella 5 – Medie dei dati di Performance Monitor
Con l'aumento delle dimensioni dei dati, aumenta anche il sovraccarico delle prestazioni della conversione implicita lato colonna. La % media del tempo di elaborazione per il test di conversione implicita lato colonna (TestID =2) è, ancora una volta, quasi dieci volte maggiore rispetto agli altri test in cui non si è verificata la conversione implicita lato colonna che ha portato a una scansione dell'indice. Inoltre, la media delle richieste batch/sec per il test di conversione implicita lato colonna era appena inferiore al 10% degli altri test. La durata dei test in cui non si verificavano conversioni implicite ha richiesto entrambi un minuto, mentre il test di conversione implicita lato colonna ha richiesto quasi undici minuti per essere eseguito.
TestID | Tempo_cpu medio (µs) | Durata media (µs) | Lettura_logica media |
---|---|---|---|
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 |
Tabella 6 – Medie degli eventi estesi
I risultati degli eventi estesi iniziano effettivamente a mostrare l'overhead delle prestazioni causato dalle conversioni implicite lato colonna per il carico di lavoro. Il tempo medio di cpu per esecuzione salta a oltre 214 ms ed è oltre 200 volte il tempo di cpu per le istruzioni che non hanno le conversioni implicite lato colonna. La durata è anche quasi 60 volte quella delle istruzioni che non hanno le conversioni implicite lato colonna.
Riepilogo
Man mano che le dimensioni dei dati continuano ad aumentare, anche l'overhead associato alle conversioni implicite lato colonna che si traducono in una scansione dell'indice per il carico di lavoro continuerà a crescere e la cosa importante da ricordare è che a un certo punto, nessuna quantità di hardware sarà in grado di far fronte al sovraccarico delle prestazioni. Le conversioni implicite sono una cosa facile da prevenire quando esiste una buona progettazione dello schema del database e gli sviluppatori seguono buone tecniche di codifica delle applicazioni. Nelle situazioni in cui le pratiche di codifica dell'applicazione determinano una parametrizzazione che sfrutta la parametrizzazione di nvarchar, è meglio abbinare la progettazione dello schema del database alla parametrizzazione della query piuttosto che utilizzare colonne varchar nella progettazione del database e incorrere nell'overhead delle prestazioni dalla conversione implicita lato colonna.
Scarica gli script demo:Implicit_Conversion_Tests.zip (5 KB)