Obtenha o primeiro dia da semana no SQL Server

97

Estou tentando agrupar registros por semana, armazenando a data agregada como o primeiro dia da semana. No entanto, a técnica padrão que uso para arredondar as datas não parece funcionar corretamente com semanas (embora funcione para dias, meses, anos, trimestres e qualquer outro período de tempo que eu apliquei).

Aqui está o SQL:

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

Isso retorna 2011-08-22 00:00:00.000, que é uma segunda-feira, não um domingo. Selecionando @@datefirstretorna 7, que é o código de domingo, para que o servidor esteja configurado corretamente até onde eu sei.

Posso contornar isso facilmente alterando o código acima para:

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

Mas o fato de ter de fazer essa exceção me deixa um pouco inquieto. Além disso, desculpas se esta for uma pergunta duplicada. Encontrei algumas questões relacionadas, mas nenhuma que abordasse este aspecto especificamente.

Joe Smith rápido
fonte
9
(@@DATEFIRST + DATEPART(DW, @SomeDate)) % 7permanece constante, independentemente da @@datefirstconfiguração, eu acho. Com segunda-feira = 2.
Martin Smith

Respostas:

149

Para responder por que você está recebendo uma segunda-feira e não um domingo:

Você está adicionando um número de semanas à data 0. O que é a data 0? 1900-01-01. Qual foi o dia 01/01/1900? Segunda-feira. Então, em seu código, você está dizendo, quantas semanas se passaram desde segunda-feira, 1º de janeiro de 1900? Vamos chamar isso de [n]. Ok, agora adicione [n] semanas a segunda-feira, 1 ° de janeiro de 1900. Você não deve se surpreender que isso acaba sendo uma segunda-feira. DATEADDnão tem ideia de que você deseja adicionar semanas, mas apenas até chegar a um domingo, é apenas adicionar 7 dias, depois adicionar mais 7 dias, ... assim como DATEDIFFapenas reconhece os limites que foram ultrapassados. Por exemplo, ambos retornam 1, embora algumas pessoas reclamem que deve haver alguma lógica sensata embutida para arredondar para cima ou para baixo:

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

Para responder como conseguir um domingo:

Se você quiser um domingo, escolha uma data base que não seja segunda-feira, mas sim um domingo. Por exemplo:

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

Isso não será interrompido se você alterar sua DATEFIRSTconfiguração (ou se seu código estiver sendo executado para um usuário com uma configuração diferente) - desde que você ainda queira um domingo, independentemente da configuração atual. Se você quer que essas duas respostas para jive, então você deve usar uma função que não dependem da DATEFIRSTconfiguração, por exemplo,

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

Portanto, se você alterar sua DATEFIRSTconfiguração para segunda, terça-feira, o que for, o comportamento mudará. Dependendo de qual comportamento você deseja, você pode usar uma destas funções:

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

...ou...

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

Agora, você tem muitas alternativas, mas qual tem o melhor desempenho? Ficaria surpreso se houvesse grandes diferenças, mas reuni todas as respostas fornecidas até agora e as executei em dois conjuntos de testes - um barato e outro caro. Eu medi as estatísticas do cliente porque não vejo a E / S ou a memória desempenhando um papel no desempenho aqui (embora elas possam ser importantes dependendo de como a função é usada). Em meus testes, os resultados são:

Consulta de atribuição "barata":

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

Consulta de atribuição "cara":

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

Posso retransmitir os detalhes dos meus testes, se desejar - parando aqui, pois isso já está ficando muito prolixo. Fiquei um pouco surpreso ao ver o Curt sair como o mais rápido no topo, devido ao número de cálculos e código embutido. Talvez eu execute alguns testes mais completos e blog sobre isso ... se vocês não têm nenhuma objeção a eu publicar suas funções em outro lugar.

Aaron Bertrand
fonte
Portanto, se eu considerar que minhas semanas começam no domingo e terminam no sábado, posso obter o último dia da semana para qualquer data @d assim: SELECT DATEADD (wk, DATEDIFF (wk, '19041231', @d), '19041231')
Baodad
21

Para aqueles que precisam obter:

Segunda-feira = 1 e Domingo = 7:

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

Domingo = 1 e sábado = 7:

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

Acima havia um exemplo semelhante, mas graças ao dobro "% 7" seria muito mais lento.

Kakkarot
fonte
Isso também funciona muito bem para obter o número do dia do início da semana, sendo Dom ou Seg. Obrigado
Fandango68
Alternativamente select (datediff(dd,5,cal.D_DATE)%7 + 1)eselect (datediff(dd,6,cal.D_DATE)%7 + 1)
vasja
8

Para quem precisa da resposta no trabalho e a função de criação é proibida pelo seu DBA, a seguinte solução funcionará:

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

Isso dá o início da semana. Aqui, presumo que os domingos são o início das semanas. Se você acha que segunda-feira é o começo, você deve usar:

select *,
cast(DATEADD(day, -1*(DATEPART(WEEKDAY, YouDate)-2), YourDate) as DATE) as WeekStart
From.....
Philip Chen
fonte
5

Isso funciona maravilhosamente bem para mim:

CRIAR FUNÇÃO [dbo]. [StartOfWeek]
(
  @INPUTDATE DATETIME
)
RETORNA DATETIME

COMO
INÍCIO
  - ISTO não funciona em função.
  - SET DATEFIRST 1 - definir segunda-feira como o primeiro dia da semana.

  DECLARE @DOW INT - para armazenar o dia da semana
  SET @INPUTDATE = CONVERT (VARCHAR (10), @INPUTDATE, 111)
  SET @DOW = DATEPART (DW, @INPUTDATE)

  - Conversão mágica de segunda para 1, terça para 2, etc.
  - independentemente do que o servidor SQL pensa sobre o início da semana.
  - Mas aqui temos o domingo marcado como 0, mas vamos corrigir isso depois.
  SET @DOW = (@DOW + @@ DATEFIRST - 1)% 7
  IF @DOW = 0 SET @DOW = 7 - fixo para domingo

  DATA DE RETORNO (DD, 1 - @ DOW, @ INPUTDATE)

FIM
trailmax
fonte
Isso parece retornar na segunda-feira dada a data de hoje, não no domingo. O OP já tem uma função que retorna segunda-feira, ele quer que ela retorne domingo. :-)
Aaron Bertrand
doh! Devo ler as perguntas com mais cuidado da próxima vez. No entanto, minha solução pode ser facilmente ajustada, se ainda for necessário. Parece que OP está satisfeito com a resposta aceita de qualquer maneira -)
trailmax
Esta é a solução correta na minha máquina, pois para mim: DATEADD (ww, DATEDIFF (ww, 0, CONVERT (DATE, '2017-10-8')), 0) retorna 2017-10-9!
Execute o CMD de
3

Pesquisei este script:

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

Curt
fonte
2

Talvez você precise disso:

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

Ou

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

Função

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
fonte
6
DATEPART(DWdepende de@@datefirst
Martin Smith
Eu gosto da simplicidade deste. Parece funcionar muito bem para conjuntos de dados muito grandes também.
Quick Joe Smith
2
Por que não apenas fazer o parâmetro de entrada, DATEentão você não tem que fazer nenhuma conversão subótima de VARCHARida e volta apenas para retirar qualquer componente de tempo acidental que seja passado.
Aaron Bertrand
A função Converter foi usada porque o valor retornado não precisa de Timevalores.
Gandarez
1
Sim, mas a questão é que converter para um varchar e vice-versa é caro. Se você tem apenas um parâmetro DATE, não se importa se o tempo foi incluído ... ele é retirado para você.
Aaron Bertrand
2
CRIAR FUNÇÃO dbo.fnFirstWorkingDayOfTheWeek
(
    @currentDate date
)
RETORNOS INT
COMO
INÍCIO
    - obter configuração DATEFIRST
    DECLARAR @ds int = @@ DATEFIRST 
    - obter o número do dia da semana na configuração DATEFIRST atual
    DECLARE @dow int = DATEPART (dw, @ currentDate) 

    DECLARAR @wd int = 1 + (((@ dow + @ ds)% 7) +5)% 7 - é sempre retornar Seg como 1, Ter como 2 ... Dom como 7 

    DATA DE RETORNO (dd, 1- @ wd, @ currentDate) 

FIM
JG JIN
fonte
Esta é a única função que funcionou para mim no SQL Server 2005. Obrigado
Fandango68
@ Fernando68 Você pode explicar como outras soluções não funcionaram?
Aaron Bertrand
@AaronBertrand, desculpe, não me lembro, mas acho que estava me concentrando em uma resposta rápida e tentei a sua, mas por algum motivo não funcionou para mim.
Fandango68
@ Fernando68 Bem, isso é muito útil. : - \
Aaron Bertrand
2

Para o básico (domingo da semana atual)

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

Se na semana anterior:

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

Internamente, construímos uma função que faz isso, mas se você precisar rápido e sujo, esta servirá.

JamesDavisSr
fonte
0

Como a data juliana 0 é uma segunda-feira, basta adicionar o número de semanas ao domingo, que é o dia anterior a -1. selecionar dateadd (wk, datediff (wk, 0, getdate ()), - 1)

Iggy
fonte
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

Essa é minha lógica. Defina o primeiro da semana como segunda-feira, calcule qual é o dia da semana em que um determinado dia é e, em seguida, usando DateAdd e Case, calculo qual seria a data na segunda-feira anterior daquela semana.

user2479728
fonte
-1

Não tenho problemas com nenhuma das respostas fornecidas aqui, no entanto, acho que a minha é muito mais simples de implementar e entender. Eu não executei nenhum teste de desempenho nele, mas deve ser negligenciável.

Portanto, deduzi minha resposta do fato de que as datas são armazenadas no servidor SQL como números inteiros (estou falando apenas sobre o componente de data). Se você não acredita em mim, tente este SELECT CONVERT (INT, GETDATE ()) e vice-versa.

Agora, sabendo disso, você pode fazer algumas equações matemáticas interessantes. Você pode encontrar um melhor, mas aqui está o meu.

/*
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
fonte
1
Acho que isso não funciona para mim. My @@DATEFIRSTtambém é 7, mas se @testDatefor o início da semana, isso retornará uma data que é o dia anterior.
row1
-1

Eu tive um problema parecido. Dado uma data, eu queria obter a data da segunda-feira daquela semana.

Usei a seguinte lógica: encontre o número do dia da semana no intervalo de 0-6 e subtraia-o da data de origem.

Usei: DATEADD (dia, - (DATEPART (dia da semana,) + 5)% 7,)

Como DATEPRRT (dia da semana,) retorna 1 = Domingo ... 7 = Sábado, DATEPART (dia da semana,) + 5)% 7 retorna 0 = Segunda ... 6 = Domingo.

Subtrair esse número de dias da data original resulta na segunda-feira anterior. A mesma técnica pode ser usada em qualquer dia inicial da semana.

Pat Lane
fonte
-1

Eu achei isso simples e útil. Funciona mesmo se o primeiro dia da semana for domingo ou segunda-feira.

DECLARAR @BaseDate AS Data

SET @BaseDate = GETDATE ()

DECLARAR @FisrtDOW AS Data

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

ElJoel
fonte
-3

Talvez eu esteja simplificando demais aqui, e pode ser o caso, mas isso parece funcionar para mim. Ainda não tive problemas com ele ...

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'
marca
fonte
Você pode obter respostas diferentes aqui se tentar configurações diferentes para SET DATEFIRST.
Aaron Bertrand
5
Bem, eu não votei contra, mas sua resposta não mencionou DATEFIRSTnada (há três anos e meio), e ainda não menciona . E você também deve evitar formatos regionais como m/d/y, mesmo em cenários onde m e d são iguais.
Aaron Bertrand