Bestimmen Sie den 3. Freitag eines jeden Monats

16

Ich muss die Daten ermitteln, die der "3. Freitag jedes Monats" für einen Datumsbereich von "1.1.1996 - 30.8.2014" in SQL Server sind.

Ich erwarte, dass ich eine Kombination von DENSE_RANK()und verwenden sollte PARTITION BY(), um "rank = 3" zu setzen. Ich bin jedoch neu in SQL und kann den richtigen Code nicht finden.

Emcor
quelle

Antworten:

26

Gegeben:

  • Freitag heißt "Freitag"
  • Der 3. Freitag des Monats fällt immer vom 15. bis 21. des Monats

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

Sie könnten auch weekdaymit verwenden datepart(), aber es ist besser lesbar mit einem Namen IMO. Zeichenkettenvergleiche sind jedoch offensichtlich langsamer.

Philᵀᴹ
quelle
14

Um eine sprach- / kulturunabhängige Antwort zu erhalten, müssen Sie unterschiedliche Wochentagsnamen und den Beginn der Woche berücksichtigen.

In Italien ist Freitag "Venerdì" und der erste Wochentag ist Montag, nicht Sonntag wie in den USA.

1900-01-01 war ein Montag, daher können wir diese Informationen verwenden, um den Wochentag unabhängig vom Gebietsschema zu berechnen:

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;
Spaghettidba
quelle
12

Ein anderer Weg, der Phils Antwort als Basis verwendet und sich um verschiedene Einstellungen kümmert:

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

Der 5Code (falls Sie einen anderen Wochentag als Freitag wünschen) sollte (der gleiche wie der SET DATEFIRSTCode) sein:

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

Sie können auch einfach ein "bekannt gutes" Datum verwenden, um angesichts der Spracheinstellungen sicher zu sein. Wenn Sie beispielsweise nach Freitagen suchen, überprüfen Sie einen Kalender und stellen Sie fest, dass der 2. Januar 2015 ein Freitag war. Der erste Vergleich könnte dann geschrieben werden als:

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

Siehe auch So erhalten Sie den N-ten Wochentag eines Monats von Peter Larsson.

ypercubeᵀᴹ
quelle
4

Ich habe tatsächlich einen Artikel über diese Art der Berechnung bei geschrieben hier

Grundsätzlich können Sie mit dem folgenden Code den 3. Freitag eines jeden Monats in einem beliebigen Datumsbereich ermitteln

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
jyao
quelle
2

Ja, ich weiß, dass dies ein alter Beitrag ist. Ich dachte, ich würde den Dingen trotz ihres Alters eine andere Richtung geben. Heh ... und ich entschuldige mich. Mir ist gerade aufgefallen, dass ich das, was @jyao oben gepostet hat, fast dupliziert habe.

Anhand der aktuellen Ausgabe der ursprünglichen Frage des OP konnte ich nicht herausfinden, warum Personen die Antworten, die sie gaben, veröffentlicht haben.

Bei der Durchsicht der Änderungen habe ich die ursprüngliche Frage gefunden und sie unten gepostet ...

Ich habe eine Zeitreihe vom 1.1.1996 - 30.8.2014 in einer SQL-Datenbank, zB mit der Tabelle "db.dbo.datestable".

Ich muss die Daten bestimmen, die der "3. Freitag jedes Monats" für diesen Datumsbereich in SQL sind.

Ich gehe davon aus, dass ich eine Kombination aus "DENSE_RANK ()" und "PARTITION BY ()" verwenden sollte, um "rank = 3" festzulegen. Ich bin jedoch neu in SQL und kann den richtigen Code nicht finden.

Können Sie dieses Problem lösen?

Der Teil der ursprünglichen Frage, den ich durch Fettdruck hervorgehoben habe, scheint der Schlüssel zu sein. Ich könnte mich sicherlich irren, aber mir scheint, dass das OP angab, dass er eine "Kalender" -Tabelle mit dem Namen "dbo.datestable" hat, und das macht für mich einen enormen Unterschied und ich verstehe jetzt, warum viele der Antworten sind, was sie sind, einschließlich derjenigen, die die Daten generiert, weil es am 10. November veröffentlicht wurde ... einen Tag nach der endgültigen Bearbeitung der Frage, die die letzten Überreste des Verweises auf die "dbo.datestable" entfernt.

Wie gesagt, ich könnte mich irren, aber hier ist meine Interpretation der ursprünglichen Frage.

Ich habe eine "Kalender" -Tabelle mit dem Namen "dbo.datestable". Wie kann ich bei einem bestimmten Datumsbereich, der von dieser Tabelle abgedeckt wird, nur die Daten zurückgeben, die der 3. Freitag eines jeden Monats innerhalb des angegebenen Datumsbereichs sind?

Da die herkömmlichen Methoden hierfür bereits behandelt wurden, füge ich eine Alternative hinzu, die für einige hilfreich sein könnte.

Lassen Sie uns ein paar der Spalten simulieren, von denen ich denke, dass sie das OP bereits in seiner Tabelle haben wird. Natürlich schätze ich die Spaltennamen. Bitte tragen Sie die entsprechenden Spalten für Ihre "Kalender" -Tabelle ein. Außerdem mache ich das alles in TempDB, damit ich nicht die Chance nutze, die echte "Kalender" -Tabelle von jemandem zu stören.

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

Es ist auch eine Selbstverständlichkeit, dass ich nicht weiß, ob das OP Änderungen an seiner "Kalender" -Tabelle vornehmen kann, so dass dies ihm möglicherweise nicht hilft, aber es kann anderen helfen. In diesem Sinne fügen wir eine DWoM-Spalte (Wochentag für den Monat) hinzu. Wenn Ihnen der Name nicht gefällt, können Sie ihn jederzeit auf Ihrer eigenen Box ändern.

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

Als nächstes müssen wir die neue Spalte füllen. Der OP hatte ein Gefühl dafür in seinem ursprünglichen unverfälschten Posten.

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

Da es sich um eine Spalte mit fester Länge handelt, wurden nur einige Seitenteile erstellt, sodass der Clustered Index neu erstellt werden muss, damit die Tabelle aus Gründen der Leistung so viele Zeilen wie möglich enthält.

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

Sobald dies erledigt ist, werden Abfragen, bei denen beispielsweise der 3. Freitag eines jeden Monats in einem bestimmten Datumsbereich zurückgegeben wird, trivial und ziemlich offensichtlich zu lesen.

--===== 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
;
Jeff Moden
quelle
0

Hier ist eine einfache Lösung zum Ausschneiden und Einfügen. Sie können dies in eine Funktion verwandeln, wenn Sie möchten.

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
Jeffm
quelle