填充日期维度表的最佳方法

Joh*_*nux 8 sql-server-2008 data-warehouse business-intelligence dimension star-schema

我希望在 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)
Run Code Online (Sandbox Code Playgroud)

我编写了一个函数 DateListInRange(D1,D2),它返回两个参数日期 D1 和 D2 之间的所有日期。

IE。参数“2014-01-01”和“2014-01-03”将返回:

2014-01-01
2014-01-02
2014-01-03
Run Code Online (Sandbox Code Playgroud)

我想为一个范围内的所有日期填充 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')
Run Code Online (Sandbox Code Playgroud)

如果我对财政数据做同样的事情,那么在每个 case 语句中都会有相当多的重复,使用函数可以避免,并且可能在日期列表上交叉应用 TVF。

请注意,我使用的是 SQL Server 2008,因此很多较新的日期功能很少。

Aar*_*and 13

更新:有关创建和填充日历或维度表的更通用示例,请参阅此提示:

对于手头的具体问题,这是我的尝试。我将使用您用来确定诸如 Fiscal_MonthNumber 和 Fiscal_MonthName 之类的魔法来更新它,因为现在它们是您问题中唯一不直观的部分,而且它是您实际上没有包含的唯一有形信息。

填充日历表的“最佳”(读取:最有效)方法恕我直言,是使用集合而不是循环。并且您可以生成这个集合,而无需将逻辑埋入用户定义的函数中,这实际上除了封装之外没有任何好处 - 否则它只是另一个需要维护的对象。我在本博客系列中更详细地讨论了这一点:

如果您想继续使用您的函数,请确保它不是多语句表值函数;这根本不会有效率。您要确保它是内联的(例如,只有一条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.
Run Code Online (Sandbox Code Playgroud)

有了该表,您可以从您选择的任何开始日期对任意多年的数据执行单个、基于集合的插入。只需指定开始日期和年数。我使用“堆叠 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;
Run Code Online (Sandbox Code Playgroud)

现在,您仍然需要处理这些“假期”和“工作日”列 - 这会变得有点麻烦,但是您需要使用日期范围内出现的任何假期更新这三列。像圣诞节这样的事情真的很简单:

UPDATE dbo.DateDimension
  SET IsWorkday = 0, IsHoliday = 1, HolidayName = 'Christmas'
  WHERE Calendar_Month_Number = 12 AND Calendar_DayOfMonth = 25;
Run Code Online (Sandbox Code Playgroud)

像复活节这样的事情变得更加棘手 -很多年前我在这里写过一些想法

当然,与公共假期等完全无关的公司非工作日必须由您直接更新 - 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
);
Run Code Online (Sandbox Code Playgroud)

结果:

消息 4936,级别 16,状态 1,第 130 行
无法保留表“Test”中的计算列“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
);
Run Code Online (Sandbox Code Playgroud)

但是,同样的错误。

我们可以使用“零日期”(1900-01-01) 和“零日期”(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
);
Run Code Online (Sandbox Code Playgroud)

成功!

如果您有兴趣为其中一些计算使用计算列,请告诉我。

哦,还有最后一件事:我不知道你为什么要清理这张表并从头开始重新填充它。这些事情中有多少会改变?你会经常改变你的财政年度吗?更改您想如何拼写三月?将您的一周设置为每周一开始,下周四开始?这真的应该是一个构建一次的表,然后你做一些小的调整(比如用新的/更改的假期信息更新各个行)。