Я вирішив це, маючи дуже просту таблицю календаря - кожен рік має один рядок на підтримуваний часовий пояс зі стандартним зміщенням та початковою датою / кінцевою датою / кінцевою датою DST та її зміщення (якщо цей часовий пояс підтримує це). Тоді вбудована в схему функція з табличним значенням, яка забирає час джерела (звичайно в UTC) і додає / віднімає зміщення.
Це, очевидно, ніколи не буде дуже успішним, якщо ви повідомляєте про велику частину даних; поділ може здатися, що це допомагає, але у вас все одно будуть випадки, коли останні кілька годин за один рік або перші кілька годин у наступному році фактично належать до іншого року при перетворенні на певний часовий пояс - так що ви ніколи не зможете отримати справжній розділ ізоляція, за винятком випадків, коли ваш діапазон звітності не включає 31 грудня чи 1 січня.
Є кілька дивних випадків, які потрібно врахувати:
2014-11-02 05:30 UTC та 2014-11-02 06:30 UTC обидва переходять на 01:30 у східному часовому поясі (наприклад, один вперше був 01:30 локально, а потім один вдруге, коли годинник відкинувся з 2:00 до 1:00 ранку, а минула ще півгодини). Тож вам потрібно вирішити, як поводитися з цією годиною звітування - згідно з UTC, ви повинні побачити подвоєний трафік або обсяг того, що ви вимірюєте, як тільки ці дві години відображаються на одну годину в часовому поясі, який дотримується DST. Сюди також можна грати в веселі ігри з послідовністю подій, оскільки щось, що логічно повинно було статися після того, як могло з’явитися щось іншестатися до цього, як тільки час буде встановлено на одну годину замість двох. Крайній приклад - це перегляд сторінки, який стався о 05:59 UTC, потім клацання, яке відбулося о 06:00 UTC. У UTC час вони траплялися хвилини один від одного, але коли переходили на східний час, вигляд стався о 1:59 ранку, а клацання відбулося на годину раніше.
2014-03-09 02:30 у США ніколи не буває. Це тому, що о 02:00 ми перекидаємо годинник вперед до 3:00 ранку. Тому, ймовірно, ви захочете помилитися, якщо користувач введе такий час і попросить вас перетворити його в UTC або спроектувати форму, щоб користувачі не могли вибрати такий час.
Навіть маючи на увазі ці крайові випадки, я все ще думаю, що ви маєте правильний підхід: зберігайте дані в UTC. Набагато простіше відображати дані в інші часові пояси з UTC, ніж з деякого часового поясу в інший часовий пояс, особливо коли різні часові пояси починаються / закінчуються DST в різні дати, і навіть той самий часовий пояс може перемикатися, використовуючи різні правила в різні роки ( наприклад, США змінили правила 6 років тому або близько того).
Ви хочете використовувати таблицю календаря для всього цього, а не якийсь CASE
виразний вираз (не твердження ). Я щойно написав трисерійну серію для MSSQLTips.com про це; Я думаю, що третя частина буде для вас найбільш корисною:
http://www.mssqltips.com/sqlservertip/3173/handle-conversion-between-time-zones-in-sql-server--part-1/
http://www.mssqltips.com/sqlservertip/3174/handle-conversion-between-time-zones-in-sql-server--part-2/
http://www.mssqltips.com/sqlservertip/3175/handle-conversion-between-time-zones-in-sql-server--part-3/
Тим часом справжній живий приклад
Скажімо, у вас дуже проста таблиця фактів. Єдиний факт, який мене хвилює в цьому випадку, - це час події, але я додам безглуздий GUID лише для того, щоб зробити таблицю достатньо широкою, щоб нею було цікаво. Знову ж таки, щоб бути явним, таблиця фактів зберігає події лише в UTC та час UTC. Я навіть суфіксував стовпчик, _UTC
щоб не було плутанини.
CREATE TABLE dbo.Fact
(
EventTime_UTC DATETIME NOT NULL,
Filler UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID()
);
GO
CREATE CLUSTERED INDEX x ON dbo.Fact(EventTime_UTC);
GO
Тепер давайте завантажимо нашу таблицю фактів на 10 000 000 рядків - що представляють кожні 3 секунди (1200 рядків на годину) з 2013-12-30 в опівночі UTC до десь після 5:00 UTC 2014-12-12. Це гарантує, що дані відхиляються від меж року, а також DST вперед та назад для декількох часових поясів. Це виглядає дійсно страшно, але в моїй системі знадобилося ~ 9 секунд. Таблиця повинна скласти приблизно 325 Мб.
;WITH x(c) AS
(
SELECT TOP (10000000) DATEADD(SECOND,
3*(ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1),
'20131230')
FROM sys.all_columns AS s1
CROSS JOIN sys.all_columns AS s2
ORDER BY s1.[object_id]
)
INSERT dbo.Fact WITH (TABLOCKX) (EventTime_UTC)
SELECT c FROM x;
І просто щоб показати, як виглядатиме типовий запит запиту щодо цієї таблиці рядків 10ММ, якщо я запускаю цей запит:
SELECT DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0),
COUNT(*)
FROM dbo.Fact
WHERE EventTime_UTC >= '20140308'
AND EventTime_UTC < '20140311'
GROUP BY DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0);
Я отримую цей план, і він повертається за 25 мілісекунд *, виконуючи 358 прочитаних, щоб повернути 72 погодні підсумки:
* Тривалість, виміряна нашим безкоштовним SQL провідником плану Sentry , який відкидає результати, тому це не включає час передачі даних в мережі, візуалізацію тощо. В якості додаткової відмови від відповідальності я працюю в SQL Sentry.
Це займе трохи більше часу, очевидно, якщо я зроблю діапазон занадто великий - місяць даних займає 258 мс, два місяці займає понад 500 мс і так далі. Паралелізм може спричинити:
Тут ви починаєте думати про інші, кращі рішення для задоволення запитів звітування, і це не має нічого спільного з тим, у якому часовому поясі відображатиметься ваш вихід. Я не буду вступати в це, я просто хочу продемонструвати, що конверсія часового поясу насправді не змусить ваші запити звітів висмоктувати все набагато більше, і вони вже можуть смоктати, якщо ви отримуєте великі діапазони, які не підтримуються належним чином покажчики. Я збираюся дотримуватися невеликих діапазонів дат, щоб показати, що логіка правильна, і дозволю вам хвилюватися, щоб переконатися, що ваші запити звітності на основі діапазону працюють адекватно, з перетвореннями часових поясів або без них.
Гаразд, зараз нам потрібні таблиці для зберігання наших часових поясів (із зміщенням за хвилини, оскільки не у всіх є навіть години у неробочому режимі UTC) та дати зміни DST для кожного підтримуваного року. Для простоти я збираюся лише ввести кілька часових поясів і один рік, щоб відповідати вищезазначеним даним.
CREATE TABLE dbo.TimeZones
(
TimeZoneID TINYINT NOT NULL PRIMARY KEY,
Name VARCHAR(9) NOT NULL,
Offset SMALLINT NOT NULL, -- minutes
DSTName VARCHAR(9) NOT NULL,
DSTOffset SMALLINT NOT NULL -- minutes
);
Включено декілька часових поясів для різноманітності, деякі з компенсацією за півгодини, а деякі з них не дотримуються DST. Слід зазначити , що в Австралії, в південній півкулі спостерігається DST під час нашої зими, так що їх годинник йде назад в квітні і вперед в жовтні. (Наведена вище таблиця гортає назви, але я не впевнений, як зробити це менш заплутаним для часових поясів південної півкулі.)
INSERT dbo.TimeZones VALUES
(1, 'UTC', 0, 'UTC', 0),
(2, 'GMT', 0, 'BST', 60),
-- London = UTC in winter, +1 in summer
(3, 'EST', -300, 'EDT', -240),
-- East coast US (-5 h in winter, -4 in summer)
(4, 'ACDT', 630, 'ACST', 570),
-- Adelaide (Australia) +10.5 h Oct - Apr, +9.5 Apr - Oct
(5, 'ACST', 570, 'ACST', 570);
-- Darwin (Australia) +9.5 h year round
Тепер календарна таблиця, щоб знати, коли змінюються ТЗ. Я збираюся лише вставити цікаві рядки (кожен часовий пояс вище, і лише зміни DST за 2014 рік). Для зручності обчислень вперед і назад, я зберігаю як момент у UTC, де змінюється часовий пояс, так і той самий момент у місцевому часі. Для часових поясів, які не дотримуються DST, це стандартно протягом усього року, а DST "запускається" 1 січня.
CREATE TABLE dbo.Calendar
(
TimeZoneID TINYINT NOT NULL FOREIGN KEY
REFERENCES dbo.TimeZones(TimeZoneID),
[Year] SMALLDATETIME NOT NULL,
UTCDSTStart SMALLDATETIME NOT NULL,
UTCDSTEnd SMALLDATETIME NOT NULL,
LocalDSTStart SMALLDATETIME NOT NULL,
LocalDSTEnd SMALLDATETIME NOT NULL,
PRIMARY KEY (TimeZoneID, [Year])
);
Ви можете, безумовно, заповнити це алгоритмами (і майбутня серія підказок використовує деякі розумні методи, засновані на наборі, якщо я це так говорю), а не циклічне заповнення вручну, що у вас є. Для цієї відповіді я вирішив просто вручну зарахувати один рік на п’ять часових поясів, і я не збираюся турбувати жодних фантазійних хитрощів.
INSERT dbo.Calendar VALUES
(1, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00'),
(2, '20140101', '20140330 01:00','20141026 00:00','20140330 02:00','20141026 01:00'),
(3, '20140101', '20140309 07:00','20141102 06:00','20140309 03:00','20141102 01:00'),
(4, '20140101', '20140405 16:30','20141004 16:30','20140406 03:00','20141005 02:00'),
(5, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00');
Гаразд, значить, у нас є дані про факти та наші таблиці "виміру" (я стискаюся, коли це кажу), тож яка логіка? Ну, я припускаю, що користувачі виберуть свій часовий пояс і введуть діапазон дат для запиту. Я також припускаю, що діапазон дат буде повним днем у власному часовому поясі; немає часткових днів, не маю на увазі часткових годин. Таким чином вони передадуть дату початку, кінцеву дату та TimeZoneID. Звідти ми будемо використовувати скалярну функцію для перетворення дати початку / кінця з цього часового поясу в UTC, що дозволить нам фільтрувати дані на основі діапазону UTC. Після того, як ми це зробимо і виконали наші агрегації на ньому, ми можемо застосувати перетворення згрупованих часів назад у часовий пояс джерела, перш ніж відображатись користувачеві.
Скалярний АДС:
CREATE FUNCTION dbo.ConvertToUTC
(
@Source SMALLDATETIME,
@SourceTZ TINYINT
)
RETURNS SMALLDATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN
(
SELECT DATEADD(MINUTE, -CASE
WHEN @Source >= src.LocalDSTStart
AND @Source < src.LocalDSTEnd THEN t.DSTOffset
WHEN @Source >= DATEADD(HOUR,-1,src.LocalDSTStart)
AND @Source < src.LocalDSTStart THEN NULL
ELSE t.Offset END, @Source)
FROM dbo.Calendar AS src
INNER JOIN dbo.TimeZones AS t
ON src.TimeZoneID = t.TimeZoneID
WHERE src.TimeZoneID = @SourceTZ
AND t.TimeZoneID = @SourceTZ
AND DATEADD(MINUTE,t.Offset,@Source) >= src.[Year]
AND DATEADD(MINUTE,t.Offset,@Source) < DATEADD(YEAR, 1, src.[Year])
);
END
GO
І таблична функція:
CREATE FUNCTION dbo.ConvertFromUTC
(
@Source SMALLDATETIME,
@SourceTZ TINYINT
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT
[Target] = DATEADD(MINUTE, CASE
WHEN @Source >= trg.UTCDSTStart
AND @Source < trg.UTCDSTEnd THEN tz.DSTOffset
ELSE tz.Offset END, @Source)
FROM dbo.Calendar AS trg
INNER JOIN dbo.TimeZones AS tz
ON trg.TimeZoneID = tz.TimeZoneID
WHERE trg.TimeZoneID = @SourceTZ
AND tz.TimeZoneID = @SourceTZ
AND @Source >= trg.[Year]
AND @Source < DATEADD(YEAR, 1, trg.[Year])
);
І процедура, яка його використовує ( редагувати : оновлено для обробки 30-хвилинної зсувної групи):
CREATE PROCEDURE dbo.ReportOnDateRange
@Start SMALLDATETIME, -- whole dates only please!
@End SMALLDATETIME, -- whole dates only please!
@TimeZoneID TINYINT
AS
BEGIN
SET NOCOUNT ON;
SELECT @Start = dbo.ConvertToUTC(@Start, @TimeZoneID),
@End = dbo.ConvertToUTC(@End, @TimeZoneID);
;WITH x(t,c) AS
(
SELECT DATEDIFF(MINUTE, @Start, EventTime_UTC)/60,
COUNT(*)
FROM dbo.Fact
WHERE EventTime_UTC >= @Start
AND EventTime_UTC < DATEADD(DAY, 1, @End)
GROUP BY DATEDIFF(MINUTE, @Start, EventTime_UTC)/60
)
SELECT
UTC = DATEADD(MINUTE, x.t*60, @Start),
[Local] = y.[Target],
[RowCount] = x.c
FROM x OUTER APPLY
dbo.ConvertFromUTC(DATEADD(MINUTE, x.t*60, @Start), @TimeZoneID) AS y
ORDER BY UTC;
END
GO
(Можливо, ви хочете перейти на коротке замикання там або окрему збережену процедуру, якщо користувач захоче звітувати в UTC - очевидно, що переклад на UTC і з нього буде досить марною роботою.)
Приклад дзвінка:
EXEC dbo.ReportOnDateRange
@Start = '20140308',
@End = '20140311',
@TimeZoneID = 3;
Повертається через 41 мс * та генерує цей план:
* Знову з відхиленими результатами.
Протягом 2 місяців він повертається за 507 мс, а план ідентичний, окрім кількості рахунків:
Хоча трохи складніше і дещо збільшується час роботи, я досить впевнений, що такий тип підходу вийде набагато, набагато краще, ніж підхід мостової таблиці. І це приклад без манжети для відповіді dba.se; Я впевнений, що мою логіку та ефективність можна покращити набагато розумнішими за мене.
Ви можете ознайомитись з даними, щоб побачити крайові випадки, про які я розповідаю - жодного ряду виводу за годину, коли годинники котяться вперед, два ряди за годину, коли вони відкочуються назад (і ця година траплялася двічі). Ви також можете грати з поганими значеннями; якщо ви проходите в 20140309 02:30 за східним часом, наприклад, це не вийде занадто добре.
У мене можуть бути не всі припущення щодо того, як працюватиме ваша звітність, тому вам, можливо, доведеться внести деякі корективи. Але я думаю, що це стосується основ.