El mejor enfoque para completar la tabla de dimensiones de fecha

8

Estoy buscando completar una tabla de dimensiones de fecha en una base de datos SQL Server 2008. Los campos en la tabla son los siguientes:

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

He escrito una función DateListInRange (D1, D2) que devuelve todas las fechas entre dos fechas de parámetros D1 y D2 inclusive.

es decir. los parámetros '2014-01-01' y '2014-01-03' devolverían:

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

Quiero llenar la tabla DATE_DIM para todas las fechas dentro de un rango, es decir, 2010-01-01 a 2020-01-01. La mayoría de los campos se pueden completar con las funciones SQL 2008 DATEPART, DATENAME y YEAR.

Los datos fiscales contienen un poco más de lógica, algunos de los cuales dependen unos de otros. Por ejemplo: Trimestre fiscal 1 -> El mes fiscal debe ser 1, 2 o 3 Trimestre fiscal 2 -> El mes fiscal debe ser 4, 5 o 6

Puedo escribir fácilmente una función con valores de tabla que acepte una fecha específica, y luego muestre todos los datos fiscales, o TODOS los campos, incluso. Entonces solo necesitaría esta función para ejecutarse en cada fila de la función DateListInRange.

No estoy muy preocupado por la velocidad, ya que solo tendrá que rellenarse algunas veces al año cuando se modifique la tabla de vacaciones.

¿Cuál es la mejor manera de escribir esto en SQL?

Actualmente es así:

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

Si hago lo mismo con los datos fiscales, habrá bastante repetición en cada declaración de caso, podría evitarse utilizando una función y tal vez cruzar la aplicación de TVF en la lista de fechas.

Tenga en cuenta que estoy usando SQL Server 2008, por lo que una gran cantidad de nuevas funciones de fecha es mínima.

JohnLinux
fuente

Respuestas:

12

ACTUALIZACIÓN : para obtener un ejemplo más genérico de crear y completar un calendario o tabla de dimensiones, consulte este consejo:

Para la pregunta específica en cuestión, aquí está mi intento. Actualizaré esto con la magia que usas para determinar cosas como Fiscal_MonthNumber y Fiscal_MonthName, porque en este momento son la única parte no intuitiva de tu pregunta, y es la única información tangible que realmente no incluiste.

La "mejor" (léase: la más eficiente) forma de llenar una tabla de calendario, en mi humilde opinión, es usar un conjunto, en lugar de un bucle. Y puede generar este conjunto sin enterrar la lógica en funciones definidas por el usuario, que realmente no le dan más que encapsulación; de lo contrario, es solo otro objeto para mantener. Hablo de esto con mucho más detalle en esta serie de blogs:

Si desea seguir usando su función, asegúrese de que no sea una función con valores de tabla de varias instrucciones; eso no va a ser eficiente en absoluto. Desea asegurarse de que esté en línea (por ejemplo, que tenga una sola RETURNdeclaración y no una @tabledeclaración explícita ), que tenga WITH SCHEMABINDINGy no use CTE recursivos. Fuera de una función, así es como lo haría:

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.

Con la tabla en su lugar, puede realizar una inserción única basada en conjuntos de tantos años de datos como desee desde la fecha de inicio que elija. Simplemente especifique la fecha de inicio y el número de años. Utilizo una técnica de "CTE apilado" para evitar la redundancia y solo realizo una gran cantidad de cálculos una vez; Las columnas de salida de los CTE anteriores se utilizan posteriormente en otros cálculos posteriores.

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

Ahora, todavía tiene que lidiar con estas columnas de "feriado" y "día laborable"; esto se vuelve un poco más engorroso, pero necesita actualizar esas tres columnas con cualquier feriado que aparezca en su rango de fechas. Cosas como el día de Navidad son realmente fáciles:

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

Cosas como la Pascua se vuelven mucho más complicadas: he publicado algunas ideas aquí hace muchos años .

Y, por supuesto, los días no laborables de su empresa que no tienen absolutamente nada que ver con los días festivos, etc., deben ser actualizados directamente por usted: SQL Server no tendrá una forma integrada de conocer el calendario de su empresa.

Ahora, deliberadamente me mantuve alejado de calcular cualquiera de estas columnas, porque dijiste algo como lo han dicho los usuarios finales previously preferred fields they can drag and drop: no estoy seguro de si los usuarios finales realmente saben o les importa si la fuente de una columna es una columna real, una columna calculada , o proviene de una vista, consulta o función ...

Asumiendo que hacer que desee ver en el cálculo de algunas de estas columnas para facilitar en su mantenimiento (y persistir a almacenamiento de pago de la velocidad de consulta), se puede ver en eso. Sin embargo, solo como advertencia, algunas de estas columnas no se pueden definir como calculadas y persistentes porque no son deterministas. Aquí hay un ejemplo y cómo solucionarlo.

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

Resultados:

Msg 4936, Nivel 16, Estado 1, Línea 130
La columna calculada 'DayOfWeek_Number' en la tabla 'Prueba' no puede persistir porque la columna no es determinista.

La razón por la que esto no puede persistir es porque muchas funciones relacionadas con la fecha dependen de la configuración de la sesión del usuario, como DATEFIRST. SQL Server no puede persistir en la columna anterior porque DATEPART(WEEKDAYdebería dar resultados diferentes, dados los mismos datos, para dos usuarios diferentes que tienen DATEFIRSTconfiguraciones diferentes .

Entonces puede ser inteligente y decir, bueno, puedo configurarlo para que sea el número de días, módulo 7, compensado de algún día que sé que es un sábado (digamos '2000-01-01'). Entonces intentas:

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

Pero, el mismo error.

En lugar de usar una conversión implícita de un literal de cadena que represente una fecha y hora en un formato inequívoco (para nosotros, pero no SQL Server), podemos usar el número de días entre la "fecha cero" (1900-01-01) y esa fecha sabemos que es un sábado (2000-01-01). Si usamos un número entero aquí para representar la diferencia en días, SQL Server no puede quejarse, porque no hay forma de malinterpretar ese número. Entonces esto 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
);

¡Éxito!

Si está interesado en buscar columnas calculadas para algunos de estos cálculos, hágamelo saber.

Ah, y una última cosa: no sé por qué alguna vez fregarías esta tabla y la volverías a llenar desde cero. ¿Cuántas de estas cosas van a cambiar? ¿Vas a alterar tu año fiscal constantemente? ¿Cambiar cómo quieres deletrear marzo? ¿Establecer su semana para comenzar el lunes una semana y el jueves la próxima? Esto realmente debería ser una tabla de compilación, y luego realiza pequeños ajustes (como actualizar filas individuales con información de vacaciones nueva / modificada).

Aaron Bertrand
fuente