Pendekatan terbaik untuk mengisi tabel dimensi tanggal

8

Saya mencari untuk mengisi tabel dimensi tanggal dalam database SQL Server 2008. Bidang dalam tabel adalah sebagai berikut:

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

Saya telah menulis fungsi DateListInRange (D1, D2) yang mengembalikan semua tanggal antara dua tanggal parameter D1 dan D2 inklusif.

yaitu. parameter '2014-01-01' dan '2014-01-03' akan kembali:

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

Saya ingin mengisi tabel DATE_DIM untuk semua tanggal dalam rentang, yaitu 2010-01-01 hingga 2020-01-01. Sebagian besar bidang dapat diisi dengan fungsi SQL 2008 DATEPART, DATENAME, dan YEAR.

Data fiskal mengandung sedikit lebih banyak logika, beberapa di antaranya saling bergantung. Misalnya: Kuartal fiskal 1 -> Bulan fiskal harus 1, 2 atau 3 Kuartal fiskal 2 -> Bulan fiskal harus 4, 5 atau 6

Saya dapat dengan mudah menulis fungsi bernilai tabel yang menerima tanggal tertentu, dan kemudian menampilkan semua data fiskal, atau SEMUA bidang bahkan. Maka saya hanya perlu fungsi ini untuk dijalankan pada setiap baris fungsi DateListInRange.

Saya tidak terlalu peduli dengan kecepatan karena ini hanya perlu diisi beberapa kali setahun ketika tabel liburan diubah.

Apa cara terbaik untuk menulis ini dalam SQL?

Saat ini seperti ini:

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

Jika saya melakukan hal yang sama untuk data fiskal akan ada sedikit pengulangan dalam setiap pernyataan kasus akan dapat dihindari menggunakan fungsi dan mungkin lintas menerapkan TVF di atas daftar tanggal.

Harap dicatat saya menggunakan SQL Server 2008 sehingga banyak fungsi tanggal yang lebih baru minimal.

JohnLinux
sumber

Jawaban:

12

UPDATE : untuk contoh yang lebih umum tentang membuat dan mengisi tabel kalender atau dimensi, lihat tip ini:

Untuk pertanyaan spesifik yang ada, inilah usaha saya. Saya akan memperbarui ini dengan sihir yang Anda gunakan untuk menentukan hal-hal seperti Fiscal_MonthNumber dan Fiscal_MonthName, karena saat ini mereka adalah satu-satunya bagian non-intuitif dari pertanyaan Anda, dan itu satu-satunya informasi nyata yang sebenarnya tidak Anda sertakan.

Cara "terbaik" (baca: paling efisien) untuk mengisi tabel kalender, IMHO, adalah dengan menggunakan satu set, daripada satu lingkaran. Dan Anda dapat menghasilkan set ini tanpa mengubur logika menjadi fungsi yang ditentukan pengguna, yang benar-benar tidak memberi Anda apa-apa selain enkapsulasi - selain itu hanya objek lain yang harus dipelihara. Saya membicarakan hal ini secara lebih rinci dalam seri blog ini:

Jika Anda ingin terus menggunakan fungsi Anda, pastikan itu bukan fungsi bernilai tabel multi-pernyataan; itu tidak akan efisien sama sekali. Anda ingin memastikan bahwa itu sebaris (misalnya memiliki satu RETURNpernyataan dan tidak ada @tablepernyataan eksplisit ), memiliki WITH SCHEMABINDING, dan tidak menggunakan CTE rekursif. Di luar fungsi, inilah cara saya melakukannya:

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.

Dengan tabel di tempat, Anda dapat melakukan satu, set-sisipan data tahun sebanyak yang Anda inginkan dari tanggal mulai apa pun yang Anda pilih. Cukup tentukan tanggal mulai dan jumlah tahun. Saya menggunakan teknik "stacked CTE" untuk menghindari redundansi dan hanya melakukan banyak perhitungan sekali; kolom output dari CTE sebelumnya kemudian digunakan dalam perhitungan lebih lanjut nanti.

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

Sekarang, Anda masih memiliki kolom "liburan" dan "hari kerja" yang tersisa untuk ditangani - ini menjadi sedikit lebih rumit, tetapi Anda perlu memperbarui ketiga kolom tersebut dengan hari libur apa pun yang muncul dalam rentang tanggal Anda. Hal-hal seperti Hari Natal sangat mudah:

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

Hal-hal seperti Paskah menjadi jauh lebih rumit - Saya telah membuat blog beberapa ide di sini bertahun-tahun yang lalu .

Dan tentu saja perusahaan Anda yang bukan hari kerja yang sama sekali tidak ada hubungannya dengan hari libur nasional, dll. Harus dimutakhirkan secara langsung oleh Anda - SQL Server tidak akan memiliki cara bawaan untuk mengetahui kalender perusahaan Anda.

Sekarang, saya sengaja tidak menghitung salah satu kolom ini, karena Anda mengatakan sesuatu seperti yang dimiliki pengguna akhir previously preferred fields they can drag and drop- Saya tidak yakin apakah pengguna akhir benar-benar tahu atau peduli apakah sumber kolom adalah kolom nyata, kolom yang dihitung , atau berasal dari tampilan, kueri, atau fungsi ...

Dengan asumsi Anda memang ingin melihat ke dalam komputasi beberapa kolom ini untuk memudahkan pemeliharaan Anda (dan bertahan mereka membayar penyimpanan untuk kecepatan permintaan), Anda dapat melihatnya. Namun, hanya sebagai peringatan, beberapa kolom ini tidak dapat didefinisikan sebagai dihitung dan bertahan karena mereka non-deterministik. Inilah satu contoh, dan bagaimana cara mengatasinya.

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

Hasil:

Msg 4936, Level 16, Negara Bagian 1, Baris 130
Kolom yang dihitung 'DayOfWeek_Number' dalam tabel 'Test' tidak dapat dipertahankan karena kolom tersebut non-deterministik.

Alasan ini tidak dapat dipertahankan adalah karena banyak fungsi terkait tanggal bergantung pada pengaturan sesi pengguna, seperti DATEFIRST. SQL Server tidak dapat mempertahankan kolom di atas karena DATEPART(WEEKDAYharus memberikan hasil yang berbeda - diberikan data yang sama - untuk dua pengguna yang berbeda yang memiliki DATEFIRSTpengaturan berbeda .

Maka Anda mungkin menjadi pintar, dan berkata, well, saya bisa mengaturnya menjadi jumlah hari, modulo 7, offset dari beberapa hari yang saya tahu adalah hari Sabtu (katakanlah '2000-01-01'). Jadi, Anda mencoba:

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

Tapi, kesalahannya sama.

Alih-alih menggunakan konversi implisit dari string literal yang mewakili waktu tanggal dalam format yang jelas (kepada kami, tetapi bukan SQL Server), kami dapat menggunakan jumlah hari antara "tanggal nol" (1900-01-01) dan tanggal yang kita tahu adalah hari Sabtu (2000-01-01). Jika kami menggunakan integer di sini untuk mewakili perbedaan dalam beberapa hari, SQL Server tidak dapat mengeluh, karena tidak ada cara untuk salah menafsirkan angka itu. Jadi ini berhasil:

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

Keberhasilan!

Jika Anda tertarik untuk mengejar kolom yang dihitung untuk beberapa perhitungan ini, beri tahu saya.

Oh, dan satu hal lagi: Saya tidak tahu mengapa Anda akan menggosok meja ini dan mengisinya kembali dari awal. Berapa banyak dari hal-hal ini yang akan berubah? Apakah Anda akan terus mengubah tahun fiskal Anda? Ubah bagaimana Anda ingin mengeja March? Atur minggu Anda untuk mulai pada hari Senin satu minggu dan Kamis berikutnya? Ini benar-benar harus berupa tabel build-it-up-once, dan kemudian Anda membuat perubahan kecil (seperti memperbarui setiap baris dengan informasi liburan baru / yang diubah).

Aaron Bertrand
sumber