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


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,因此许多较新的日期功能很少。

Answers:


12

UPDATE:有关创建和填充日历或尺寸表的更一般的示例,请参见以下提示:

对于眼前的具体问题,这是我的尝试。我将使用您用来确定诸如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.

有了该表之后,您可以从选择的任何开始日期开始执行一次基于集合的,尽可能多的数据插入。只需指定开始日期和年数即可。我使用“堆叠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
表'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
);

但是,同样的错误。

可以使用从“零日期”(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
);

成功!

如果您对这些计算中的某些计算列感兴趣,请告诉我。

哦,还有最后一件事:我不知道您为什么要清理这张桌子,然后从头开始重新填充它。这些事情中有多少会发生变化?您是否要不断更改会计年度?更改三月的拼写方式?将您的一周设置为在一个星期的星期一开始,在下一个星期的星期四开始?这确实应该是一次构建一次的表,然后进行一些细微的调整(例如使用新的/更改的假日信息更新各个行)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.