SQL Server - 把星期一(周一)當作每個星期的開始在一年中求取周數


先感嘆一句!好長時間沒有更新博客了!偶爾看到一句話,覺得被電擊了 - 庸人敗於懶,能人敗於傲!

因此,不能再懶惰了!

今天想寫一個有關計算 Week Number 的函數,剛開始覺得應該很簡單,憑着感覺七寫八寫到最后發現越寫越亂,到最后搞了快兩個小時以為解決了,結果一測還有好多數據不正確。非常有挫敗感!感覺很不服氣,覺得很丟人,跑出去站了會,冷靜下來,重新拿起紙筆認真的分析了一下,連寫到測試快半個小時還是解決了。
 
在 SQL Server 中默認情況下,每周的開始都是從周日開始算起的。但是在國內也有不一樣的要求,比如按照習慣往往要求每周從周一算起。這樣一來之前在數據倉庫中的 Week Number 可能就不准確了,因為可能很多時候都是按默認方式來生成 Week Number 的。
 
這里有三種方式可以解決這個問題:
第一種方式是直接通過 SET DATEFIRST VALUE 來更改重新生成新的 DimDate,然后每次需要單獨計算 Week Number 的時候根據 Date Key 關聯一下就可以了,但這樣就需要不斷 JOIN DimDate,每一條記錄都要 LookUp 一遍,不太好。
第二種方式就是在存儲過程中需要使用到  Week Number 的時候,就先設置一下 SET DATEFIRST 然后在使用 DATEPART() 函數來獲取 Week Number。這種方式需要每次都顯示的  SET DATEFIRST ,不太方便。
第三種方式就是直接寫一個函數,每次調用一下就可以了。
 
關於使用 SET DATEFIRST <VALUE> - <VALUE> 的值從 1 到 7,即周一到周日,默認值是 7。
 
-- The default first date in a week is Sunday, the value is 7
SELECT @@DATEFIRST  

-- Default DATEFIRST is Sunday
SELECT DATENAME(WEEK,'2013-12-31') AS WeekName  -- 53
SELECT DATENAME(WEEK,'2014-01-01') AS WeekName  -- 1
SELECT DATENAME(WEEK,'2014-01-05') AS WeekName  -- 2

-- Change the DATEFIRST to 1, Monday will be the first day of week.
SET DATEFIRST 1 

SELECT @@DATEFIRST  -- 1

-- After change the DATEFIRST to Monday
SELECT DATENAME(WEEK,'2013-12-31') AS WeekName  -- 53
SELECT DATENAME(WEEK,'2014-01-01') AS WeekName  -- 1
SELECT DATENAME(WEEK,'2014-01-05') AS WeekName  -- 1

要注意的是 SET DATEFIRST 只在當前執行中有效,也就說比如新開一個查詢頁面繼續查詢 SELECT @@DATEFIRST 則還是顯示默認值 7。

在創建時間維度的代碼中添加 SET DATEFIRST 1,表示每周以周一開始。

USE BIWORK_SSIS
GO  
SET NOCOUNT ON 

-- 設置每周的起始天為周一
SET DATEFIRST 1

IF OBJECT_ID('DimDateStartWithMonday','U') IS NOT NULL
DROP TABLE DimDateStartWithMonday
GO

CREATE TABLE DimDateStartWithMonday
(
    DateKey INT PRIMARY KEY,
    FullDate DATE NOT NULL,
    [DateName] NVARCHAR(20),
    DayNumberOfWeek TINYINT NOT NULL,
    DayNameOfWeek NVARCHAR(10) NOT NULL,
    DayNumberOfMonth TINYINT NOT NULL,
    DayNumberOfYear SMALLINT NOT NULL, 
    WeekNumberOfYear TINYINT NOT NULL,
    EnglishMonthName NVARCHAR(10) NOT NULL,
    MonthNumberOfYear TINYINT NOT NULL,
    CalendarQuarter TINYINT NOT NULL,
    CalendarSemester TINYINT NOT NULL,
    CalendarYear SMALLINT NOT NULL 
)

DECLARE @StartDate DATETIME
DECLARE @EndDate DATETIME

SELECT @StartDate = '2001-01-01',
       @EndDate = '2035-12-31'

WHILE (@StartDate <= @EndDate)
BEGIN
    INSERT INTO DimDateStartWithMonday 
    (
        DateKey,
        FullDate,
        [DateName],
        DayNumberOfWeek,
        DayNameOfWeek,
        DayNumberOfMonth,
        DayNumberOfYear, 
        WeekNumberOfYear,
        EnglishMonthName, 
        MonthNumberOfYear,
        CalendarQuarter,
        CalendarSemester,
        CalendarYear 
    )
    SELECT CAST(CONVERT(VARCHAR(8),@StartDate,112) AS INT) AS DateKey,
           CONVERT(VARCHAR(10), @StartDate,20) AS FullDate,
           CONVERT(VARCHAR(20), @StartDate,106) AS [DateName],
           DATEPART(DW,@StartDate) AS DayNumberOfWeek,
           DATENAME(DW,@StartDate) AS DayNameOfWeek,
           DATENAME(DD,@StartDate) AS [DayOfMonth],
           DATENAME(DY,@StartDate) AS [DayOfYear],  
           DATEPART(WW,@StartDate) AS WeekNumberOfYear,
           DATENAME(MM,@StartDate) AS EnglishMonthName,
           DATEPART(MM,@StartDate) AS MonthNumberOfYear,
           DATEPART(QQ,@StartDate) AS CalendarQuarter,
           CASE WHEN DATEPART(MM,@StartDate) BETWEEN 1 AND 6
                    THEN 1
                ELSE 2
           END AS CalendarSemester,
           DATEPART(YY,@StartDate) AS CalendarYear 
            
    SET @StartDate = @StartDate + 1
END
GO

最后是函數,這個函數麻煩的地方就是要考慮周日的情況。默認情況下,周日是每個星期的第一天,但是這里改成了周一是每周的第一天,邏輯上就會復雜很多。

比如,2012-01-01 是周日,2012-01-02 是周一。按默認情況,這兩天的 Week Number 都是 1,但是這里需要把 2012-01-02 的 Week Number 變成 2。

比如,2011-01-01 是周六,2012-01-02 是周日。按默認情況,周六的 Week Number 是 1, 周日的是 2。但是這里需要把周六和周日的都變成 1, 周一的變成 2。

除此之外,還要考慮之后的每一個周日與周一的交替情況。

USE BIWORK_SSIS
GO

IF OBJECT_ID('ETLWORK_GETWEEKNUMBER','FN') IS NOT NULL
DROP FUNCTION ETLWORK_GETWEEKNUMBER
GO
 
CREATE FUNCTION ETLWORK_GETWEEKNUMBER(@DATE DATETIME)  
RETURNS INTEGER
AS
BEGIN 

    DECLARE @FIRST_DATE_OF_YEAR DATETIME = DATEADD(YYYY,DATEDIFF(YYYY,0,@DATE),0) 
    -- DECLARE @MONDAY_OF_WEEK DATETIME = DATEADD(WK,DATEDIFF(WK,0,@DATE),0) 
    -- DECLARE @PREVIOUS_DATE DATETIME = DATEADD(DAY,-1,@DATE)
    DECLARE @WEEK_NUMBER INTEGER

    -- 如果當前時間是當前年的第一天
    IF @DATE = @FIRST_DATE_OF_YEAR
        SET @WEEK_NUMBER = 1 
    -- 星期天是年第一天的情況
    ELSE IF (DATEPART(WEEKDAY,@DATE) = 1 AND DATEDIFF(DAYOFYEAR,@FIRST_DATE_OF_YEAR,@DATE)/7 + 1 = DATEPART(WEEK,@DATE))  
        SET @WEEK_NUMBER = DATEPART(WEEK,@DATE)  
    -- 星期天不是年第一天的情況
    ELSE IF (DATEPART(WEEKDAY,@DATE) = 1 AND DATEDIFF(DAYOFYEAR,@FIRST_DATE_OF_YEAR,@DATE)/7 + 1 <> DATEPART(WEEK,@DATE)) 
        SET @WEEK_NUMBER = DATEPART(WEEK,@DATE) - 1 
    -- 如果當前天的上一個周日小於年第一天
    ELSE IF DATEADD(DAY,-1,DATEADD(WK,DATEDIFF(WK,0,@DATE),0)) < @FIRST_DATE_OF_YEAR 
        SET @WEEK_NUMBER = 1 
    -- 當前天前面的一個周日正好是以周日為開始年的 7 倍的天數
    ELSE IF DATEDIFF(DAYOFYEAR,@FIRST_DATE_OF_YEAR,DATEADD(DAY,-1,DATEADD(WK,DATEDIFF(WK,0,@DATE),0) ))/7 + 1 = DATEPART(WEEK,@DATE) 
        SET @WEEK_NUMBER = DATEPART(WEEK,@DATE) + 1  
    ELSE 
        SET @WEEK_NUMBER = DATEPART(WEEK,@DATE)   
           
    RETURN @WEEK_NUMBER
END
GO

為了方便理解,可以查看下面的查詢。

DECLARE @DATE DATETIME = '2012-01-29'
DECLARE @FIRST_DATE_OF_YEAR DATETIME = DATEADD(YYYY,DATEDIFF(YYYY,0,@DATE),0) 

SELECT DATEPART(WEEK,@DATE), -- 一年中的周數,默認以周日開始 
       DATEADD(WK,DATEDIFF(WK,0,@DATE),0), -- 當前周的周一,默認從周日開始,但是仍然找周一
       DATEADD(DAY,-1,DATEADD(WK,DATEDIFF(WK,0,@DATE),0)), -- 當前周先找周一,然后往前一天找到周日
       DATEDIFF(DAYOFYEAR,@FIRST_DATE_OF_YEAR,DATEADD(DAY,-1,DATEADD(WK,DATEDIFF(WK,0,@DATE),0))), -- 當前天離年第一天的間隔
       DATEDIFF(DAYOFYEAR,@FIRST_DATE_OF_YEAR,DATEADD(DAY,-1,DATEADD(WK,DATEDIFF(WK,0,@DATE),0) ))/7 + 1  -- 按天計算的周數

測試一下,查找不匹配的 Week Number - 30多年的數據結果都匹配,記得要新開一個頁面,以免之前 SET DATEFIRST 的影響。

查詢部分數據的 WeekNumber。

當然,我感覺寫的還是有點復雜,誰解決過類似問題的,期望有人能提出更簡潔的寫法。


更多 BI 文章請參看 BI 系列隨筆列表 (SSIS, SSRS, SSAS, MDX, SQL Server)

如果覺得這篇文章看了對您有幫助,請幫助推薦,以方便他人在 BIWORK 博客推薦欄中快速看到這些文章。


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM