Cách tiếp cận tốt nhất cho bảng thứ nguyên ngày


8

Tôi đang tìm cách điền vào bảng thứ nguyên ngày trong cơ sở dữ liệu SQL Server 2008. Các trường trong bảng như sau:

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

Tôi đã viết một hàm DateListInRange (D1, D2) trả về tất cả các ngày giữa hai ngày tham số D1 và D2.

I E. tham số '2014-01-01' và '2014-01-03' sẽ trở lại:

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

Tôi muốn điền vào bảng DATE_DIM cho tất cả các ngày trong một phạm vi, tức là 2010-01-01 đến 2020-01-01. Hầu hết các trường có thể được điền với các hàm SQL 2008 DATEPART, DATENAME và NĂM.

Dữ liệu tài chính chứa logic hơn một chút, một số trong đó phụ thuộc vào nhau. Ví dụ: Quý tài chính 1 -> Tháng tài chính phải là 1, 2 hoặc 3 Quý tài chính 2 -> Tháng tài chính phải là 4, 5 hoặc 6

Tôi có thể dễ dàng viết một hàm có giá trị bảng chấp nhận một ngày cụ thể và sau đó xuất tất cả dữ liệu tài chính hoặc TẤT CẢ các trường. Sau đó, tôi sẽ chỉ cần chức năng này để chạy trên mỗi hàng của hàm DateListInRange.

Tôi không quan tâm lắm đến tốc độ vì điều này sẽ chỉ cần được đưa ra một vài lần trong năm khi bảng ngày lễ bị thay đổi.

Cách tốt nhất để viết điều này trong SQL là gì?

Hiện tại nó như thế này:

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

Nếu tôi làm tương tự cho dữ liệu tài chính, sẽ có khá nhiều sự lặp lại trong mỗi tuyên bố trường hợp có thể tránh được bằng cách sử dụng một hàm và có thể áp dụng chéo TVF qua danh sách ngày.

Xin lưu ý rằng tôi đang sử dụng SQL Server 2008 vì vậy rất nhiều chức năng ngày mới hơn là tối thiểu.

Câu trả lời:


12

CẬP NHẬT : để biết ví dụ chung hơn về việc tạo và điền vào bảng lịch hoặc thứ nguyên, hãy xem mẹo này:

Đối với câu hỏi cụ thể trong tay, đây là nỗ lực của tôi. Tôi sẽ cập nhật điều này với phép thuật mà bạn sử dụng để xác định những thứ như Frypt_MonthNumber và Frypt_MonthName, bởi vì ngay bây giờ chúng là phần không trực quan duy nhất trong câu hỏi của bạn và đó là thông tin hữu hình duy nhất bạn thực sự không bao gồm.

Cách "tốt nhất" (đọc: hiệu quả nhất) để điền vào bảng lịch, IMHO, là sử dụng một tập hợp, thay vì một vòng lặp. Và bạn có thể tạo tập hợp này mà không cần chôn logic vào các hàm do người dùng xác định, điều này thực sự không mang lại cho bạn bất cứ điều gì ngoài việc đóng gói - nếu không thì đó chỉ là một đối tượng khác để duy trì. Tôi nói về điều này chi tiết hơn rất nhiều trong loạt blog này:

Nếu bạn muốn tiếp tục sử dụng chức năng của mình, hãy đảm bảo rằng đó không phải là hàm có giá trị bảng nhiều câu lệnh; điều đó sẽ không hiệu quả chút nào Bạn muốn đảm bảo rằng nó là nội tuyến (ví dụ: có một RETURNtuyên bố duy nhất và không có @tabletuyên bố rõ ràng ), có WITH SCHEMABINDINGvà không sử dụng CTE đệ quy. Ngoài chức năng, đây là cách tôi sẽ làm:

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.

Với bảng đã có, bạn có thể thực hiện một lần chèn dựa trên tập hợp bao nhiêu năm dữ liệu bạn muốn từ bất kỳ ngày bắt đầu nào bạn chọn. Chỉ cần xác định ngày bắt đầu và số năm. Tôi sử dụng kỹ thuật "xếp chồng CTE" để tránh dư thừa và chỉ thực hiện một loạt các phép tính một lần; các cột đầu ra từ các CTE trước đó sau đó được sử dụng trong các tính toán tiếp theo sau này.

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

Bây giờ, bạn vẫn còn các cột "ngày nghỉ" và "ngày làm việc" để giải quyết - việc này sẽ hơi phức tạp hơn một chút, nhưng bạn cần cập nhật ba cột đó với bất kỳ ngày lễ nào xuất hiện trong phạm vi ngày của bạn. Những thứ như ngày Giáng sinh thực sự dễ dàng:

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

Những thứ như Lễ Phục Sinh trở nên phức tạp hơn nhiều - tôi đã viết một số ý tưởng ở đây nhiều năm trước .

Và tất nhiên, công ty của bạn không phải là ngày làm việc hoàn toàn không liên quan gì đến ngày lễ, v.v. phải được bạn cập nhật trực tiếp - SQL Server sẽ không có cách tích hợp để biết lịch của công ty bạn.

Bây giờ, tôi cố tình tránh xa việc tính toán bất kỳ cột nào trong số này, bởi vì bạn đã nói điều gì đó giống như người dùng cuối có previously preferred fields they can drag and drop- Tôi không chắc người dùng cuối có thực sự biết hoặc quan tâm nếu nguồn của cột là cột thực, cột được tính không hoặc xuất phát từ chế độ xem, truy vấn hoặc chức năng ...

Giả sử bạn làm muốn nhìn vào tính toán một số các cột này để giảm bớt về bảo trì của bạn (và tồn tại chúng sang bộ nhớ trả tiền cho tốc độ truy vấn), bạn có thể nhìn vào đó. Tuy nhiên, giống như một cảnh báo, một số trong các cột này không thể được định nghĩa là được tính toán và tồn tại bởi vì chúng không mang tính quyết định. Đây là một ví dụ và cách khắc phục nó.

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

Các kết quả:

Msg 4936, Cấp 16, Trạng thái 1, Dòng 130
Cột được tính 'DayOfWeek_Number' trong bảng 'Kiểm tra' không thể được duy trì vì cột không xác định.

Lý do điều này không thể được duy trì là vì nhiều chức năng liên quan đến ngày dựa vào cài đặt phiên của người dùng, như thế DATEFIRST. Máy chủ SQL không thể duy trì cột trên vì DATEPART(WEEKDAYsẽ cho các kết quả khác nhau - được cung cấp cùng một dữ liệu - cho hai người dùng khác nhau có các DATEFIRSTcài đặt khác nhau .

Sau đó, bạn có thể trở nên thông minh, và nói, tốt, tôi có thể đặt nó là số ngày, modulo 7, bù vào một ngày nào đó tôi biết là thứ bảy (giả sử '2000-01-01'). Vì vậy, bạn hãy thử:

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

Nhưng, cùng một lỗi.

Thay vì sử dụng một chuyển đổi ngầm định từ một chuỗi ký tự đại diện cho thời gian ngày theo định dạng rõ ràng (với chúng tôi, nhưng không phải là SQL Server), chúng tôi có thể sử dụng số ngày giữa "ngày không" (1900-01-01) và ngày đó chúng ta biết là thứ bảy (2000-01-01). Nếu chúng tôi sử dụng một số nguyên ở đây để biểu thị sự khác biệt theo ngày, SQL Server không thể khiếu nại, vì không có cách nào để hiểu sai số đó. Vì vậy, điều này hoạt động:

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

Sự thành công!

Nếu bạn quan tâm đến việc theo đuổi các cột được tính toán cho một số tính toán này, hãy cho tôi biết.

Ồ, và một điều cuối cùng: Tôi không biết tại sao bạn lại chùi xuống cái bàn này và điền lại từ đầu. Có bao nhiêu trong số những điều này sẽ thay đổi? Bạn sẽ thay đổi năm tài chính của bạn liên tục? Thay đổi cách bạn muốn đánh vần tháng ba? Đặt tuần của bạn để bắt đầu vào thứ Hai một tuần và thứ năm tuần sau? Đây thực sự nên là một bảng xây dựng một lần, và sau đó bạn thực hiện các điều chỉnh nhỏ (như cập nhật các hàng riêng lẻ với thông tin kỳ nghỉ mới / thay đổi).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.