Melhor abordagem para preencher a tabela de dimensões de data

8

Estou procurando preencher uma tabela de dimensão de data em um banco de dados do SQL Server 2008. Os campos na tabela são os seguintes:

[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)

Eu escrevi uma função DateListInRange (D1, D2) que retorna todas as datas entre duas datas de parâmetro D1 e D2, inclusive.

ie os parâmetros '2014-01-01' e '2014-01-03' retornariam:

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

Desejo preencher a tabela DATE_DIM para todas as datas dentro de um intervalo, ou seja, 01-01-2010 a 01-01-2020. A maioria dos campos pode ser preenchida com as funções SQL 2008 DATEPART, DATENAME e YEAR.

Os dados fiscais contêm um pouco mais de lógica, alguns dos quais dependem um do outro. Por exemplo: Trimestre fiscal 1 -> O mês fiscal deve ser 1, 2 ou 3 Trimestre fiscal 2 -> O mês fiscal deve ser 4, 5 ou 6

Posso escrever facilmente uma função com valor de tabela que aceita uma data específica e, em seguida, gera todos os dados fiscais ou TODOS os campos. Então, eu precisaria apenas dessa função para executar em cada linha da função DateListInRange.

Não estou muito preocupado com a velocidade, pois isso só precisará ser preenchido algumas vezes por ano quando a tabela de feriados for alterada.

Qual é a melhor maneira de escrever isso no SQL?

Atualmente é assim:

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')

Se eu fizer o mesmo com os dados fiscais, haverá uma certa repetição em cada caso. A instrução poderia ser evitada usando uma função e talvez a aplicação cruzada do TVF na lista de datas.

Observe que estou usando o SQL Server 2008, portanto, muitas funcionalidades de data mais recentes são mínimas.

JohnLinux
fonte

Respostas:

12

UPDATE : para obter um exemplo mais genérico de criação e preenchimento de um calendário ou tabela de dimensões, consulte esta dica:

Para a pergunta específica em questão, aqui está minha tentativa. Vou atualizar isso com a mágica que você usa para determinar coisas como Fiscal_MonthNumber e Fiscal_MonthName, porque agora elas são a única parte não intuitiva da sua pergunta e são as únicas informações tangíveis que você realmente não incluiu.

A maneira "melhor" (leia-se: mais eficiente) de preencher uma tabela de calendário, IMHO, é usar um conjunto, em vez de um loop. E você pode gerar esse conjunto sem enterrar a lógica em funções definidas pelo usuário, que realmente não ganham nada além de encapsulamento - caso contrário, é apenas outro objeto a ser mantido. Eu falo sobre isso com muito mais detalhes nesta série de blogs:

Se você quiser continuar usando sua função, verifique se ela não é uma função com valor de tabela com várias instruções; isso não será nada eficiente. Você quer ter certeza de que está alinhado (por exemplo, possui uma única RETURNdeclaração e nenhuma @tabledeclaração explícita ), possui WITH SCHEMABINDINGe não usa CTEs recursivas. Fora de uma função, aqui está como eu faria isso:

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.

Com a tabela no lugar, você pode executar uma inserção única e baseada em conjunto de quantos anos de dados desejar a partir da data de início escolhida. Basta especificar a data de início e o número de anos. Eu uso uma técnica de "CTE empilhado" para evitar redundância e realizar apenas uma série de cálculos uma vez; as colunas de saída dos CTEs anteriores são usadas posteriormente posteriormente em cálculos adicionais.

-- 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;

Agora, você ainda tem essas colunas "feriado" e "dia útil" para resolver - isso fica um pouco mais complicado, mas você precisa atualizar essas três colunas com os feriados que aparecerem no período. Coisas como o dia de Natal são realmente fáceis:

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

Coisas como a Páscoa ficam muito mais complicadas - escrevi algumas idéias aqui há muitos anos .

E, é claro, a sua empresa, que não tem dias úteis, que não tem absolutamente nada a ver com feriados etc. deve ser atualizada diretamente por você - o SQL Server não terá uma maneira integrada de conhecer o calendário da sua empresa.

Agora, propositadamente, fiquei longe de calcular qualquer uma dessas colunas, porque você disse algo como os usuários finais têm previously preferred fields they can drag and drop- não tenho certeza se eles realmente sabem ou se importam se a origem de uma coluna é uma coluna real, uma coluna computada , ou vem de uma visualização, consulta ou função ...

Supondo que você faz querer olhar para computar algumas dessas colunas para facilitar na sua manutenção (e persistem-los para o armazenamento de pagamento para a velocidade de consulta), você pode olhar para isso. No entanto, apenas como um aviso, algumas dessas colunas não podem ser definidas como computadas e persistidas porque são não determinísticas. Aqui está um exemplo e como contornar isso.

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

Resultados:

Msg 4936, Nível 16, Estado 1, Linha 130
A coluna computada 'DayOfWeek_Number' na tabela 'Teste' não pode ser mantida porque a coluna é não determinística.

A razão pela qual isso não pode ser mantido é porque muitas funções relacionadas à data dependem das configurações da sessão do usuário, como DATEFIRST. O SQL Server não pode persistir na coluna acima porque DATEPART(WEEKDAYdeve fornecer resultados diferentes - com os mesmos dados - para dois usuários diferentes que possuem DATEFIRSTconfigurações diferentes .

Então você pode ficar esperto e dizer: bem, eu posso configurá-lo para o número de dias, módulo 7, compensado a partir de um dia que sei ser um sábado (digamos '2000-01-01'). Então você tenta:

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

Mas, mesmo erro.

Em vez de usar uma conversão implícita de um literal de seqüência de caracteres que representa uma data e hora em um formato inequívoco (para nós, mas não para o SQL Server), podemos usar o número de dias entre a "data zero" (1900-01-01) e essa data que sabemos é sábado (01-01-200 2000). Se usarmos um número inteiro aqui para representar a diferença em dias, o SQL Server não poderá reclamar, porque não há como interpretar mal esse número. Então, isso funciona:

-- 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
);

Sucesso!

Se você estiver interessado em buscar colunas computadas para alguns desses cálculos, entre em contato.

Ah, e uma última coisa: não sei por que você esfregaria esta tabela e a preencheria novamente do zero. Quantas dessas coisas vão mudar? Você vai alterar seu ano fiscal constantemente? Mude como você quer soletrar março? Defina sua semana para começar na segunda-feira uma semana e quinta-feira na próxima? Essa realmente deve ser uma tabela de construir uma vez e, em seguida, você faz pequenos ajustes (como atualizar linhas individuais com informações de férias novas / alteradas).

Aaron Bertrand
fonte