日期表和日历计算

时间:2018-02-04 18:02:45

标签: sql-server date datetime calendar

这里关于SO的问题经常围绕着涉及SQL日期的计算。在创建需要将数据(事件或事务)隔离到某个时段的报表时,它可能特别相关,可能以某种方式对其进行汇总,然后以可理解的方式呈现它。另一个常见的情况是,您需要以某种方式及时预测或推进工作 - 例如,计算未来的最后期限或监控合规性,甚至只是评估查询是否会在未来的日期执行应有的操作。

即使是可以直接表达的时间段,例如"查询运行时的最后2个完整月份,#34;可能变得非常复杂,无法计算为查询中的单个表达式如果天真地接近,将涉及代码性差,难以概括和修改以及难以验证的代码。或者是凌乱,脆弱和功能失调。

最强大的解决方案的基础是使用"日期表"。但这不是(必然)只是一个日期列表,但也包含有关每个日期的各种事实和属性。所有数据库包都涉及一组标准日期函数,这些日期函数从日期(例如每月的某天)中提取各种属性,但这些属性远非全面。

所以,接下来回答一下,我要提出的问题是" 我们如何创建一个基本日期表,并将其操作为一个简单的日历"

1 个答案:

答案 0 :(得分:1)

我将从SQL Server的角度来看待这个答案。创建和使用日期表有多种特定方法。它们可以作为查询的一部分动态生成,或者生成一个,它们可以作为数据库中的持久表进行存储和索引。过去,存储过程中的命令性代码可能已用于准备存储的日期表。现在,我们可以使用递归查询来获取连续日期的基本列表,这最好归入它自己的函数:

CREATE FUNCTION [dbo].[generate_series_datetime2] 
(   
     @series_start  DATETIME2(0) = {d '2000-01-01'}
    ,@series_max    DATETIME2(0) = {d '2029-12-31'}
    ,@stepping_days INT = 1
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN 
(
    WITH internal_generate_series_datetime2(datetime2_value) AS
    (
        SELECT @series_start
        UNION ALL
        SELECT DATEADD(DAY, @stepping_days, datetime2_value)
        FROM internal_generate_series_datetime2
        WHERE DATEADD(DAY, @stepping_days, datetime2_value) <= @series_max
    )
    SELECT datetime2_value
    FROM internal_generate_series_datetime2
)

例如这样称呼:

SELECT * FROM generate_series_datetime2('2001-01-01','2001-01-03',default) OPTION (MAXRECURSION 0)

datetime2_value
---------------------------
2001-01-01 00:00:00
2001-01-02 00:00:00
2001-01-03 00:00:00

但是,当我们能够隔离每个日期的属性并单独解决它们时,日期表的真正威力就来了。任何类型的日历都包含各种周期。天,周,月,年。唯一的问题是,任何事物都不会以任何简单的常规方式相互关联。公历的唯一全部重复是每400年一次,或146,097天 - 这比大多数现代国家已经存在的时间更长,更不用说企业了。

常见的公民和商业用途的主要周期是日,周(7天),两周(2周),农历(4周),月,季(3个月)和年(12个月)。在一些班次模式中,八周或八天的周很常见。当然,为特定目的设计了各种各样的任意循环,通常包含它们自己的不规则性和偶然的例外,它们不能清楚地映射到这些已建立的循环中的任何一个。

周,四周和月球都映射到固定的天数。宿舍和年份都映射到固定的月份。但是,一个月内没有一致的天数 - 也就是说,以日计价的周期不容易映射到以月计价的周期,这是疼痛发生的常见地方。

然后,这些周期的固定方式存在问题&#34;锚定&#34;。有一个熟悉的格里高利年,从1月1日开始,ISO年份(从包含格里高利年的第一个星期四的星期一开始),大多数企业有一个财政年度不同于这些(和业务对于商业而言,大多数州都有一个历史悠久的纳税年度,这与现在认为合理的任何东西都不同,但是大型官僚机构的家具已经安排在其中。

在英国,税收年度从6月4日开始,税收月份将在随后的每个6日结束,并且适用的税收周将在该纳税年度的4月6日的任何工作日延期。税收堡垒和月球根据其第一个组成周的周数进行编号,但偏差分别为1或3!有明确的逻辑和理由,但不是立即显而易见的。

我只会在这个答案中解释格里高利和ISO年份,但我也会提到一个我将称之为“罗马年”的概念。&#34; - 也就是说,除了格里高利逻辑之外的一年,但是在1月3日而不是1月1日翻身。在我看到使用这种方法的地方,我不确定它是否有任何一致认可的名称。这样做的好处是,一年中的闰日(29日至2月)总是在年底,因此每年都会编号为#34;从一年到下一年是一致的。因此,例如,圣诞节总是落在罗马年的第300天&#34; - 所以这一天可以在一年内作为单一的一致值来解决。

日期周期也常常向下移动到锚点而不是向上 - 例如,每个月的第二天到最后一天&#34;或者#34;最后一天年度&#34;。并且通常希望根据与参考日期的偏移来解决日期 - 例如,说“&#34;三个月前&#34;即使它跨越一个循环的边界(而不是将其表示为-1年和+9个月前,将2月转换为前一个11月),也可以使用此工作。另一种共同解决日期的方式是&#34;该月的第1个和第3个星期四&#34;。以上述任何方式处理日期是从SQL Sever提供的标准函数中删除的几个步骤。

最后,我将采用我称之为&#34; heptrig窗口&#34;的概念,隐含地用于常见的日历表示,而不是明确地用于寻址日期。注意到编程中的命名是最困难的问题之一,我还没有找到这个概念的明确名称,所以我设计了这个。

几乎所有用于人类使用的日历都涉及设定一个固定的星期网格,然后将工作日覆盖到&#34;月周&#34;每月4至6个星期(取决于月份的长短和开始的工作日),或进入37天的窗口(5周和2天,再次从左侧偏移,取决于哪个工作日这个月开始)。前者适用于&#34;月计划者&#34;输入一周宽的日历,然后填写当月的日期。后者适用于&#34;年计划者&#34;类型日历,这是37天宽,所有工作日都填满。

在实践中,对于月份计划者类型的日历,其中一个月可以覆盖最多42个单元格的正方形网格,在第6个月中只能占用周一或周二位置(基于31天的月份开始)在太阳上),因此在这样的网格中只使用了37个不同的单元格(如年度计划者类型的日历)。 &#34; Heptrig&#34;从&#34; heptatrigesimal&#34;浓缩意思是基础-37,并且似乎从其他任何东西中清楚地说明了这个概念。

但是对日历问题进行了充分的讨论,现在讨论可以解决问题的代码。

我们希望不仅能够为日期表生成属性,还能为我们关注的任何其他表生成属性,从而允许我们加入公共属性。所以我设计了一个函数,它可以交叉应用于任何后续连接两侧的表。

我已经以这样的方式编写了函数,使其成为表值(允许通过单个交叉应用返回多个列),确定性,可内联,并且仅使用标量表达式。 Inlinability允许查询引擎避免评估它可以识别的任何未在最终输出中使用的列 - 因此当函数仅用于它能够生成的一个或两个列时,我们不必担心效率低下。

确定性意味着数据库将其识别为一个函数,对于该函数,我们总是为相同的给定参数获得相同的值(即,它的结果将根据它的次数而变化。例如,评估函数或在更大的查询中评估函数的顺序 - 这可能有助于查询优化器,但遗憾的是,需要在函数的中间块中重复列名作为{的要求{1}}属性。我试图采用一致的方法来确定列的命名方式,并使用尽可能短的缩写来保持清晰度。

以下是计算给定日期属性的函数的代码:

WITH SCHEMABINDING

找出命名逻辑应该不会太困难。 CREATE FUNCTION [dbo].[datetime2_params_fxn] ( @datetime2 DATETIME2(0) ) RETURNS TABLE WITH SCHEMABINDING AS RETURN ( WITH block_a AS ( SELECT @datetime2 AS datetime2_value ,CONVERT(DATETIME2(0),'0001-01-01',102) AS day_one_of_grego_cal ) ,block_b AS ( SELECT datetime2_value ,(DATEDIFF(MM, day_one_of_grego_cal, datetime2_value) + 1) AS mth_of_grego_cal ,(DATEDIFF(DD, day_one_of_grego_cal, datetime2_value) + 1) AS day_of_grego_cal FROM block_a ) ,block_c AS ( SELECT datetime2_value ,mth_of_grego_cal ,day_of_grego_cal ,DATEPART(YEAR, datetime2_value) AS yr_of_cal ,DATEPART(QUARTER, datetime2_value) AS qtr_of_yr ,DATEPART(MONTH, datetime2_value) AS mth_of_yr ,DATEPART(DAYOFYEAR, datetime2_value) AS day_of_yr ,DATEPART(DAY, datetime2_value) AS day_of_mth ,DATEPART(DAY, EOMONTH(datetime2_value)) AS num_days_of_mth ,DATEPART(ISO_WEEK, datetime2_value) AS wk_of_iso_yr ,(mth_of_grego_cal - (3 - 1)) AS mth_of_roman_cal FROM block_b ) ,block_d AS ( SELECT datetime2_value ,yr_of_cal ,qtr_of_yr ,mth_of_yr ,day_of_yr ,day_of_mth ,(((mth_of_grego_cal - 1) / 3) + 1) AS qtr_of_cal ,mth_of_grego_cal AS mth_of_cal ,(((day_of_grego_cal - 1) / 14) + 1) AS fort_of_cal ,(((day_of_grego_cal - 1) / 7) + 1) AS wk_of_cal ,(((day_of_grego_cal - 1) % 14) + 1) AS day_of_fort ,(((day_of_grego_cal - 1) % 7) + 1) AS day_of_wk ,day_of_grego_cal AS day_of_cal ,DATEPART(DAYOFYEAR, DATEFROMPARTS(yr_of_cal,12,31)) AS num_days_of_yr ,num_days_of_mth ,wk_of_iso_yr ,(((mth_of_roman_cal - 1) / 12) + 1) AS yr_of_roman_cal FROM block_c ) ,block_e AS ( SELECT datetime2_value ,yr_of_cal ,qtr_of_yr ,mth_of_yr ,day_of_yr ,day_of_mth ,qtr_of_cal ,mth_of_cal ,fort_of_cal ,wk_of_cal ,day_of_fort ,day_of_wk ,day_of_cal ,(((day_of_mth - 1) / 7) + 1) AS wkday_of_mth --i.e. first x-day of the month ,(7 - (((day_of_mth - day_of_wk) + 6) % 7)) AS first_wkday_of_mth ,num_days_of_yr ,num_days_of_mth ,(num_days_of_yr - 365) AS is_leap_year ,IIF(day_of_wk > 5, 1, 0) AS is_weekend ,IIF(day_of_wk <= 5, 1, 0) AS is_midweek ,DATEPART(YEAR, DATEADD(DAY, (4 - day_of_wk), datetime2_value)) AS yr_of_iso_cal ,wk_of_iso_yr ,yr_of_roman_cal FROM block_d ) SELECT yr_of_cal ,qtr_of_yr ,mth_of_yr ,day_of_yr ,day_of_mth ,qtr_of_cal ,mth_of_cal ,fort_of_cal ,wk_of_cal ,day_of_fort ,day_of_wk ,day_of_cal ,wkday_of_mth ,((num_days_of_yr + 1) - day_of_yr) AS day_from_end_of_yr ,((num_days_of_mth + 1) - day_of_mth) AS day_from_end_of_mth ,num_days_of_yr ,num_days_of_mth ,is_leap_year ,is_weekend ,is_midweek ,yr_of_iso_cal ,wk_of_iso_yr ,yr_of_roman_cal ,((((day_of_yr + 306) - 1) % (365 + is_leap_year)) + 1) AS day_of_roman_yr ,(((first_wkday_of_mth + day_of_mth - 2) / 7) + 1) AS wk_of_heptrig_wnd ,(day_of_mth + first_wkday_of_mth - 1) AS day_of_heptrig_wnd FROM block_e ) 列在整个公历中连续编号(即它们不会在任何边界处重置),因此可以从参考日期轻松表示偏移。有几个值只是中间计算,不会输出。

以下代码演示了创建一个日历,该日历在每个月的第1个和第3个星期四突出显示(_of_cal}:

*

结果如此:

WITH 
dynamic_date_table AS
(
    SELECT *
    FROM generate_series_datetime2('2000-01-01','2030-12-31',1)
    CROSS APPLY datetime2_params_fxn(datetime2_value)
)
,marked_days AS
(
    SELECT
        *
        ,IIF(day_of_wk = 4 /*thursday*/ AND wkday_of_mth IN (1,3) /*1st and 3rd occurences in mth*/, 1, 0) AS is_marked_day
    FROM dynamic_date_table
)
,pivot_source AS
(
    SELECT
        yr_of_cal
        ,mth_of_yr
        ,wk_of_heptrig_wnd
        ,day_of_wk  --column header
        ,STR(day_of_mth,2) + IIF(is_marked_day = 1, '*', ' ')   AS cell_value
    FROM marked_days
    UNION ALL
    SELECT 
        yr_of_cal
        ,mth_of_yr
        --where end of the month falls on a sunday, add an extra empty heptrig week 
        --row below the last actual week for presentation purposes - e.g. 2018-09-30
        ,(wk_of_heptrig_wnd + 1)
        ,NULL
        ,NULL
    FROM marked_days
    WHERE day_of_wk = 7 --sunday
    AND day_from_end_of_mth = 1 --last day of month
)
,pivoted_data AS
(
    SELECT * FROM pivot_source PIVOT (
        MAX(cell_value)
        FOR day_of_wk IN ([1],[2],[3],[4],[5],[6],[7])
    ) AS pivot_result
)
,final_presentation AS
(
    SELECT 
        (STR(yr_of_cal,4) + ' ')    AS [Year]
        ,STR(mth_of_yr,2)           AS [Mth]
        ,STR(wk_of_heptrig_wnd,1)   AS [Hwk]
        ,ISNULL([1], '   ')         AS Mon
        ,ISNULL([2], '   ')         AS Tue
        ,ISNULL([3], '   ')         AS Wed
        ,ISNULL([4], '   ')         AS Thu
        ,ISNULL([5], '   ')         AS Fri
        ,ISNULL([6], '   ')         AS Sat
        ,ISNULL([7], '   ')         AS Sun
    FROM pivoted_data
)
SELECT *
FROM final_presentation
WHERE [Year] IN (2018)
ORDER BY 1,2,3
OPTION (MAXRECURSION 0)

我知道有些读者会对在SQL中包含表示代码感到惊愕,但这仅仅是为了演示。 Year Mth Hwk Mon Tue Wed Thu Fri Sat Sun ----- ---- ---- ---- ---- ---- ---- ---- ---- ---- 2018 1 1 1 2 3 4* 5 6 7 2018 1 2 8 9 10 11 12 13 14 2018 1 3 15 16 17 18* 19 20 21 2018 1 4 22 23 24 25 26 27 28 2018 1 5 29 30 31 2018 2 1 1* 2 3 4 2018 2 2 5 6 7 8 9 10 11 2018 2 3 12 13 14 15* 16 17 18 2018 2 4 19 20 21 22 23 24 25 2018 2 5 26 27 28 [...] 步骤的结果结束了所有非表示逻辑,例如,该步骤的结果可以转移到Excel并使用数据透视表功能产生相同的结果。

在这个主题上还有很多可以说和展示的内容,上面的pivot_source函数被削减并整合了一些更全面的函数(在原始环境中进行了测试),但是我希望这可以作为如何处理各种日历问题的基础。