Uzyskaj pierwszy dzień tygodnia w programie SQL Server

97

Próbuję grupować rekordy według tygodnia, zapisując zagregowaną datę jako pierwszy dzień tygodnia. Jednak standardowa technika, której używam do zaokrąglania dat, nie wydaje się działać poprawnie z tygodniami (chociaż działa w przypadku dni, miesięcy, lat, kwartałów i innych ram czasowych, do których ją zastosowałem).

Oto SQL:

select "start_of_week" = dateadd(week, datediff(week, 0, getdate()), 0);

To powraca 2011-08-22 00:00:00.000, czyli w poniedziałek, a nie w niedzielę. Wybranie @@datefirstzwrotów 7, czyli kodu na niedzielę, dzięki czemu serwer jest skonfigurowany poprawnie, o ile wiem.

Mogę to łatwo ominąć, zmieniając powyższy kod na:

select "start_of_week" = dateadd(week, datediff(week, 0, getdate()), -1);

Ale fakt, że muszę zrobić taki wyjątek, trochę mnie niepokoi. Przepraszamy, jeśli jest to powtórzone pytanie. Znalazłem kilka powiązanych pytań, ale żadne nie dotyczyło konkretnie tego aspektu.

Szybki Joe Smith
źródło
9
(@@DATEFIRST + DATEPART(DW, @SomeDate)) % 7@@datefirstmyślę, że pozostaje niezmienna niezależnie od ustawienia. Poniedziałek = 2.
Martin Smith

Odpowiedzi:

150

Aby odpowiedzieć, dlaczego otrzymujesz poniedziałek, a nie niedzielę:

Dodajesz liczbę tygodni do daty 0. Co to jest data 0? 1900-01-01. Jaki był dzień 01.01.1900? Poniedziałek. Więc w swoim kodzie mówisz, ile tygodni minęło od poniedziałku 1 stycznia 1900 roku? Nazwijmy to [n]. Ok, teraz dodaj [n] tygodni do poniedziałku 1 stycznia 1900. Nie powinieneś być zaskoczony, że kończy się to w poniedziałek. DATEADDnie ma pojęcia, że ​​chcesz dodawać tygodnie, ale tylko do niedzieli, po prostu dodaje 7 dni, a następnie dodaje 7 kolejnych dni ... tak jak DATEDIFFrozpoznaje tylko granice, które zostały przekroczone. Na przykład oba zwracają 1, mimo że niektórzy ludzie narzekają, że powinna być wbudowana rozsądna logika do zaokrąglania w górę lub w dół:

SELECT DATEDIFF(YEAR, '2010-01-01', '2011-12-31');
SELECT DATEDIFF(YEAR, '2010-12-31', '2011-01-01');

Aby odpowiedzieć, jak dostać niedzielę:

Jeśli chcesz niedzielę, wybierz datę bazową, która nie jest poniedziałek, ale raczej niedziela. Na przykład:

DECLARE @dt DATE = '1905-01-01';
SELECT [start_of_week] = DATEADD(WEEK, DATEDIFF(WEEK, @dt, CURRENT_TIMESTAMP), @dt);

To się nie zepsuje, jeśli zmienisz swoje DATEFIRSTustawienie (lub Twój kod jest uruchomiony dla użytkownika z innym ustawieniem) - pod warunkiem, że nadal chcesz niedzielę, niezależnie od bieżącego ustawienia. Jeśli chcesz, aby te dwie odpowiedzi były jive, powinieneś użyć funkcji, która zależy od DATEFIRSTustawienia, np

SELECT DATEADD(DAY, 1-DATEPART(WEEKDAY, CURRENT_TIMESTAMP), CURRENT_TIMESTAMP);

Więc jeśli zmienisz DATEFIRSTustawienie na poniedziałek, wtorek, co masz, zachowanie się zmieni. W zależności od tego, jakiego zachowania chcesz, możesz użyć jednej z następujących funkcji:

CREATE FUNCTION dbo.StartOfWeek1 -- always a Sunday
(
    @d DATE
)
RETURNS DATE
AS
BEGIN
    RETURN (SELECT DATEADD(WEEK, DATEDIFF(WEEK, '19050101', @d), '19050101'));
END
GO

...lub...

CREATE FUNCTION dbo.StartOfWeek2 -- always the DATEFIRST weekday
(
    @d DATE
)
RETURNS DATE
AS
BEGIN
    RETURN (SELECT DATEADD(DAY, 1-DATEPART(WEEKDAY, @d), @d));
END
GO

Masz teraz wiele alternatyw, ale która z nich działa najlepiej? Zdziwiłbym się, gdyby były jakieś większe różnice, ale zebrałem wszystkie dotychczas udzielone odpowiedzi i przeprowadziłem je przez dwa zestawy testów - jeden tani i jeden drogi. Zmierzyłem statystyki klienta, ponieważ nie widzę tutaj I / O lub pamięci odgrywających rolę w wydajności (chociaż mogą one mieć znaczenie w zależności od sposobu wykorzystania funkcji). W moich testach wyniki to:

Zapytanie o przypisanie „Tanie”:

Function - client processing time / wait time on server replies / total exec time
Gandarez     - 330/2029/2359 - 0:23.6
me datefirst - 329/2123/2452 - 0:24.5
me Sunday    - 357/2158/2515 - 0:25.2
trailmax     - 364/2160/2524 - 0:25.2
Curt         - 424/2202/2626 - 0:26.3

Zapytanie o przypisanie „Drogie”:

Function - client processing time / wait time on server replies / total exec time
Curt         - 1003/134158/135054 - 2:15
Gandarez     -  957/142919/143876 - 2:24
me Sunday    -  932/166817/165885 - 2:47
me datefirst -  939/171698/172637 - 2:53
trailmax     -  958/173174/174132 - 2:54

W razie potrzeby mogę przekazać szczegóły moich testów - zatrzymując się tutaj, ponieważ jest to już dość rozwlekłe. Byłem trochę zaskoczony, widząc, że Curt wyszedł jako najszybszy na najwyższym poziomie, biorąc pod uwagę liczbę obliczeń i kod wbudowany. Może przeprowadzę dokładniejsze testy i napiszę o tym na blogu ... jeśli nie macie żadnych zastrzeżeń co do tego, żebym opublikował wasze funkcje gdzie indziej.

Aaron Bertrand
źródło
Tak więc, jeśli uznam , że moje tygodnie zaczynają się w niedzielę i kończą w sobotę, mogę uzyskać ostatni dzień tygodnia dla dowolnej daty @ d, takiej jak ta: SELECT DATEADD (wk, DATEDIFF (wk, '19041231', @d), 19041231)
Baodad
22

Dla tych, którzy potrzebują:

Poniedziałek = 1 i Niedziela = 7:

SELECT 1 + ((5 + DATEPART(dw, GETDATE()) + @@DATEFIRST) % 7);

Niedziela = 1 i Sobota = 7:

SELECT 1 + ((6 + DATEPART(dw, GETDATE()) + @@DATEFIRST) % 7);

Powyżej był podobny przykład, ale dzięki podwójnemu „% 7” byłby znacznie wolniejszy.

Kakkarot
źródło
1
Działa to również świetnie, aby uzyskać numer dnia od początku tygodnia, czyli niedziela lub poniedziałek. Dzięki
Fandango68
Alternatywnie select (datediff(dd,5,cal.D_DATE)%7 + 1)iselect (datediff(dd,6,cal.D_DATE)%7 + 1)
vasja
9

Dla tych, którzy potrzebują odpowiedzi w pracy, a tworzenie funkcji jest zabronione przez Twojego administratora, sprawdzi się następujące rozwiązanie:

select *,
cast(DATEADD(day, -1*(DATEPART(WEEKDAY, YouDate)-1), YourDate) as DATE) as WeekStart
From.....

To daje początek tego tygodnia. Tutaj zakładam, że niedziele to początek tygodni. Jeśli myślisz, że poniedziałek to początek, zastosuj:

select *,
cast(DATEADD(day, -1*(DATEPART(WEEKDAY, YouDate)-2), YourDate) as DATE) as WeekStart
From.....
Philip Chen
źródło
5

To działa wspaniale dla mnie:

UTWÓRZ FUNKCJĘ [dbo]. [StartOfWeek]
(
  @INPUTDATE DATETIME
)
TERMIN ZWROTU

TAK JAK
ZACZYNAĆ
  - TO nie działa w funkcji.
  - SET DATEFIRST 1 - ustaw poniedziałek jako pierwszy dzień tygodnia.

  DEKLARUJ @DOW INT - aby zapamiętać dzień tygodnia
  SET @INPUTDATE = CONVERT (VARCHAR (10), @INPUTDATE, 111)
  SET @DOW = DATEPART (DW, @INPUTDATE)

  - Magiczna konwersja poniedziałku na 1, wtorku na 2 itd.
  - niezależnie od tego, co SQL serwer myśli o rozpoczęciu tygodnia.
  - Ale tutaj mamy niedzielę oznaczoną jako 0, ale naprawimy to później.
  SET @DOW = (@DOW + @@ DATEFIRST - 1)% 7
  IF @ DOW = 0 SET @DOW = 7 - poprawka na niedzielę

  RETURN DATEADD (DD, 1 - @ DOW, @ INPUTDATE)

KONIEC
trailmax
źródło
Wydaje się, że powróci w poniedziałek, biorąc pod uwagę dzisiejszą datę, a nie niedzielę. OP ma już funkcję, która zwraca poniedziałek, chce, aby zwracała niedzielę. :-)
Aaron Bertrand
doh! Następnym razem powinienem uważniej czytać pytania. Jednak moje rozwiązanie można łatwo dostosować, jeśli nadal jest to wymagane. Wygląda na to, że OP i tak jest zadowolony z zaakceptowanej odpowiedzi -)
trailmax
To jest poprawne rozwiązanie na moim komputerze, ponieważ dla mnie: DATEADD (ww, DATEDIFF (ww, 0, CONVERT (DATE, '2017-10-8')), 0) zwraca 2017-10-9!
Uruchom CMD
3

Wygooglowałem ten skrypt:

create function dbo.F_START_OF_WEEK
(
    @DATE           datetime,
    -- Sun = 1, Mon = 2, Tue = 3, Wed = 4
    -- Thu = 5, Fri = 6, Sat = 7
    -- Default to Sunday
    @WEEK_START_DAY     int = 1 
)
/*
Find the fisrt date on or before @DATE that matches 
day of week of @WEEK_START_DAY.
*/
returns     datetime
as
begin
declare  @START_OF_WEEK_DATE    datetime
declare  @FIRST_BOW     datetime

-- Check for valid day of week
if @WEEK_START_DAY between 1 and 7
    begin
    -- Find first day on or after 1753/1/1 (-53690)
    -- matching day of week of @WEEK_START_DAY
    -- 1753/1/1 is earliest possible SQL Server date.
    select @FIRST_BOW = convert(datetime,-53690+((@WEEK_START_DAY+5)%7))
    -- Verify beginning of week not before 1753/1/1
    if @DATE >= @FIRST_BOW
        begin
        select @START_OF_WEEK_DATE = 
        dateadd(dd,(datediff(dd,@FIRST_BOW,@DATE)/7)*7,@FIRST_BOW)
        end
    end

return @START_OF_WEEK_DATE

end
go

http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=47307

Lakoniczny
źródło
2

Może potrzebujesz tego:

SELECT DATEADD(DD, 1 - DATEPART(DW, GETDATE()), GETDATE())

Lub

DECLARE @MYDATE DATETIME
SET @MYDATE = '2011-08-23'
SELECT DATEADD(DD, 1 - DATEPART(DW, @MYDATE), @MYDATE)

Funkcjonować

CREATE FUNCTION [dbo].[GetFirstDayOfWeek]
( @pInputDate    DATETIME )
RETURNS DATETIME
BEGIN

SET @pInputDate = CONVERT(VARCHAR(10), @pInputDate, 111)
RETURN DATEADD(DD, 1 - DATEPART(DW, @pInputDate),
               @pInputDate)

END
GO
Gandarez
źródło
6
DATEPART(DWzależy od@@datefirst
Martin Smith
Podoba mi się prostota tego. Wydaje się, że działa całkiem dobrze również w przypadku bardzo dużych zestawów danych.
Quick Joe Smith
2
Dlaczego po prostu nie wprowadzić parametru wejściowego, DATEa wtedy nie trzeba wykonywać żadnych nieoptymalnych konwersji do VARCHARi z powrotem, tylko po to, aby usunąć wszelkie przekazane przypadkowe składniki czasu.
Aaron Bertrand
Użyto funkcji Convert, ponieważ zwrócona wartość nie wymaga Timewartości.
Gandarez
1
Tak, ale chodzi o to, że przejście na varchar iz powrotem jest kosztowne. Jeśli masz tylko parametr DATE, nie obchodzi Cię, czy czas został uwzględniony ... zostanie on za Ciebie usunięty.
Aaron Bertrand
2
UTWÓRZ FUNKCJĘ dbo.fnFirstWorkingDayOfTheWeek
(
    @currentDate date
)
ZWROTY INT
TAK JAK
ZACZYNAĆ
    - pobierz ustawienie DATEFIRST
    ZADEKLAROWAĆ @ds int = @@ DATEFIRST 
    - uzyskaj numer dnia tygodnia zgodnie z bieżącym ustawieniem DATEFIRST
    ZADEKLAROWAĆ @dow int = DATEPART (dw, @ currentDate) 

    ZADEKLAROWAĆ @wd int = 1 + (((@ dow + @ ds)% 7) +5)% 7 - to zawsze zwraca Mon jako 1, Wt jako 2 ... Niedziela jako 7 

    RETURN DATEADD (dd, 1- @ wd, @ currentDate) 

KONIEC
JG JIN
źródło
To jedyna funkcja, która działała dla mnie w SQL Server 2005. Dziękuję
Fandango68
@ Fernando68 Czy możesz wyjaśnić, dlaczego inne rozwiązania nie zadziałały?
Aaron Bertrand
@AaronBertrand przepraszam, nie pamiętam, ale myślę, że skupiałem się na szybkiej odpowiedzi i wypróbowałem twoją, ale z jakiegoś powodu to nie zadziałało.
Fandango68
@ Fernando68 Cóż, to jest bardzo pomocne. : - \
Aaron Bertrand
2

Podstawowy (niedziela bieżącego tygodnia)

select cast(dateadd(day,-(datepart(dw,getdate())-1),getdate()) as date)

Jeśli poprzedni tydzień:

select cast(dateadd(day,-(datepart(dw,getdate())-1),getdate()) -7 as date)

Wewnętrznie zbudowaliśmy funkcję, która to robi, ale jeśli potrzebujesz szybkiego i brudnego, zrobi to.

JamesDavisSr
źródło
0

Ponieważ data juliańska 0 to poniedziałek, wystarczy dodać liczbę tygodni do niedzieli, czyli dzień przed -1 Np. select dateadd (wk, datediff (wk, 0, getdate ()), - 1)

Iggy
źródło
0
Set DateFirst 1;

Select 
    Datepart(wk, TimeByDay) [Week]
    ,Dateadd(d,
                CASE 
                WHEN  Datepart(dw, TimeByDay) = 1 then 0
                WHEN  Datepart(dw, TimeByDay) = 2 then -1
                WHEN  Datepart(dw, TimeByDay) = 3 then -2
                WHEN  Datepart(dw, TimeByDay) = 4 then -3
                WHEN  Datepart(dw, TimeByDay) = 5 then -4
                WHEN  Datepart(dw, TimeByDay) = 6 then -5
                WHEN  Datepart(dw, TimeByDay) = 7 then -6
                END
                , TimeByDay) as StartOfWeek

from TimeByDay_Tbl

Taka jest moja logika. Ustaw pierwszy dzień tygodnia na poniedziałek, a następnie oblicz, jaki jest dzień tygodnia danego dnia, a następnie używając DateAdd i Case obliczam, jaka data byłaby w poprzedni poniedziałek tego tygodnia.

user2479728
źródło
-1

Nie mam żadnych problemów z żadną z podanych tutaj odpowiedzi, jednak myślę, że moja jest o wiele prostsza do wdrożenia i zrozumienia. Nie przeprowadzałem na nim żadnych testów wydajności, ale powinno to być pomijalne.

Tak więc swoją odpowiedź zaczerpnąłem z faktu, że daty są przechowywane na serwerze SQL jako liczby całkowite (mówię tylko o komponencie daty). Jeśli mi nie wierzysz, wypróbuj to SELECT CONVERT (INT, GETDATE ()) i na odwrót.

Teraz wiedząc o tym, możesz zrobić fajne równania matematyczne. Możesz wymyślić lepszy, ale tutaj jest mój.

/*
TAKEN FROM http://msdn.microsoft.com/en-us/library/ms181598.aspx
First day of the week is
1 -- Monday
2 -- Tuesday
3 -- Wednesday
4 -- Thursday
5 -- Friday
6 -- Saturday
7 (default, U.S. English) -- Sunday
*/

--Offset is required to compensate for the fact that my @@DATEFIRST setting is 7, the default. 
DECLARE @offSet int, @testDate datetime
SELECT @offSet = 1, @testDate = GETDATE()

SELECT CONVERT(DATETIME, CONVERT(INT, @testDate) - (DATEPART(WEEKDAY, @testDate) - @offSet))
Ryk
źródło
1
Uważam, że to nie działa dla mnie. Mój @@DATEFIRSTto również 7, ale jeśli @testDatejest to początek tygodnia, zwraca datę, która jest dzień wcześniej.
wiersz1
-1

Miałem podobny problem. Biorąc pod uwagę datę, chciałem poznać poniedziałek tego tygodnia.

Użyłem następującej logiki: znajdź numer dnia tygodnia z zakresu 0-6, a następnie odejmij go od daty początkowej.

Użyłem: DATEADD (dzień, - (DATEPART (dzień tygodnia,) + 5)% 7,)

Ponieważ DATEPRRT (dzień tygodnia,) zwraca 1 = niedzielae ... 7 = sobota, DATEPART (dzień roboczy,) + 5)% 7 zwraca 0 = poniedziałek ... 6 = niedziela.

Odejmowanie tej liczby dni od pierwotnej daty daje poprzedni poniedziałek. Tę samą technikę można zastosować w każdym początkowym dniu tygodnia.

Pat Lane
źródło
-1

Wydało mi się to proste i przydatne. Działa nawet jeśli pierwszy dzień tygodnia to niedziela lub poniedziałek.

ZADEKLAROWAĆ @BaseDate jako datę

SET @BaseDate = GETDATE ()

ZADEKLAROWAĆ @FisrtDOW jako datę

SELECT @FirstDOW = DATEADD (d, DATEPART (WEEKDAY, @ BaseDate) * -1 + 1, @BaseDate)

ElJoel
źródło
-3

Może nadmiernie upraszczam tutaj i może tak być, ale wydaje mi się, że to działa. Nie napotkałem jeszcze żadnych problemów ...

CAST('1/1/' + CAST(YEAR(GETDATE()) AS VARCHAR(30)) AS DATETIME) + (DATEPART(wk, YOUR_DATE) * 7 - 7) as 'FirstDayOfWeek'
CAST('1/1/' + CAST(YEAR(GETDATE()) AS VARCHAR(30)) AS DATETIME) + (DATEPART(wk, YOUR_DATE) * 7) as 'LastDayOfWeek'
znak
źródło
Możesz tutaj uzyskać różne odpowiedzi, jeśli spróbujesz różnych ustawień SET DATEFIRST.
Aaron Bertrand
5
Cóż, nie oddałem głosu, ale w Twojej odpowiedzi w ogóle nie ma wzmianki DATEFIRST(od trzech i pół roku) i nadal nie ma. Powinieneś także unikać formatów regionalnych, takich jak m/d/y, nawet w scenariuszach, w których m i d są takie same.
Aaron Bertrand