През последния месец се ангажирах с много клиенти, които са имали проблеми с имплицитното преобразуване от страна на колоните, свързани с техните OLTP работни натоварвания. В два пъти натрупаният ефект от имплицитните преобразувания от страна на колоните беше основната причина за цялостния проблем с производителността на преглеждания SQL Server и за съжаление няма магическа настройка или опция за конфигурация, която можем да настроим, за да подобрим ситуацията когато това е така. Въпреки че можем да предложим предложения за коригиране на други, по-ниско висящи плодове, които може да повлияят на производителността като цяло, ефектът от имплицитните преобразувания от страна на колоната е нещо, което изисква или промяна в дизайна на схемата, за да се коригира, или промяна на кода, за да се предотврати колоната- странично преобразуване от възникване спрямо текущата схема на базата данни напълно.
Неявните преобразувания са резултат от машината на базата данни, сравняваща стойности на различни типове данни по време на изпълнение на заявка. Списък с възможните неявни преобразувания, които биха могли да възникнат вътре в двигателя на базата данни, може да бъде намерен в темата Books Online Преобразуване на тип данни (Database Engine). Неявните преобразувания винаги се извършват въз основа на приоритета на типа данни за типовете данни, които се сравняват по време на операцията. Редът на приоритета на типове данни може да бъде намерен в темата на Books Online Приоритет на типа данни (Transact-SQL). Наскоро писах в блог за имплицитните конверсии, които водят до сканиране на индекс, и предоставих диаграми, които могат да се използват и за определяне на най-проблемните неявни преобразувания.
Настройване на тестовете
За да демонстрирам излишните разходи за производителност, свързани с имплицитни преобразувания от страна на колона, които водят до сканиране на индекс, проведох серия от различни тестове срещу базата данни AdventureWorks2012, използвайки таблицата Sales.SalesOrderDetail за създаване на тестови таблици и набори от данни. Най-често срещаното имплицитно преобразуване от страна на колоната, което виждам като консултант, се случва, когато типът на колоната е char или varchar, а кодът на приложението предава параметър, който е nchar или nvarchar, и филтрира в колоната char или varchar. За да симулирам този тип сценарий, създадох копие на таблицата SalesOrderDetail (наречена SalesOrderDetail_ASCII) и промених колоната CarrierTrackingNumber от nvarchar на varchar. Освен това добавих неклъстериран индекс в колоната CarrierTrackingNumber към оригиналната таблица SalesOrderDetail, както и новата таблица 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
Новата таблица SalesOrderDetail_ASCII има 121 317 реда и е с размер 17,5 MB и ще се използва за оценка на режийните разходи на малка таблица. Също така създадох таблица, която е десет пъти по-голяма, използвайки модифицирана версия на скрипта Enlarging the AdventureWorks Sample Databases от моя блог, която съдържа 1 334 487 реда и е с размер 190 MB. Тестовият сървър за това е същият 4 vCPU VM с 4 GB RAM, работещ с Windows Server 2008 R2 и SQL Server 2012, със Service Pack 1 и Cumulative Update 3, който използвах в предишни статии, така че таблиците ще се поберат изцяло в паметта , елиминиране на дисковия вход/изход от влияние върху провежданите тестове.
Тестовото работно натоварване е генерирано с помощта на серия от скриптове на PowerShell, които избират списъка с CarrierTrackingNumbers от таблицата SalesOrderDetail, изграждайки ArrayList, и след това произволно избират CarrierTrackingNumber от ArrayList, за да запитате таблицата SalesOrderDetail_ASCII с помощта на параметър varchar и параметър и след това параметър. след това да направите заявка за таблицата SalesOrderDetail, като използвате параметър nvarchar, за да предоставите сравнение за това къде колоната и параметърът са nvarchar. Всеки от отделните тестове изпълнява изявлението 10 000 пъти, за да позволи измерване на режийните разходи за производителност при продължително работно натоварване.
#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
Втори набор от тестове беше извършен срещу таблиците SalesOrderDetailEnlarged_ASCII и SalesOrderDetailEnlarged, използвайки същата параметризация като първия набор от тестове, за да се покаже разликата между допълнителните разходи, тъй като размерът на данните, съхранявани в таблицата, се увеличава с времето. Окончателен набор от тестове също беше извършен срещу таблицата SalesOrderDetail, използвайки колоната ProductID като колона за филтриране с типове параметри int, bigint и след това smallint, за да се осигури сравнение на режийните разходи за неявни преобразувания, които не водят до сканиране на индекс за сравнение.
Забележка:Всички скриптове са приложени към тази статия, за да позволят възпроизвеждане на неявните тестове за преобразуване за по-нататъшна оценка и сравнение.
Резултати от теста
По време на всяко от тестовите изпълнения, Performance Monitor беше конфигуриран да изпълнява набор за събиране на данни, който включваше процесор\% процесорно време и броячи на SQL Server:SQLStatisitics\Batch Requests/sec за проследяване на режийните разходи за производителност за всеки от тестовете. Освен това разширените събития са конфигурирани да проследяват събитието rpc_completed, за да позволи проследяване на средната продължителност, cpu_time и логическите четения за всеки от тестовете.
Small Table CarrierTrackingNumber Results
Фигура 1 – Графика на броячите за монитор на производителността
TestID | Тип данни на колона | Тип данни за параметри | Ср. % процесорно време | Ср. пакетни заявки/сек | Продължителност h:mm:ss |
---|---|---|---|---|---|
1 | Варчар | Варчар | 2.5 | 192.3 | 0:00:51 |
2 | Варчар | Nvarchar | 19.4 | 46,7 | 0:03:33 |
3 | Nvarchar | Nvarchar | 2.6 | 192.3 | 0:00:51 |
Таблица 2 – Средни данни за монитора на производителността
От резултатите можем да видим, че имплицитното преобразуване от страна на колоната от varchar към nvarchar и полученото сканиране на индекса имат значително влияние върху производителността на работното натоварване. Средното % време на процесора за теста за имплицитно преобразуване от страна на колоната (TestID =2) е почти десет пъти по-голямо от другите тестове, при които неявното преобразуване от страна на колоната, водещо до сканиране на индекс, не се е случило. Освен това, средните пакетни заявки/сек за теста за имплицитно преобразуване от страна на колоната бяха малко под 25% от другите тестове. Продължителността на тестовете, при които не се осъществиха неявни преобразувания, отне 51 секунди, въпреки че данните бяха съхранени като nvarchar в тест номер 3, използвайки тип данни nvarchar, изискващ два пъти повече пространство за съхранение. Това се очаква, защото таблицата все още е по-малка от буферния пул.
TestID | Ср. cpu_time (µs) | Средна продължителност (µs) | Ср. 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 |
Таблица 3 – Средни стойности за разширени събития
Данните, събрани от събитието rpc_completed в Разширени събития, показват, че средното cpu_time, продължителност и логически четения, свързани със заявките, които не извършват имплицитно преобразуване от страна на колоната, са приблизително еквивалентни, където неявното преобразуване от страна на колоната изисква значителен CPU режийни разходи, както и по-дълга средна продължителност със значително по-логично четене.
Разширена таблица CarrierTrackingNumber Results
Фигура 4 – Графика на броячите за монитор на производителността
TestID | Тип данни на колона | Тип данни за параметри | Ср. % процесорно време | Ср. пакетни заявки/сек | Продължителност h:mm:ss |
---|---|---|---|---|---|
1 | Варчар | Варчар | 7.2 | 164.0 | 0:01:00 |
2 | Варчар | Nvarchar | 83,8 | 15.4 | 0:10:49 |
3 | Nvarchar | Nvarchar | 7.0 | 166,7 | 0:01:00 |
Таблица 5 – Средни данни за монитора на производителността
С увеличаването на размера на данните се увеличава и производителността на имплицитното преобразуване от страна на колоната. Средното % време на процесора за теста за имплицитно преобразуване от страна на колоната (TestID =2) отново е почти десет пъти повече от другите тестове, при които неявното преобразуване от страна на колоната, водещо до сканиране на индекс, не се е случило. Освен това, средните пакетни заявки/сек за теста за имплицитно преобразуване от страна на колоната бяха малко под 10% от другите тестове. Продължителността на тестовете, при които не са осъществени неявни преобразувания, отне една минута, докато тестът за неявно преобразуване от страна на колоната изисква близо единадесет минути за изпълнение.
TestID | Ср. cpu_time (µs) | Средна продължителност (µs) | Ср. logical_reads |
---|---|---|---|
1 | 728,5 | 1036,5 | 569.6 |
2 | 214 174,6 | 59 519,1 | 4358,2 |
3 | 821.5 | 1032,4 | 553,5 |
Таблица 6 – Средни стойности за разширени събития
Резултатите от разширените събития наистина започват да показват режийните разходи за производителност, причинени от имплицитните преобразувания от страна на колоната за работното натоварване. Средното cpu_time на изпълнение скача до над 214 ms и е над 200 пъти cpu_time за изразите, които нямат неявни преобразувания от страна на колоната. Продължителността също е почти 60 пъти по-голяма от тази на изразите, които нямат неявни преобразувания от страната на колоните.
Резюме
Тъй като размерът на данните продължава да се увеличава, режийните разходи, свързани с имплицитните преобразувания от страната на колоните, които водят до сканиране на индекс за работното натоварване, също ще продължат да нарастват и важното нещо, което трябва да запомните е, че в даден момент няма количество хардуер ще могат да се справят с режийните разходи за производителност. Неявните преобразувания са лесни за предотвратяване, когато съществува добър дизайн на схема на база данни и разработчиците следват добри техники за кодиране на приложения. В ситуации, в които практиките за кодиране на приложения водят до параметризация, която използва параметризация на nvarchar, е по-добре да съпоставите дизайна на схемата на базата данни с параметризацията на заявката, отколкото да използвате varchar колони в дизайна на базата данни и да поемете допълнителни разходи за производителност от имплицитното преобразуване от страна на колоната.
Изтеглете демонстрационните скриптове:Implicit_Conversion_Tests.zip (5 KB)