Когато SQL Server 2012 все още беше в бета версия, писах в блог за новия FORMAT()
функция:SQL Server v.Next (Denali) :CTP3 T-SQL подобрения:FORMAT().
По това време бях толкова развълнуван от новата функционалност, че дори не мислех да правя тестове за производителност. Разгледах това в по-скорошна публикация в блога, но единствено в контекста на премахване на времето от дата и час:Изрязване на време от дата и час – продължение.
Миналата седмица моят добър приятел Джейсън Хорнър (блог | @jasonhorner) ме троли с тези туитове:
| |
Проблемът ми с това е точно този FORMAT()
изглежда удобно, но е изключително неефективно в сравнение с други подходи (о, и това AS VARCHAR
нещата също са зле). Ако правите това еднократно и за малки набори от резултати, няма да се притеснявам твърде много за това; но в мащаб може да стане доста скъпо. Нека илюстрирам с пример. Първо, нека създадем малка таблица с 1000 псевдослучайни дати:
SELECT TOP (1000) d = DATEADD(DAY, CHECKSUM(NEWID())%1000, o.create_date) INTO dbo.dtTest FROM sys.all_objects AS o ORDER BY NEWID(); GO CREATE CLUSTERED INDEX d ON dbo.dtTest(d);
Сега, нека заредим кеша с данните от тази таблица и илюстрираме три от често срещаните начина, по които хората са склонни да представят точно времето:
SELECT d, CONVERT(DATE, d), CONVERT(CHAR(10), d, 120), FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest;
Сега нека изпълним отделни заявки, които използват тези различни техники. Ще ги стартираме всеки 5 пъти и ще изпълним следните варианти:
- Избиране на всички 1000 реда
- Избиране на TOP (1), подредено по групирания индексен ключ
- Присвояване на променлива (което принуждава пълно сканиране, но не позволява на изобразяването на SSMS да пречи на производителността)
Ето скрипта:
-- select all 1,000 rows GO SELECT d FROM dbo.dtTest; GO 5 SELECT d = CONVERT(DATE, d) FROM dbo.dtTest; GO 5 SELECT d = CONVERT(CHAR(10), d, 120) FROM dbo.dtTest; GO 5 SELECT d = FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest; GO 5 -- select top 1 GO SELECT TOP (1) d FROM dbo.dtTest ORDER BY d; GO 5 SELECT TOP (1) CONVERT(DATE, d) FROM dbo.dtTest ORDER BY d; GO 5 SELECT TOP (1) CONVERT(CHAR(10), d, 120) FROM dbo.dtTest ORDER BY d; GO 5 SELECT TOP (1) FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest ORDER BY d; GO 5 -- force scan but leave SSMS mostly out of it GO DECLARE @d DATE; SELECT @d = d FROM dbo.dtTest; GO 5 DECLARE @d DATE; SELECT @d = CONVERT(DATE, d) FROM dbo.dtTest; GO 5 DECLARE @d CHAR(10); SELECT @d = CONVERT(CHAR(10), d, 120) FROM dbo.dtTest; GO 5 DECLARE @d CHAR(10); SELECT @d = FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest; GO 5
Сега можем да измерим производителността със следната заявка (моята система е доста тиха; на вашата може да се наложи да извършите по-разширено филтриране от просто execution_count
):
SELECT [t] = CONVERT(CHAR(255), t.[text]), s.total_elapsed_time, avg_elapsed_time = CONVERT(DECIMAL(12,2),s.total_elapsed_time / 5.0), s.total_worker_time, avg_worker_time = CONVERT(DECIMAL(12,2),s.total_worker_time / 5.0), s.total_clr_time FROM sys.dm_exec_query_stats AS s CROSS APPLY sys.dm_exec_sql_text(s.[sql_handle]) AS t WHERE s.execution_count = 5 AND t.[text] LIKE N'%dbo.dtTest%' ORDER BY s.last_execution_time;
Резултатите в моя случай бяха доста последователни:
Заявка (съкратена) | Продължителност (микросекунди) | |||
---|---|---|---|---|
общо_изминало | avg_elapsed | total_clr | ||
ИЗБЕРЕТЕ 1000 реда | SELECT d FROM dbo.dtTest ORDER BY d; |
1,170 |
234.00 |
0 |
SELECT d = CONVERT(DATE, d) FROM dbo.dtTest ORDER BY d; |
2,437 |
487.40 |
0 |
|
SELECT d = CONVERT(CHAR(10), d, 120) FROM dbo.dtTest ORD ... |
151,521 |
30,304.20 |
0 |
|
SELECT d = FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest ORDER ... |
240,152 |
48,030.40 |
107,258 |
|
SELECT TOP (1) | SELECT TOP (1) d FROM dbo.dtTest ORDER BY d; |
251 |
50.20 |
0 |
SELECT TOP (1) CONVERT(DATE, d) FROM dbo.dtTest ORDER BY ... |
440 |
88.00 |
0 |
|
SELECT TOP (1) CONVERT(CHAR(10), d, 120) FROM dbo.dtTest ... |
301 |
60.20 |
0 |
|
SELECT TOP (1) FORMAT(d, 'yyyy-MM-dd') FROM dbo.dtTest O ... |
1,094 |
218.80 |
589 |
|
Assign variable | DECLARE @d DATE; SELECT @d = d FROM dbo.dtTest; |
639 |
127.80 |
0 |
DECLARE @d DATE; SELECT @d = CONVERT(DATE, d) FROM dbo.d ... |
644 |
128.80 |
0 |
|
DECLARE @d CHAR(10); SELECT @d = CONVERT(CHAR(10), d, 12 ... | 1,972 |
394.40 |
0 |
|
DECLARE @d CHAR(10); SELECT @d = FORMAT(d, 'yyyy-MM-dd') ... |
118,062 |
23,612.40 |
98,556 |
And to visualize the avg_elapsed_time
изход (щракнете за увеличаване):
FORMAT() очевидно е губещият:avg_elapsed_time резултати (микросекунди)
Какво можем да научим от тези резултати (отново):
- Първо и най-важно,
FORMAT()
е скъпо . FORMAT()
може, разбира се, да осигури повече гъвкавост и да даде по-интуитивни методи, които са съвместими с тези в други езици като C#. Въпреки това, в допълнение към неговите допълнителни разходи, и докатоCONVERT()
номерата на стилове са загадъчни и по-малко изчерпателни, така или иначе може да се наложи да използвате по-стария подход, тъй катоFORMAT()
е валиден само в SQL Server 2012 и по-нова версия.- Дори в режим на готовност
CONVERT()
методът може да бъде драстично скъп (макар и изключително много в случая, когато SSMS трябваше да изобрази резултатите - той ясно обработва низовете по различен начин от стойностите на датата). - Простото изтегляне на стойността за дата и час директно от базата данни винаги е било най-ефективно. Трябва да профилирате какво допълнително време е необходимо на вашето приложение да форматира датата по желание на нивото на презентацията - много е вероятно да не искате SQL Server изобщо да се занимава с красив формат (и всъщност мнозина биха спорили че тук винаги е мястото на тази логика).
Тук говорим само за микросекунди, но също така говорим само за 1000 реда. Увеличете това до действителните си размери на таблицата и въздействието от избора на грешен подход за форматиране може да бъде опустошително.
Ако искате да изпробвате този експеримент на собствената си машина, качих примерен скрипт:FormatIsNiceAndAllBut.sql_.zip