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.


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.


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.


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.....

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

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


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

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

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.


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)


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.


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

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.


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


-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'

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.