Най-честата необходимост от премахване на времето от стойност за дата и час е да се получат всички редове, които представляват поръчки (или посещения, или злополуки), възникнали в даден ден. Въпреки това, не всички техники, които се използват за това, са ефективни или дори безопасни.
TL;DR версия
Ако искате заявка за безопасен диапазон, която се представя добре, използвайте диапазон с отворен край или, за еднодневни заявки на SQL Server 2008 и по-нови, използвайте CONVERT(DATE)
:
ДЕКЛАРИРАНЕ @днес DATETIME; -- само на <=2005:SET @today =DATEADD(DAY, DATEDIFF(DAY, '20000101', CURRENT_TIMESTAMP), '20000101'); -- или от 2008 г. и по-нови:SET @today =CONVERT(DATE, CURRENT_TIMESTAMP); -- и след това използвайте отворен диапазон в заявката:...WHERE OrderDate>=@today AND OrderDateНякои предупреждения:
- Внимавайте с
DATEDIFF
подход, тъй като могат да възникнат някои аномалии в оценката на кардиналността (вижте тази публикация в блога и въпроса за препълването на стека, който го подтикна за повече информация).- Докато последният все още потенциално ще използва търсене на индекс (за разлика от всеки друг израз без възможност за саргиране, който някога съм срещал), трябва да внимавате да преобразувате колоната в дата, преди да сравнявате. Този подход също може да доведе до фундаментално погрешни оценки на мощността. Вижте този отговор от Мартин Смит за повече подробности.
Във всеки случай, прочетете нататък, за да разберете защо това са единствените два подхода, които препоръчвам.
Не всички подходи са безопасни
Като несигурен пример виждам, че този се използва много:
КЪДЕ Дата на поръчката МЕЖДУ DATEDIFF(DAY, 0, GETDATE()) И DATEADD(MILLISECOND, -3, DATEDIFF(DAY, 0, GETDATE()) + 1);Има няколко проблема с този подход, но най-забележителният е изчисляването на „края“ на днешния ден – ако основният тип данни е
SMALLDATETIME
, този краен диапазон ще се закръгли нагоре; ако еDATETIME2
, теоретично бихте могли да пропуснете данни в края на деня. Ако изберете минути, наносекунди или друга празнина, за да посрещнете текущия тип данни, вашата заявка ще започне да има странно поведение, ако типът данни някога се промени по-късно (и нека бъдем честни, ако някой промени типа на тази колона, за да бъде повече или по-малко детайлен, те не тичат наоколо, проверявайки всяка една заявка, която осъществява достъп до нея). Необходимостта от кодиране по този начин в зависимост от типа на данните за дата/час в основната колона е фрагментирана и податлива на грешки. Много по-добре е да използвате отворени периоди от време за това:Говоря за това много повече в няколко стари публикации в блога:
- Какво общо имат МЕЖДУ и дявола?
- Лоши навици, които да се откажем:неправилно обработване на заявки за дата/обхват
Но исках да сравня ефективността на някои от по-често срещаните подходи, които виждам там. Винаги съм използвал диапазони с отворен край и от SQL Server 2008 успяхме да използваме
CONVERT(DATE)
и все още използва индекс за тази колона, който е доста мощен.SELECT CONVERT(CHAR(8), CURRENT_TIMESTAMP, 112);SELECT CONVERT(CHAR(10), CURRENT_TIMESTAMP, 120);SELECT CONVERT(DATE, CURRENT_TIMESTAMP);SELECT DATEADD(DAY, DATEDIFF(DAY, '1900010) CURRENT_TIMESTAMP), '19000101');SELECT CONVERT(DATETIME, DATEDIFF(DAY, '19000101', CURRENT_TIMESTAMP));SELECT CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, CURRENT_TIMESTAMP)(DATETIME, SELECT TIMESTAMP)); CONVERT(FLOAT, CURRENT_TIMESTAMP)));Прост тест за производителност
За да извърша много прост първоначален тест за ефективност, направих следното за всяко от горните твърдения, като зададох променлива на изхода от изчислението 100 000 пъти:
ИЗБЕРЕТЕ SYSDATETIME();GO DECLARE @d DATETIME =[метод на преобразуване];GO 100000 SELECT SYSDATETIME();GOНаправих това три пъти за всеки метод и всички те продължиха в диапазона от 34-38 секунди. Така че, строго погледнато, има много незначителни разлики в тези методи при извършване на операциите в паметта:
По-сложен тест за ефективност
Исках също да сравня тези методи с различни типове данни (
DATETIME
,SMALLDATETIME
иDATETIME2
), както срещу клъстериран индекс, така и срещу купчина, както и със и без компресия на данни. Така че първо създадох проста база данни. Чрез експериментиране установих, че оптималният размер за обработка на 120 милиона реда и цялата активност на регистрационните файлове, която може да възникне (и за предотвратяване на намеса на събитията за автоматично нарастване на тестването) е файл с данни от 20 GB и дневник от 3 GB:СЪЗДАВАНЕ НА БАЗА ДАННИ [Datetime_Testing]В ОСНОВНО (ИМЕ =N'Datetime_Testing_Data', FILENAME =N'D:\DATA\Datetime_Testing.mdf', SIZE =20480000KB , MAXSIZE =UNLIMITED, FILEGR20 =FILEGROWTHED, FILEGR20 'Datetime_Testing_Log', FILENAME =N'E:\LOGS\Datetime_Testing_log.ldf', SIZE =3000000KB , MAXSIZE =НЕОГРАНИЧЕН, FILEGROWTH =20480KB );След това създадох 12 таблици:
-- клъстериран индекс без компресия:CREATE TABLE dbo.smalldatetime_nocompression_clustered(dt SMALLDATETIME);CREATE CLUSTERED INDEX x ON dbo.smalldatetime_nocompression_clustered(dt); -- купчина без компресия:CREATE TABLE dbo.smalldatetime_nocompression_heap(dt SMALLDATETIME); -- клъстериран индекс с компресия на страница:CREATE TABLE dbo.smalldatetime_compression_clustered(dt SMALLDATETIME) WITH (DATA_COMPRESSION =PAGE); СЪЗДАЙТЕ КЛУСТРИРАН ИНДЕКС x ON dbo.smalldatetime_compression_clustered(dt)WITH (DATA_COMPRESSION =PAGE); -- купчина с компресия на страница:CREATE TABLE dbo.smalldatetime_compression_heap(dt SMALLDATETIME)WITH (DATA_COMPRESSION =PAGE);[След това повторете отново за DATETIME и DATETIME2.]
След това вмъкнах 10 000 000 реда във всяка таблица. Направих това, като създадох изглед, който ще генерира същите 10 000 000 дати всеки път:
CREATE VIEW dbo.TenMillionDatesAS SELECT TOP (10000000) d =DATEADD(MINUTE, ROW_NUMBER() OVER (ORDER BY s1.[object_id]), '19700101') FROM sys.all_columns КАТО syss.ASCROSSall s JOIN sys.all_columns. ПОРЪЧАЙТЕ ПО s1.[object_id];Това ми позволи да попълвам таблиците по следния начин:
INSERT /* dt_comp_clus */ dbo.datetime_compression_clustered(dt) SELECT CONVERT(DATETIME, d) ОТ dbo.TenMillionDates;CHECKPOINT;INSERT /* dt2_comp_clus */ dbo.datetime2_compression_clustered(VERTIME,dt)SELECT .TenMillionDates;CHECKPOINT;INSERT /* sdt_comp_clus */ dbo.smalldatetime_compression_clustered(dt) SELECT CONVERT(SMALLDATETIME, d) ОТ dbo.TenMillionDates;CHECKPOINT;[След това повторете отново за купчините и некомпресирания клъстериран индекс. Поставих
CHECKPOINT
между всяко вмъкване, за да се гарантира повторно използване на дневника (моделът за възстановяване е прост).]Вмъкнете времена и използвано място
Ето времето за всяко вмъкване (както е заснето с Plan Explorer):
А ето и количеството пространство, заето от всяка таблица:
SELECT [таблица] =OBJECT_NAME([object_id]), row_count, page_count =reserved_page_count, reserved_size_MB =reserved_page_count * 8/1024FROM sys.dm_db_partition_stats WHERE OBJECT_NAME([object_id']) date LIKE>
Ефективност на модела на заявката
След това се заех да тествам два различни модела на заявки за производителност:
- Преброяване на редовете за конкретен ден, като се използват горните седем подхода, както и отворения период от време
- Преобразуване на всички 10 000 000 реда с помощта на горните седем подхода, както и просто връщане на необработените данни (тъй като форматирането от страна на клиента може да е по-добро)
[С изключение на
FLOAT
методи иDATETIME2
колона, тъй като това преобразуване не е законно.]За първия въпрос заявките изглеждат така (повтарящи се за всеки тип таблица):
SELECT /* C_CHAR10 - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE CONVERT(CHAR(10), dt, 120) ='19860301'; SELECT /* C_CHAR8 - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE CONVERT(CHAR(8), dt, 112) ='19860301'; SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) ='19860301'; SELECT /* C_DATETIME - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) ='19860301'; SELECT /* C_DATE - dt_comp_clus */ COUNT(*) FROM dbo.datetime_compression_clustered WHERE CONVERT(DATE, dt) ='19860301'; SELECT /* C_INT_FLOAT - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) ='19860301'; SELECT /* DATEADD - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') ='19860301 SELECT /* RANGE - dt_comp_clus */ COUNT(*) ОТ dbo.datetime_compression_clustered WHERE dt>='19860301' И dt <'19860302';Резултатите срещу клъстериран индекс изглеждат така (щракнете за уголемяване):
Тук виждаме, че преобразуването към дата и отвореният диапазон с помощта на индекс са най-добри. Въпреки това, срещу купчина, конвертирането до дата всъщност отнема известно време, което прави отворения диапазон оптималния избор (щракнете, за да увеличите):
И ето втория набор от заявки (отново, повтарящи се за всеки тип таблица):
SELECT /* C_CHAR10 - dt_comp_clus */ dt =CONVERT(CHAR(10), dt, 120) ОТ dbo.datetime_compression_clustered; SELECT /* C_CHAR8 - dt_comp_clus */ dt =CONVERT(CHAR(8), dt, 112) ОТ dbo.datetime_compression_clustered; SELECT /* C_FLOOR_FLOAT - dt_comp_clus */ dt =CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, dt))) FROM dbo.datetime_compression_clustered; SELECT /* C_DATETIME - dt_comp_clus */ dt =CONVERT(DATETIME, DATEDIFF(DAY, '19000101', dt)) FROM dbo.datetime_compression_clustered; SELECT /* C_DATE - dt_comp_clus */ dt =CONVERT(DATE, dt) FROM dbo.datetime_compression_clustered; SELECT /* C_INT_FLOAT - dt_comp_clus */ dt =CONVERT(DATETIME, CONVERT(INT, CONVERT(FLOAT, dt))) FROM dbo.datetime_compression_clustered; SELECT /* DATEADD - dt_comp_clus */ dt =DATEADD(DAY, DATEDIFF(DAY, '19000101', dt), '19000101') ОТ dbo.datetime_compression_clustered; SELECT /* RAW - dt_comp_clus */ dt FROM dbo.datetime_compression_clustered;Фокусирайки се върху резултатите за таблици с клъстериран индекс, става ясно, че преобразуването до дата е много близък до избора на необработените данни (щракнете за увеличаване):
(За този набор от заявки, купчината показа много сходни резултати – практически неразличими.)
Заключение
В случай, че искате да преминете към основната линия, тези резултати показват, че преобразуванията в паметта не са важни, но ако преобразувате данни при излизане от таблица (или като част от предикат за търсене), избраният от вас метод може да има драматично въздействие върху производителността. Преобразува се в
DATE
(за един ден) или използването на отворен период от време във всеки случай ще доведе до най-добра производителност, докато най-популярният метод там – преобразуването в низ – е абсолютно ужасен.Виждаме също, че компресията може да има приличен ефект върху пространството за съхранение, с много незначително въздействие върху производителността на заявката. Ефектът върху производителността на вмъкване изглежда зависи също толкова от това дали таблицата има клъстериран индекс, а не от това дали компресирането е активирано или не. Въпреки това, с клъстериран индекс на място, имаше забележимо увеличение във времето, необходимо за вмъкване на 10 милиона реда. Нещо, което трябва да имате предвид и да балансирате със спестяването на дисково пространство.
Очевидно може да има много повече тестове, с по-съществени и разнообразни натоварвания, които може да проуча допълнително в бъдеща публикация.