web-dev-qa-db-fra.com

Déterminez le 3e vendredi de chaque mois

J'ai besoin de déterminer les dates qui sont le "3e vendredi de chaque mois" pour une plage de dates de "1.1.1996 - 30.8.2014" dans SQL Server.

Je pense que je devrais utiliser une combinaison de DENSE_RANK() et PARTITION BY() pour définir "rang = 3". Cependant, je suis nouveau sur SQL et incapable de trouver le bon code.

16
emcor

Donné:

  • Vendredi est appelé "vendredi"
  • Le 3e vendredi du mois tombera toujours du 15 au 21 du mois

    select thedate
    from yourtable
    where datename(weekday, thedate) = 'Friday'
    and datepart(day, thedate)>=15 and datepart(day, thedate)<=21;
    

Vous pouvez également utiliser weekday avec datepart(), mais il est plus lisible avec un nom IMO. Les comparaisons de chaînes seront évidemment plus lentes cependant.

26
Philᵀᴹ

Afin d'obtenir une réponse indépendante de la langue/culture, vous devez tenir compte des différents noms de semaine et du début de la semaine.

En Italie, le vendredi est "Venerdì" et le premier jour de la semaine est le lundi, pas le dimanche comme aux États-Unis.

1900-01-01 était un lundi, nous pouvons donc utiliser ces informations pour calculer le jour de la semaine de manière indépendante des paramètres régionaux:

WITH dates AS (
    SELECT DATEADD(day, number, GETDATE()) AS theDate
    FROM master.dbo.spt_values
    WHERE type = 'P'
)
SELECT theDate, DATENAME(dw, theDate), DATEPART(dw, theDate)
FROM dates
WHERE DATEDIFF(day, '19000101', theDate) % 7 = 4
    AND DATEPART(day, thedate)>=15 and DATEPART(day, thedate)<=21;
14
spaghettidba

Une autre façon, qui utilise la réponse de Phil comme base et prend en charge différents paramètres:

select thedate
from yourtable
where (datepart(weekday, thedate) + @@DATEFIRST - 2) % 7 + 1 = 5   -- 5 -> Friday
  and (datepart(day, thedate) - 1) / 7 + 1 = 3 ;                   -- 3 -> 3rd week

Le 5 le code (si vous voulez un jour de semaine autre que le vendredi) doit être (le même que SET DATEFIRST codes):

1 for Monday
2 for Tuesday
3 for Wednesday
4 for Thursday
5 for Friday
6 for Saturday
7 for Sunday

Vous pouvez également simplement utiliser une date "bonne connue" pour être en sécurité face aux paramètres de langue. Par exemple, si vous recherchez des vendredis, consultez un calendrier et vérifiez que le 2 janvier 2015 était un vendredi. La première comparaison pourrait alors s'écrire:

DATEPART(weekday,thedate) = DATEPART(weekday,'20150102') --Any Friday

Voir aussi Comment obtenir le Nième jour de la semaine d'un mois par Peter Larsson.

12
ypercubeᵀᴹ

J'ai en fait écrit un article sur ce type de calcul à ici

Fondamentalement, vous pouvez utiliser le code suivant pour rechercher le 3e vendredi de chaque mois dans n'importe quelle plage de dates

USE TEMPDB
set nocount on;
IF OBJECT_ID('dbo.#t') is not null 
 DROP TABLE dbo.#t;
CREATE TABLE #t ([Date] datetime,
  [Year] smallint, [Quarter] tinyint, [Month] tinyint
, [Day] smallint -- from 1 to 366 = 1st to 366th day in a year
, [Week] tinyint -- from 1 to 54 = the 1st to 54th week in a year; 
, [Monthly_week] tinyint -- 1/2/3/4/5=1st/2nd/3rd/4th/5th week in a month
, [Week_day] tinyint -- 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat, 7=Sun
);
GO
USE TEMPDB
-- populate the table #t, and the day of week is defined as
-- 1=Mon, 2=Tue, 3=Wed, 4=Thu,5=Fri, 6=Sat, 7=Sun
;WITH   C0   AS (SELECT c FROM (VALUES(1),(1)) AS D(c)),
  C1   AS (SELECT 1 AS c FROM C0 AS A CROSS JOIN C0 AS B),
  C2   AS (SELECT 1 AS c FROM C1 AS A CROSS JOIN C1 AS B),
  C3   AS (SELECT 1 AS c FROM C2 AS A CROSS JOIN C2 AS B),
  C4   AS (SELECT 1 AS c FROM C3 AS A CROSS JOIN C3 AS B), 
  C5   AS (SELECT 1 AS c FROM C4 AS A CROSS JOIN C3 AS B),
  C6   AS (select rn=row_number() over (order by c)  from C5),
  C7   as (select [date]=dateadd(day, rn-1, '19000101') FROM C6 WHERE rn <= datediff(day, '19000101', '99991231')+1)

INSERT INTO #t ([year], [quarter], [month], [week], [day], [monthly_week], [week_day], [date])
SELECT datepart(yy, [DATE]), datepart(qq, [date]), datepart(mm, [date]), datepart(wk, [date])
     , datediff(day, dateadd(year, datediff(year, 0, [date]), 0), [date])+1
  , datepart(week, [date]) -datepart(week, dateadd(month, datediff(month, 0, [date]) , 0))+1
  , CASE WHEN datepart(dw, [date])+@@datefirst-1 > 7 THEN (datepart(dw, [date])+@@datefirst-1)%7
         ELSE datepart(dw, [date])+@@datefirst-1 END
 , [date]
FROM C7
    --where [date] between '19900101' and '20990101'; -- if you want to populate a range of dates
GO

select convert(char(10), [Date], 120) 
from #t
where Monthly_week=3
and week_day=5
and [date] between '2015-01-01' and '2015-12-31' -- change to your own date range
4
jyao

Oui, je sais que c'est un ancien poste. Je pensais que je fournirais une perspective différente sur les choses malgré son âge. Heh ... et mes excuses. Je viens de réaliser que j'ai presque reproduit ce que @jyao a posté ci-dessus.

Sur la base de la modification actuelle de la question d'origine du PO, je ne pouvais pas comprendre pourquoi les gens publiaient les réponses.

En parcourant les modifications, j'ai trouvé la question d'origine et je l'ai postée ci-dessous ...

J'ai une série chronologique allant du 1.1.1996 au 30.8.2014 dans une base de données SQL, par ex. avec la table "db.dbo.datestable".

J'ai besoin de déterminer les dates qui sont le "3e vendredi de chaque mois" pour cette plage de dates dans SQL.

Je pense que je devrais utiliser une combinaison de "DENSE_RANK ()" et "PARTITION BY ()" pour définir "rank = 3". Cependant, je ne connais pas SQL et je ne trouve pas le bon code.

Pouvez-vous résoudre ce problème?

La partie de la question originale que j'ai soulignée en gras, semble être la clé. Je pourrais certainement me tromper, mais il me semble que le PO déclarait qu'il avait une table "Calendrier" appelée "dbo.datestable" et, pour moi, cela fait une énorme différence et je comprends maintenant pourquoi beaucoup de réponses sont ce qu'ils incluent celui qui a généré les dates car il a été posté le 10 novembre ... un jour après le dernier montage sur la question, ce qui a supprimé les derniers vestiges de la référence au "dbo.datestable".

Comme je l'ai dit, je peux me tromper, mais voici mon interprétation de la question d'origine.

J'ai une table "Calendrier" appelée "dbo.datestable". Étant donné toute plage de dates couverte par ce tableau, comment puis-je renvoyer uniquement les dates qui sont le 3e vendredi de chaque mois dans cette plage de dates donnée?

Étant donné que les méthodes conventionnelles pour ce faire ont déjà été couvertes, j'ajouterai une alternative qui pourrait être utile pour certains.

Simulons quelques colonnes que je pense que l'OP aura déjà dans sa table. Bien sûr, je devine les noms des colonnes. Veuillez sous toutes les colonnes équivalentes pour votre table "Calendrier". De plus, je fais tout cela dans TempDB, donc je ne prends pas le risque d'interférer avec la vraie table "Calendrier" de quelqu'un.

--=================================================================================================
--      Simulate just a couple of the necessary columns of the OPs "Calendar" table.
--      This is not a part of the solution.  We're just trying to simulate what the OP has.
--=================================================================================================
--===== Variables to control the dates that will appear in the "Calendar" table.
DECLARE  @StartDT   DATETIME
        ,@EndDT     DATETIME
;
 SELECT  @StartDT = '1900' --Will be inclusive start of this year in calculations.
        ,@EndDT   = '2100' --Will be exclusive start of this year in calculations.
;
--===== Create the "Calendar" table with just enough columns to simulate the OP's.
 CREATE TABLE #datestable
        (
         TheDate    DATETIME NOT NULL
        ,DW         TINYINT  NOT NULL  --SQL standard abbreviate of "Day of Week"
        )
;
--===== Populate the "Calendar" table (uses "Minimal Logging" in 2008+ this case).    
   WITH cteGenDates AS
(
 SELECT TOP (DATEDIFF(dd,@StartDT,@EndDT)) --You can use "DAY" instead of "dd" if you prefer. I don't like it, though.
        TheDate = DATEADD(dd, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1, @StartDT)
   FROM      sys.all_columns ac1
  CROSS JOIN sys.all_columns ac2
)
 INSERT INTO #datestable WITH (TABLOCK)
 SELECT  TheDate
        ,DW = DATEDIFF(dd,0,TheDate)%7+1 --Monday is 1, Friday is 5, Sunday is 7 etc.
   FROM cteGenDates
 OPTION (RECOMPILE) -- Help keep "Minimal Logging" in the presence of variables.
;
--===== Add the expected named PK for this example.
  ALTER TABLE #datestable 
    ADD CONSTRAINT PK_datestable PRIMARY KEY CLUSTERED (TheDate)
;

C'est aussi une donnée que je ne sais pas si l'OP peut apporter des modifications à sa table "Calendrier", donc cela pourrait ne pas l'aider mais cela pourrait aider les autres. Dans cet esprit, ajoutons une colonne DWoM (jour de la semaine pour le mois). Si vous n'aimez pas le nom, n'hésitez pas à le changer en ce dont vous avez besoin sur votre propre boîte.

--===== Add the new column.
  ALTER TABLE #datestable
    ADD DWOM TINYINT NOT NULL DEFAULT (0)
;

Ensuite, nous devons remplir la nouvelle colonne. Le PO en avait une idée dans son poste d'origine non altéré.

--===== Populate the new column using the CTE trick for updates so that
     -- we can use a Windowing Function in an UPDATE.
   WITH cteGenDWOM AS
(
 SELECT DW# = ROW_NUMBER() OVER (PARTITION BY DATEDIFF(mm,0,TheDate), DW
                                     ORDER BY TheDate)
        ,DWOM
   FROM #datestable
)
 UPDATE cteGenDWOM
    SET DWOM = DW#
;

Maintenant, comme il s'agit d'une colonne de longueur fixe, qui vient de créer un groupe de fractionnements de page, nous devons donc reconstruire l'index clusterisé pour "reconditionner" la table afin d'avoir autant de lignes par page que possible pour des raisons de performances.

--===== "Repack" the Clustered Index to get rid of the page splits we 
     -- caused by adding the new column.
  ALTER INDEX PK_datestable
     ON #datestable
        REBUILD WITH (FILLFACTOR = 100, SORT_IN_TEMPDB = ON)
;

Une fois cela fait, les requêtes qui font des choses comme retourner le 3ème vendredi de chaque mois dans une plage de dates donnée deviennent triviales et assez évidentes à lire.

--===== Return the 3rd Friday of every month included in the given date range.
 SELECT *
   FROM #datestable
  WHERE TheDate >= '1996-01-01' --I never use "BETWEEN" for dates out of habit for end date offsets.
    AND TheDate <= '2014-08-30'
    AND DW      =  5 --Friday
    AND DWOM    =  3 --The 3rd one for every month
  ORDER BY TheDate
;
2
Jeff Moden

Voici une solution simple de copier-coller. Vous pouvez en faire une fonction si vous le souhaitez.

Declare @CurrDate Date
Set @CurrDate = '11-20-2016'

declare @first datetime -- First of the month of interest (no time part)
declare @nth tinyint -- Which of them - 1st, 2nd, etc.
declare @dow tinyint -- Day of week we want
set @first = DATEFROMPARTS(YEAR(@CurrDate), MONTH(@CurrDate), 1) 
set @nth = 3
set @dow = 6
declare @result datetime
set @result = @first + 7*(@nth-1)
select  @result + (7 + @dow - datepart(weekday,@result))%7
0
jeffm