Лучший подход для заполнения таблицы измерения даты

8

Я ищу, чтобы заполнить таблицу измерения даты в базе данных SQL Server 2008. Поля в таблице следующие:

[DateId]                    INT IDENTITY(1,1) PRIMARY KEY
[DateTime]                  DATETIME
[Date]                      DATE
[DayOfWeek_Number]          TINYINT
[DayOfWeek_Name]            VARCHAR(9)
[DayOfWeek_ShortName]       VARCHAR(3)
[Week_Number]               TINYINT
[Fiscal_DayOfMonth]         TINYINT
[Fiscal_Month_Number]       TINYINT
[Fiscal_Month_Name]         VARCHAR(12)
[Fiscal_Month_ShortName]    VARCHAR(3)
[Fiscal_Quarter]            TINYINT     
[Fiscal_Year]               INT
[Calendar_DayOfMonth]       TINYINT
[Calendar_Month Number]     TINYINT     
[Calendar_Month_Name]       VARCHAR(9)
[Calendar_Month_ShortName]  VARCHAR(3)
[Calendar_Quarter]          TINYINT
[Calendar_Year]             INT
[IsLeapYear]                BIT
[IsWeekDay]                 BIT
[IsWeekend]                 BIT
[IsWorkday]                 BIT
[IsHoliday]                 BIT
[HolidayName]               VARCHAR(255)

Я написал функцию DateListInRange (D1, D2), которая возвращает все даты между двумя датами параметров D1 и D2 включительно.

то есть. параметры '2014-01-01' и '2014-01-03' будут возвращать:

2014-01-01
2014-01-02
2014-01-03

Я хочу заполнить таблицу DATE_DIM для всех дат в диапазоне, то есть с 2010-01-01 по 2020-01-01. Большинство полей могут быть заполнены функциями SQL 2008 DATEPART, DATENAME и YEAR.

Финансовые данные содержат немного больше логики, некоторые из которых зависят друг от друга. Например: финансовый квартал 1 -> финансовый месяц должен составлять 1, 2 или 3 финансовый квартал 2 -> финансовый месяц должен составлять 4, 5 или 6

Я могу легко написать табличную функцию, которая принимает определенную дату, а затем выводит все фискальные данные или ВСЕ поля. Тогда мне просто нужно, чтобы эта функция запускалась в каждой строке функции DateListInRange.

Я не очень обеспокоен скоростью, так как это нужно будет заполнять только несколько раз в год, когда меняется таблица праздников.

Каков наилучший способ написать это в SQL?

В настоящее время это так:

SELECT 
    [Date],
    CAST([Date] AS DATE)                AS [Date],
    DATEPART(W,[Date])                  AS [DayOfWeek_Number], -- First day of week is sunday
    DATENAME(W,[Date])                  AS [DayOfWeek_Name],
    SUBSTRING(DATENAME(DW,[Date]),1,3)  AS [DayOfWeek_ShortName],
    DATEPART(WK, [Date])                AS [WeekNumber],
    DATEPART(M, [Date])                 AS [Calendar_Month_Number],
    DATENAME(M, [Date])                 AS [Calendar_Month_Name],
    SUBSTRING(DATENAME(M, [Date]),1,3)  AS [Calendar_Month_ShortName],
    DATEPART(QQ, [Date])                AS [Calendar_Quarter],
    YEAR([Date])                        AS [Calendar_Year],

    CASE WHEN
    (
        (YEAR([Date]) % 4 = 0) AND (YEAR([Date]) % 100 != 0) 
        OR
        (YEAR([Date]) % 400 = 0)
    )
    THEN 1 ELSE 0 
    END                                     AS [IsLeapYear],

    CASE WHEN
    (
        DATEPART(W,[Date]) = 1 OR DATEPART(W,[Date]) = 7
    )
    THEN 0 ELSE 1
    END                                     AS [IsWeekDay]
FROM [DateListForRange] 
('2014-01-01','2014-01-31')

Если я сделаю то же самое для фискальных данных, в каждом случае будет достаточно частых повторений, можно было бы избежать использования оператора и, возможно, перекрестного применения TVF по списку дат.

Обратите внимание, что я использую SQL Server 2008, поэтому многие новые функции даты минимальны.

JohnLinux
источник

Ответы:

12

ОБНОВЛЕНИЕ : для более общего примера создания и заполнения календаря или таблицы измерений, смотрите этот совет:

Для конкретного вопроса, вот моя попытка. Я дополню это магией, которую вы используете для определения таких вещей, как Fiscal_MonthNumber и Fiscal_MonthName, потому что на данный момент это единственная неинтуитивная часть вашего вопроса, и это единственная осязаемая информация, которую вы фактически не включили.

«Лучший» (читай: самый эффективный) способ заполнения таблицы календаря, IMHO, - это использование набора, а не цикла. И вы можете сгенерировать этот набор, не вкладывая логику в пользовательские функции, которые на самом деле не приносят вам ничего, кроме инкапсуляции - в противном случае это просто еще один объект для обслуживания. Я расскажу об этом более подробно в этой серии блогов:

Если вы хотите продолжать использовать свою функцию, убедитесь, что она не является табличной функцией с несколькими утверждениями; это не будет эффективным вообще. Вы хотите убедиться, что он встроенный (например, имеет одно RETURNутверждение и не имеет явного @tableобъявления), имеет WITH SCHEMABINDINGи не использует рекурсивные CTE. За пределами функции, вот как я бы это сделал:

CREATE TABLE dbo.DateDimension
(
  [Date]                      DATE PRIMARY KEY,
  [DayOfWeek_Number]          TINYINT,
  [DayOfWeek_Name]            VARCHAR(9),
  [DayOfWeek_ShortName]       VARCHAR(3),
  [Week_Number]               TINYINT,
  [Fiscal_DayOfMonth]         TINYINT,
  [Fiscal_Month_Number]       TINYINT,
  [Fiscal_Month_Name]         VARCHAR(12),
  [Fiscal_Month_ShortName]    VARCHAR(3),
  [Fiscal_Quarter]            TINYINT,     
  [Fiscal_Year]               SMALLINT,
  [Calendar_DayOfMonth]       TINYINT,
  [Calendar_Month Number]     TINYINT,     
  [Calendar_Month_Name]       VARCHAR(9),
  [Calendar_Month_ShortName]  VARCHAR(3),
  [Calendar_Quarter]          TINYINT,
  [Calendar_Year]             SMALLINT, 
  [IsLeapYear]                BIT,
  [IsWeekDay]                 BIT,
  [IsWeekend]                 BIT,
  [IsWorkday]                 BIT,
  [IsHoliday]                 BIT,
  [HolidayName]               VARCHAR(255)
);
-- add indexes, constraints, etc.

Имея эту таблицу, вы можете выполнить одиночную вставку на основе набора данных за столько лет, сколько пожелаете, начиная с любой выбранной даты начала. Просто укажите дату начала и количество лет. Я использую технику «сложенного CTE», чтобы избежать избыточности и выполнить целый ряд вычислений только один раз; выходные столбцы из более ранних CTE затем впоследствии используются в дальнейших вычислениях.

-- these are important:
SET LANGUAGE US_ENGLISH;
SET DATEFIRST 7;

DECLARE @start DATE = '20100101', @years TINYINT = 20;

;WITH src AS
(
  -- you don't need a function for this...
  SELECT TOP (DATEDIFF(DAY, @start, DATEADD(YEAR, @years, @start)))
    d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY s1.number)-1, @start)
   FROM master.dbo.spt_values AS s1
   CROSS JOIN master.dbo.spt_values AS s2
   -- your own numbers table works much better here, but this'll do
),
w AS 
(
  SELECT d, 
    wd      = DATEPART(WEEKDAY,d), 
    wdname  = DATENAME(WEEKDAY,d), 
    wnum    = DATEPART(ISO_WEEK,d),
    qnum    = DATEPART(QUARTER, d),
    y       = YEAR(d),
    m       = MONTH(d),
    mname   = DATENAME(MONTH,d),
    md      = DAY(d)
  FROM src
),
q AS
(
  SELECT *, 
    wdsname   = LEFT(wdname,3),
    msname    = LEFT(mname,3),
    IsWeekday = CASE WHEN wd IN (1,7) THEN 0 ELSE 1 END,
    fq1 = DATEADD(DAY,25,DATEADD(MONTH,2,DATEADD(YEAR,YEAR(d)-1900,0)))
  FROM w
),
q1 AS
(
  SELECT *, 
    -- useless, just inverse of IsWeekday, but okay:
    IsWeekend = CASE WHEN IsWeekday = 1 THEN 0 ELSE 1 END,
    fq = COALESCE(NULLIF(DATEDIFF(QUARTER,DATEADD(DAY,6,fq1),d) 
         + CASE WHEN md >= 26 AND m%3 = 0 THEN 2 ELSE 1 END,0),4)
    FROM q
)
--INSERT dbo.DimWithDateAllPersisted(Date)
SELECT 
  DateKey = d,
  DayOfWeek_Number = wd,
  DayOfWeek_Name = wdname,
  DayOfWeek_ShortName = wdsname,
  Week_Number = wnum,
  -- I'll update these four lines when I have usable info
  Fiscal_DayOfMonth      = 0,--'?magic?',
  Fiscal_Month_Number    = 0,--'?magic?',
  Fiscal_Month_Name      = 0,--'?magic?',
  Fiscal_Month_ShortName = 0,--'?magic?',
  Fiscal_Quarter = fq,
  Fiscal_Year = CASE WHEN fq = 4 AND m < 3 THEN y-1 ELSE y END,
  Calendar_DayOfMonth = md,
  Calendar_Month_Number = m,
  Calendar_Month_Name = mname,
  Calendar_Month_ShortName = msname,
  Calendar_Quarter = qnum,
  Calendar_Year = y,
  IsLeapYear = CASE 
    WHEN (y%4 = 0 AND y%100 != 0) OR (y%400 = 0) THEN 1 ELSE 0 END,
  IsWeekday,
  IsWeekend,
  IsWorkday = CASE WHEN IsWeekday = 1 THEN 1 ELSE 0 END,
  IsHoliday = 0,
  HolidayName = ''
FROM q1;

Теперь у вас все еще есть эти столбцы «выходной» и «рабочий день», с которыми приходится иметь дело - это становится немного более громоздким, но вам нужно обновить эти три столбца с любыми выходными, которые появляются в вашем диапазоне дат. Такие вещи, как Рождество очень просты:

UPDATE dbo.DateDimension
  SET IsWorkday = 0, IsHoliday = 1, HolidayName = 'Christmas'
  WHERE Calendar_Month_Number = 12 AND Calendar_DayOfMonth = 25;

Такие вещи, как Пасха, становятся намного сложнее - я опубликовал здесь некоторые идеи много лет назад .

И, конечно, нерабочие дни вашей компании, которые не имеют абсолютно никакого отношения к праздничным дням и т. Д., Должны обновляться непосредственно вами - у SQL Server не будет встроенного способа узнать календарь вашей компании.

Теперь я намеренно держался подальше от вычисления любого из этих столбцов, потому что вы сказали что-то вроде конечных пользователей previously preferred fields they can drag and drop- я не уверен, что конечные пользователи действительно знают или заботятся, является ли источник столбца реальным столбцом, вычисляемым столбцом или исходит из представления, запроса или функции ...

Предполагая, что вы действительно хотите изучить некоторые из этих столбцов, чтобы упростить обслуживание (и сохранить их, чтобы оплачивать хранилище для скорости запросов), вы можете в этом разобраться. Однако, как предупреждение, некоторые из этих столбцов не могут быть определены как вычисленные и сохраненные, поскольку они недетерминированы. Вот один пример, и как обойти это.

CREATE TABLE dbo.Test
(
  [date] DATE PRIMARY KEY,
  DayOfWeek_Number AS DATEPART(WEEKDAY, [date]) PERSISTED
);

Результаты:

Сообщение 4936, Уровень 16, Состояние 1, Строка 130
Вычисляемый столбец 'DayOfWeek_Number' в таблице 'Тест' не может быть сохранен, поскольку столбец недетерминирован.

Причина, по которой это не может быть сохранено, заключается в том, что многие функции, связанные с датами, полагаются на настройки сеанса пользователя, например DATEFIRST. SQL Server не может сохранить вышеприведенный столбец, потому что он DATEPART(WEEKDAYдолжен давать разные результаты - при одинаковых данных - для двух разных пользователей, у которых разные DATEFIRSTпараметры.

Тогда вы можете проявить смекалку и сказать: ну, я могу установить это число дней, по модулю 7, смещение от того дня, который я знаю, как субботу (скажем, '2000-01-01'). Итак, вы попробуйте:

CREATE TABLE dbo.Test
(
  [date] DATE PRIMARY KEY,
  DayOfWeek_Number AS 
    COALESCE(NULLIF(DATEDIFF(DAY,'20000101',[date])%7,0),7) PERSISTED
);

Но та же ошибка.

Вместо того чтобы использовать неявное преобразование из строкового литерала, который представляет время даты в однозначном (для нас, но не в SQL Server) формате, мы можем использовать количество дней между «нулевой датой» (1900-01-01) и та дата, которую мы знаем, - суббота (2000-01-01). Если мы будем использовать здесь целое число для представления разницы в днях, SQL Server не сможет пожаловаться, потому что нет способа неверно истолковать это число. Так что это работает:

-- SELECT DATEDIFF(DAY, 0, '20000101');  -- 36524

CREATE TABLE dbo.Test
(
  [date] DATE PRIMARY KEY,
  DayOfWeek_Number AS 
    COALESCE(NULLIF(DATEDIFF(DAY,36524,[date])%7,0),7) PERSISTED
    -----------------------------^^^^^  only change
);

Успех!

Если вы заинтересованы в поиске вычисляемых столбцов для некоторых из этих вычислений, дайте мне знать.

Да, и еще одна вещь: я не знаю, почему вы когда-нибудь почистили бы этот стол и заново заполнили его с нуля. Сколько из этих вещей изменится? Собираетесь ли вы постоянно менять свой финансовый год? Изменить, как вы хотите записать март? Установить свою неделю, чтобы начать в понедельник одну неделю и четверг в следующую? Это действительно должна быть таблица «один раз в сборку», а затем вы вносите небольшие изменения (например, обновляете отдельные строки новой / измененной информацией о праздниках).

Аарон Бертран
источник