Веднага след като видях функцията на SQL 2016 AT TIME ZONE, за която писах тук на sqlperformance.com a преди няколко месеца си спомних за доклад, който се нуждаеше от тази функция. Тази публикация формира казус за това как го видях да работи, което се вписва в T-SQL вторник този месец, организиран от Мат Гордън (@sqlatspeed). (Това е 87-ият T-SQL вторник и наистина трябва да напиша повече публикации в блога, особено за неща, които не са подканени от T-SQL вторник.)
Ситуацията беше следната и това може да ви звучи познато, ако прочетете по-ранната ми публикация.
Много преди да съществуват LobsterPot Solutions, трябваше да изготвя доклад за възникнали инциденти и по-специално да покажа колко пъти са били дадени отговори в рамките на SLA и колко пъти е пропуснато SLA. Например, инцидент Sev2, възникнал в 16:30 ч. в делничен ден, ще трябва да има отговор в рамките на 1 час, докато инцидент Sev2, възникнал в 17:30 ч. в делничен ден, ще трябва да има отговор в рамките на 3 часа. Или нещо подобно – забравям замесените числа, но си спомням, че служителите на бюрото за помощ въздъхнаха с облекчение, когато 17:00 се въртеше, защото нямаше да им се налага да реагират толкова бързо на нещата. 15-минутните сигнали за Sev1 внезапно ще се удължат до един час и спешността ще изчезне.
Но проблем ще дойде всеки път, когато лятното часово време започне или свърши.
Сигурен съм, че ако сте се занимавали с бази данни, ще знаете каква е болката при лятното часово време. Предполага се, че Бен Франклин е дошъл на идеята - и за това трябва да бъде ударен от мълния или нещо подобно. Западна Австралия го опита няколко години наскоро и разумно го изостави. И общият консенсус е да се съхраняват данни за дата/час е това да се прави в UTC.
Ако не съхранявате данни в UTC, рискувате събитието да започне в 2:45 сутринта и да завърши в 2:15 сутринта, след като часовниците се върнат назад. Или има инцидент с SLA, който започва в 1:59 сутринта точно преди часовниците да тръгнат напред. Сега тези времена са добре, ако съхранявате часовата зона, в която се намират, но в UTC времето просто работи както се очаква.
...с изключение на докладването.
Защото как трябва да знам дали определена дата е била преди да започне лятното часово време или след това? Може да знам, че инцидент се е случил в 6:30 ч. по UTC, но това 16:30 ч. в Мелбърн ли е или 17:30 ч.? Очевидно мога да преценя в кой месец е, защото знам, че Мелбърн спазва лятно часово време от първата неделя на октомври до първата неделя на април, но тогава, ако има клиенти в Бризбейн, Окланд, Лос Анджелис и Финикс, и на различни места в Индиана, нещата стават много по-сложни.
За да се заобиколи това, имаше много малко часови зони, в които SLA могат да бъдат определени за тази компания. Просто се смяташе за твърде трудно да се погрижи за повече от това. След това отчетът може да бъде персонализиран така, че да казва „Помислете, че на определена дата часовата зона се е променила от X на Y“. Чувстваше се объркано, но проработи. Нямаше нужда от нищо, за да търсим системния регистър на Windows и по принцип просто работеше.
Но тези дни бих го направил по различен начин.
Сега бих използвал AT TIME ZONE.
Виждате ли, сега мога да съхранявам информацията за часовата зона на клиента като собственост на клиента. След това бих могъл да съхранявам всеки инцидент в UTC, което ми позволява да направя необходимите изчисления около броя минути за отговор, разрешаване и т.н., като същевременно мога да докладвам, използвайки местното време на клиента. Ако приемем, че моят IncidentTime действително е бил съхранен с помощта на datetime, а не datetimeoffset, просто би било въпрос на използване на код като:
i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE c.tz
…което първо поставя i.IncidentTime без часови зони в UTC, преди да го преобразува в часовата зона на клиента. И тази часова зона може да бъде „AUS Eastern Standard Time“ или „Mauritius Standard Time“ или каквото и да е. И SQL Engine остава да разбере какво изместване да използва за това.
В този момент мога много лесно да създам отчет, който изброява всеки инцидент за даден период от време и да го покажа в местната часова зона на клиента. Мога да преобразувам стойността към типа данни за времето и след това да докладвам колко инцидента са били в рамките на работните часове или не.
И всичко това е много полезно, но какво ще кажете за индексирането, за да се справите добре с това? В крайна сметка AT TIME ZONE е функция. Но промяната на часовата зона не променя реда, в който действително са се случили инцидентите, така че би трябвало да е наред.
За да тествам това, създадох таблица, наречена dbo.Incidents, и индексирах колоната IncidentTime. След това изпълних тази заявка и потвърдих, че е използвано търсене на индекс.
select i.IncidentTime, itz.LocalTime from dbo.Incidents i cross apply (select i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where i.IncidentTime >= '20170201' and i.IncidentTime < '20170301';
Но искам да филтрирам по itz.LocalTime...
select i.IncidentTime, itz.LocalTime from dbo.Incidents i cross apply (select i.IncidentTime AT TIME ZONE 'UTC' AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where itz.LocalTime >= '20170201' and itz.LocalTime < '20170301';
Без късмет. Индексът не му хареса.
Предупрежденията са, защото трябва да преглеждам много повече от данните, които ме интересуват.
Дори се опитах да използвам таблица с поле datetimeoffset. В края на краищата, AT TIME ZONE може да промени реда при преместване от дата и час към изместване на дата и час, въпреки че редът не се променя при преминаване от отместване на дата и време към друго отместване на дата и час. Дори се опитах да се уверя, че нещото, с което го сравнявам, е в часовата зона.
select i.IncidentTime, itz.LocalTime from dbo.IncidentsOffset i cross apply (select i.IncidentTime AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where itz.LocalTime >= cast('20170201' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time' and itz.LocalTime < cast('20170301' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time';
Все още няма късмет!
Така че сега имах две възможности. Единият беше да се съхрани преобразуваната версия заедно с UTC версията и да се индексира. Мисля, че това е болка. Това със сигурност е много по-голяма промяна в базата данни, отколкото бих искал.
Другият вариант беше да използвам това, което наричам помощни предикати. Това са нещата, които виждате, когато използвате LIKE. Те са предикати, които могат да се използват като предикати за търсене, но не точно това, което искате.
Предполагам, че без значение коя часова зона ме интересува, IncidentTimes, които ме интересуват, са в много специфичен диапазон. Този диапазон е не повече от един ден по-голям от предпочитания от мен диапазон от двете страни.
Така че ще включа два допълнителни предиката.
select i.IncidentTime, itz.LocalTime from dbo.IncidentsOffset i cross apply (select i.IncidentTime AT TIME ZONE 'Cen. Australia Standard Time') itz (LocalTime) where itz.LocalTime >= cast('20170201' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time' and itz.LocalTime < cast('20170301' as datetimeoffset) AT TIME ZONE 'Cen. Australia Standard Time and i.IncidentTime >= dateadd(day,-1,'20170201') and i.IncidentTime < dateadd(day, 1,'20170301');
Сега моят индекс може да се използва. Трябва да прегледа 30 реда, преди да го филтрира до 28-те, които го интересуват – но това е много по-добре от сканирането на цялото нещо.
И знаете ли – това е поведението, което виждам през цялото време от редовни заявки, например когато правя CAST(myDateTimeColumns AS DATE) =@SomeDate или използвам LIKE.
аз съм добре с това. AT TIME ZONE е чудесен за това, че ми позволява да обработвам преобразуванията на часовите си зони и като се има предвид какво се случва с моите заявки, не е нужно да жертвам и производителността.
@rob_farley