Je cherche à remplir une table de dimension de date dans une base de données SQL Server 2008. Les champs du tableau sont les suivants:
[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)
J'ai écrit une fonction DateListInRange (D1, D2) qui renvoie toutes les dates entre deux dates de paramètre D1 et D2 inclus.
c'est à dire. les paramètres '2014-01-01' et '2014-01-03' renverraient:
2014-01-01
2014-01-02
2014-01-03
Je souhaite remplir la table DATE_DIM pour toutes les dates dans une plage, c'est-à-dire 2010-01-01 à 2020-01-01. La plupart des champs peuvent être remplis avec les fonctions SQL 2008 DATEPART, DATENAME et YEAR.
Les données budgétaires contiennent un peu plus de logique, dont certaines dépendent les unes des autres. Par exemple: Trimestre fiscal 1 -> Le mois fiscal doit être 1, 2 ou 3 Trimestre fiscal 2 -> Le mois fiscal doit être 4, 5 ou 6
Je peux facilement écrire une fonction de valeur de table qui accepte une date spécifique, puis génère toutes les données fiscales, ou TOUS les champs même. Ensuite, j'aurais juste besoin de cette fonction pour exécuter sur chaque ligne de la fonction DateListInRange.
Je ne suis pas très préoccupé par la vitesse car cela ne devra être rempli que quelques fois par an lorsque la table des vacances sera modifiée.
Quelle est la meilleure façon d'écrire cela en SQL?
Actuellement, c'est comme ça:
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')
Si je fais la même chose pour les données fiscales, il y aura pas mal de répétitions dans chaque cas, la déclaration pourrait être évitée en utilisant une fonction et peut-être en appliquant la TVF sur la liste des dates.
Veuillez noter que j'utilise SQL Server 2008, donc beaucoup de fonctionnalités de date plus récentes sont minimes.
[~ # ~] mise à jour [~ # ~] : pour un exemple plus générique de création et de remplissage d'un calendrier ou d'un tableau de dimensions, consultez cette astuce:
Pour la question spécifique à portée de main, voici ma tentative. Je mettrai à jour cela avec la magie que vous utilisez pour déterminer des choses comme Fiscal_MonthNumber et Fiscal_MonthName, car en ce moment, elles sont la seule partie non intuitive de votre question, et ce sont les seules informations tangibles que vous n'avez pas réellement incluses.
La "meilleure" manière (lire: la plus efficace) de remplir une table de calendrier, à mon humble avis, est d'utiliser un ensemble, plutôt qu'une boucle. Et vous pouvez générer cet ensemble sans enfouir la logique dans des fonctions définies par l'utilisateur, qui ne vous rapportent vraiment rien d'autre que l'encapsulation - sinon c'est juste un autre objet à maintenir. J'en parle beaucoup plus en détail dans cette série de blogs:
Si vous souhaitez continuer à utiliser votre fonction, assurez-vous que ce n'est pas une fonction table multi-instructions; ça ne va pas être efficace du tout. Vous voulez vous assurer qu'il est en ligne (par exemple, a une seule instruction RETURN
et aucune explicite @table
déclaration), a WITH SCHEMABINDING
, et n'utilise pas de CTE récursifs. En dehors d'une fonction, voici comment je le ferais:
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.
Avec la table en place, vous pouvez effectuer une insertion unique, basée sur un ensemble, d'autant d'années de données que vous le souhaitez, quelle que soit la date de début que vous choisissez. Précisez simplement la date de début et le nombre d'années. J'utilise une technique "CTE empilé" pour éviter la redondance et n'effectue une multitude de calculs qu'une seule fois; les colonnes de sortie des CTE antérieurs sont ensuite utilisées ultérieurement dans d'autres calculs.
-- 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;
Maintenant, il vous reste ces colonnes "vacances" et "journée de travail" à traiter - cela devient un peu plus lourd, mais vous devez mettre à jour ces trois colonnes avec tous les jours fériés qui apparaissent dans votre plage de dates. Des choses comme le jour de Noël sont vraiment faciles:
UPDATE dbo.DateDimension
SET IsWorkday = 0, IsHoliday = 1, HolidayName = 'Christmas'
WHERE Calendar_Month_Number = 12 AND Calendar_DayOfMonth = 25;
Des choses comme Pâques deviennent beaucoup plus délicates - j'ai blogué quelques idées ici il y a de nombreuses années .
Et bien sûr, les jours fériés de votre entreprise qui n'ont absolument rien à voir avec les jours fériés, etc. doivent être mis à jour directement par vous - SQL Server n'a pas de moyen intégré pour connaître le calendrier de votre entreprise.
Maintenant, je me suis délibérément éloigné du calcul de ces colonnes, car vous avez dit quelque chose comme les utilisateurs finaux ont previously preferred fields they can drag and drop
- Je ne sais pas si les utilisateurs finaux savent vraiment ou se soucient si la source d'une colonne est une vraie colonne, une colonne calculée, ou provient d'une vue, d'une requête ou d'une fonction ...
En supposant que vous faites souhaitez étudier le calcul de certaines de ces colonnes pour faciliter votre maintenance (et les inciter à payer le stockage pour la vitesse de requête), vous pouvez examiner cela. Cependant, à titre d'avertissement, certaines de ces colonnes ne peuvent pas être définies comme calculées et persistantes car elles ne sont pas déterministes. Voici un exemple et comment le contourner.
CREATE TABLE dbo.Test
(
[date] DATE PRIMARY KEY,
DayOfWeek_Number AS DATEPART(WEEKDAY, [date]) PERSISTED
);
Résultats:
Msg 4936, niveau 16, état 1, ligne 130
La colonne calculée 'DayOfWeek_Number' dans la table 'Test' ne peut pas être conservée car la colonne n'est pas déterministe.
La raison pour laquelle cela ne peut pas être persistant est que de nombreuses fonctions liées à la date dépendent des paramètres de session de l'utilisateur, comme DATEFIRST
. SQL Server ne peut pas conserver la colonne ci-dessus car DATEPART(WEEKDAY
devrait donner des résultats différents - pour les mêmes données - pour deux utilisateurs différents qui ont des paramètres DATEFIRST
différents.
Ensuite, vous pourriez devenir intelligent et dire, eh bien, je peux le définir comme le nombre de jours, modulo 7, compensé par rapport à un jour que je sais être un samedi (disons, '2000-01-01'
). Vous essayez donc:
CREATE TABLE dbo.Test
(
[date] DATE PRIMARY KEY,
DayOfWeek_Number AS
COALESCE(NULLIF(DATEDIFF(DAY,'20000101',[date])%7,0),7) PERSISTED
);
Mais, même erreur.
Au lieu d'utiliser une conversion implicite à partir d'un littéral de chaîne qui représente une date-heure dans un format non ambigu (pour nous, mais pas pour SQL Server), nous pouvons utiliser le nombre de jours entre la "date zéro" (1900-01-01) et cette date que nous connaissons est un samedi (2000-01-01). Si nous utilisons ici un entier pour représenter la différence en jours, SQL Server ne peut pas se plaindre, car il n'y a aucun moyen de mal interpréter ce nombre. Donc ça marche:
-- 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
);
Succès!
Si vous êtes intéressé à poursuivre les colonnes calculées pour certains de ces calculs, faites-le moi savoir.
Oh, et une dernière chose: je ne sais pas pourquoi vous feriez jamais frotter cette table et la re-remplir à partir de zéro. Combien de ces choses vont changer? Allez-vous modifier constamment votre exercice financier? Changer la façon dont vous voulez épeler mars? Réglez votre semaine pour commencer le lundi une semaine et jeudi la prochaine? Cela devrait vraiment être une table à construire une fois, puis vous apportez des modifications mineures (comme la mise à jour de lignes individuelles avec des informations de vacances nouvelles/modifiées).