web-dev-qa-db-ja.com

日付ディメンションテーブルにデータを入力するための最適な方法

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)

2つのパラメーターの日付D1とD2の間のすべての日付を返す関数DateListInRange(D1、D2)を作成しました。

すなわち。パラメータ '2014-01-01'および '2014-01-03'は以下を返します。

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

範囲内のすべての日付(2010-01-01〜2020-01-01)のDATE_DIMテーブルにデータを入力したい。ほとんどのフィールドには、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を使用しているため、新しい日付機能の多くは最小限に抑えられています。

8
JohnLinux

[〜#〜] update [〜#〜]:カレンダーまたはディメンションテーブルの作成と入力のより一般的な例については、次のヒントを参照してください。

手元にある特定の質問については、これが私の試みです。これは、Fiscal_MonthNumberやFiscal_MonthNameのようなものを決定するために使用する魔法で更新します。現時点では、これらは質問の非直感的な部分だけであり、実際には含まれていない唯一の具体的な情報だからです。

カレンダーテーブルIMHOにデータを入力するための "最良の"(読み取り:最も効率的な)方法は、ループではなくセットを使用することです。そして、ユーザー定義関数にロジックを埋め込むことなく、このセットを生成できます。これは、カプセル化以外には何も得られません。それ以外の場合は、維持する別のオブジェクトです。このことについては、このブログシリーズでさらに詳しく説明します。

関数を使い続けたい場合は、それが複数ステートメントのテーブル値関数ではないことを確認してください。それはまったく効率的ではありません。インラインであること(たとえば、単一の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;

これで、これらの「休日」列と「平日」列を処理することができます。これは少し面倒ですが、日付範囲に表示される休日でこれらの3つの列を更新する必要があります。クリスマスのようなことは本当に簡単です:

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

イースターのようなものはもっとトリッキーになります- ここで何年か前にいくつかのアイデアをブログに書いています

そしてもちろん、祝日などとはまったく関係のない非就業日は、直接更新する必要があります。SQLServerには、会社のカレンダーを知るための組み込みの方法がありません。

さて、エンドユーザーがpreviously preferred fields they can drag and dropを持っているようなものを言ったので、私はこれらの列の計算に意図的に近づきませんでした。列、計算列、またはビュー、クエリ、または関数から取得されます...

あなたがdoメンテナンスを容易にするためにこれらの列のいくつかを計算することを検討したい(そして、クエリ速度のためにストレージを支払うためにそれらを維持したい)と仮定すると、それを調査できます。ただし、警告と同じように、これらの列の一部は非決定的であるため、計算済みとして定義できず、永続化できません。以下に1つの例とその回避方法を示します。

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設定を持つ2人の異なるユーザーに対して異なる結果を与える必要があるためです。

そうすれば、賢くなるかもしれません。たとえば、土曜日であることがわかっているある日からのオフセットである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
);

しかし、同じエラー。

明確な(SQL Serverではなく)日付形式で日付時刻を表す文字列リテラルからの暗黙の変換を使用する代わりに、「ゼロ日付」(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
);

成功!

これらの計算の一部で計算列の追跡に関心がある場合は、お知らせください。

ああ、最後にもう1つあります。なぜこのテーブルをスクラブして、最初から再作成するのかわかりません。これらのうちどれだけが変わるのでしょうか?あなたは常にあなたの会計年度を変更するつもりですか?マーチの綴り方を変更しますか?週を月曜日に開始し、次の週の木曜日に開始するように設定しますか?これは実際には1回限りのビルドテーブルである必要があります。その後、マイナーな調整を行います(新しい/変更された休日情報で個々の行を更新するなど)。

12
Aaron Bertrand