Дизайн сховища даних для звітування з даними для багатьох часових поясів


10

Ми намагаємось оптимізувати дизайн сховища даних, який підтримуватиме звітність проти даних для багатьох часових поясів. Наприклад, у нас може бути звіт про вартість активності за місяць (мільйони рядків), яка повинна відображати активність, згруповану за годиною дня. І звичайно, що година дня повинна бути "місцевою" годиною для даного часового поясу.

У нас був дизайн, який добре працював, коли ми просто підтримували UTC та один місцевий час. Стандартна конструкція розмірів дати та часу для UTC та місцевого часу, ідентифікатори в таблицях Фактів. Однак такий підхід не здається масштабним, якщо нам доведеться підтримувати звітність за 100+ часових поясів.

Наші таблиці Фактів стали б дуже широкими. Крім того, нам доведеться вирішити проблему з синтаксисом у SQL, вказавши, які ідентифікатори дати та часу використовувати для групування на будь-якому даному запуску звіту. Можливо, дуже велика заява CASE?

Я бачив кілька пропозицій отримати всі дані за тимчасовим діапазоном UTC, який ви охоплюєте, а потім повернути його на рівень презентації, щоб перетворити його на локальний і агрегувати, але обмежене тестування за допомогою SSRS припускає, що це буде дуже повільним.

Я також консультувався з деякими книгами з цього питання, і всі вони, здається, говорять, просто мають UTC і конвертують на дисплеї або мають UTC та одну локальну. Буду вдячний за будь-які думки та пропозиції.

Примітка. Це запитання схоже на: Обробка часових поясів у марті / складі даних , але я не можу коментувати це питання, тому вважаю, що це заслуговує власного питання.

Оновлення: я вибрав відповідь Аарона після того, як він зробив кілька істотних оновлень і розмістив зразок коду та діаграм. Мої попередні коментарі до його відповіді більше не матимуть особливого сенсу, оскільки вони посилалися на оригінальну редакцію відповіді. Я спробую повернутися та оновити це ще раз, якщо це буде потрібно


У контексті моєї відповіді (і оновлень я опублікую її згодом), наскільки далеко заходять ваші дані? Чи покаже щомісячний звіт 28-31 набір цілодобових шматочків? Чи завжди це буде "календарний місяць" чи це може бути справді будь-який діапазон? Що має відображатись, коли однією з дат є дата переходу вперед / назад у вибраному часовому поясі? Крім того, що саме є вкладом для звіту? Чи автоматично перетворюєте місцевий час користувача в UTC, виходячи з його поточної локальної локальності, чи мають вони налаштування, чи вибирають вони вручну, чи ви робите висновок якимось іншим способом, чи хочете, щоб запит з'ясував це?
Аарон Бертран

Щоб відповісти на ваші запитання: Дані можуть тривати до двох років. У нас є кілька звітів, які показують лише один набір 24-годинних фрагментів та інші звіти, які мають цілодобовий фрагмент щодня в діапазоні дат звітів. Діапазон дат насправді може бути будь-яким, що бажає користувач. Користувач вибирає дату початку та кінця (та часи), а потім вибирає потрібний часовий пояс зі спадного меню
Пітер М,

Відповіді:


18

Я вирішив це, маючи дуже просту таблицю календаря - кожен рік має один рядок на підтримуваний часовий пояс зі стандартним зміщенням та початковою датою / кінцевою датою / кінцевою датою 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 за східним часом, наприклад, це не вийде занадто добре.

У мене можуть бути не всі припущення щодо того, як працюватиме ваша звітність, тому вам, можливо, доведеться внести деякі корективи. Але я думаю, що це стосується основ.


0

Чи можете ви зробити трансформацію в збереженому програмі або параметризованому поданні замість шару презентації? Інший варіант - створити куб і провести обчислення в кубі.

Пояснення з коментарів:

ОП стикався з проблемами продуктивності при його обмеженому тестуванні, виконуючи обчислення в презентаційному шарі. Моя пропозиція - перемістити це до бази даних. У sql ви можете виконати параметризований вигляд за допомогою функції, що оцінюється в таблиці. Виходячи з часового поясу, переданого цій функції, дані можна обчислити та повернути з таблиці UTC. Сподіваюсь, це пояснює мою оригінальну відповідь.


Отже, представлення, що містить 100+ додаткових стовпців, де кожен рядок має вихідний час у UTC, переведений на всі 100+ часових поясів? Я навіть не можу почати розуміти, як буде написаний такий погляд. Також зауважте, що у SQL Server немає "параметризованого перегляду" ...
Аарон Бертран

хм .. так що ви думаєте. і це не те, що я мав на увазі.
КНІ

1
Тож змусьте мене думати інакше. Я, до речі, не був голосуючим за неприхильний голос, просто намагався заохотити більшу ясність вашої відповіді.
Аарон Бертран

Оп зіткнувся з проблемами продуктивності з його обмеженим тестуванням, виконавши обчислення в презентаційному шарі. Моя пропозиція - перемістити це до бази даних. У sql ви можете виконати параметризований вигляд за допомогою функції, що оцінюється в таблиці. На основі часового поясу, переданого цій функції, дані можна обчислити та повернути з таблиці utc. Сподіваюсь, це пояснює мою оригінальну відповідь.
КНІ

Як це може працювати, якщо дані агрегуються? Якщо часовий пояс зміщений на 30 хвилин, дані потраплять до іншої групи. Ви не можете просто змінити мітки, що відображаються на шарі презентації.
Colin 't Hart
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.