Obtenga el primer día de la semana en SQL Server

96

Estoy tratando de agrupar registros por semana, almacenando la fecha agregada como el primer día de la semana. Sin embargo, la técnica estándar que utilizo para redondear fechas no parece funcionar correctamente con semanas (aunque sí lo hace con días, meses, años, trimestres y cualquier otro período de tiempo al que lo haya aplicado).

Aquí está el SQL:

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

Esto vuelve 2011-08-22 00:00:00.000, que es un lunes, no un domingo. Al seleccionar @@datefirstdevoluciones 7, que es el código del domingo, para que el servidor esté configurado correctamente hasta donde yo sé.

Puedo omitir esto con bastante facilidad cambiando el código anterior a:

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

Pero el hecho de que tenga que hacer tal excepción me inquieta un poco. Además, disculpas si esta es una pregunta duplicada. Encontré algunas preguntas relacionadas, pero ninguna que abordara este aspecto específicamente.

Rápido Joe Smith
fuente
9
(@@DATEFIRST + DATEPART(DW, @SomeDate)) % 7permanece constante independientemente del @@datefirstentorno, creo. Con lunes = 2.
Martin Smith

Respuestas:

148

Para responder por qué tiene un lunes y no un domingo:

Está agregando un número de semanas a la fecha 0. ¿Qué es la fecha 0? 1900-01-01. ¿Qué día fue el 01/01/1900? Lunes. Entonces, en su código está diciendo, ¿cuántas semanas han pasado desde el lunes 1 de enero de 1900? Llamemos a eso [n]. Bien, ahora agregue [n] semanas al lunes 1 de enero de 1900. No debería sorprenderse de que termine siendo lunes. DATEADDno tiene idea de que desea agregar semanas, pero solo hasta que llegue un domingo, solo agrega 7 días, luego agrega 7 días más, ... al igual que DATEDIFFsolo reconoce los límites que se han cruzado. Por ejemplo, ambos devuelven 1, aunque algunas personas se quejan de que debería haber alguna lógica sensata incorporada para redondear hacia arriba o hacia abajo:

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

Para responder cómo conseguir un domingo:

Si desea un domingo, elija una fecha base que no sea un lunes sino un domingo. Por ejemplo:

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

Esto no se interrumpirá si cambia su DATEFIRSTconfiguración (o su código se está ejecutando para un usuario con una configuración diferente), siempre que aún desee un domingo independientemente de la configuración actual. Si quieres esas dos respuestas a jive, entonces usted debe utilizar una función que no dependen de la DATEFIRSTconfiguración, por ejemplo,

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

Entonces, si cambia su DATEFIRSTconfiguración a lunes, martes, lo que sea, el comportamiento cambiará. Según el comportamiento que desee, puede utilizar una de estas funciones:

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

...o...

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

Ahora, tiene muchas alternativas, pero ¿cuál funciona mejor? Me sorprendería si hubiera diferencias importantes, pero recopilé todas las respuestas proporcionadas hasta ahora y las ejecuté a través de dos conjuntos de pruebas: una barata y otra costosa. Medí las estadísticas del cliente porque no veo que la E / S o la memoria desempeñen un papel en el rendimiento aquí (aunque pueden entrar en juego dependiendo de cómo se use la función). En mis pruebas los resultados son:

Consulta de asignación "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 asignación "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

Puedo transmitir los detalles de mis pruebas si lo deseo, deteniéndome aquí, ya que esto ya se está volviendo bastante largo. Me sorprendió un poco ver a Curt salir como el más rápido en el extremo superior, dada la cantidad de cálculos y el código en línea. Tal vez haga algunas pruebas más exhaustivas y escribo un blog al respecto ... si ustedes no tienen ninguna objeción a que publique sus funciones en otro lugar.

Aaron Bertrand
fuente
Entonces, si considero que mis semanas comienzan el domingo y terminan el sábado, puedo obtener el último día de la semana para cualquier fecha @d así: SELECT DATEADD (wk, DATEDIFF (wk, '19041231', @d), '19041231')
Baodad
21

Para estos que necesitan obtener:

Lunes = 1 y domingo = 7:

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

Domingo = 1 y sábado = 7:

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

Arriba había un ejemplo similar, pero gracias al doble "% 7" sería mucho más lento.

Kakkarot
fuente
Esto también funciona muy bien para obtener el número de día desde el comienzo de la semana siendo Domingo o Lunes Gracias
Fandango68
Alternativamente select (datediff(dd,5,cal.D_DATE)%7 + 1)yselect (datediff(dd,6,cal.D_DATE)%7 + 1)
vasja
8

Para aquellos que necesitan la respuesta en el trabajo y la función de creación está prohibida por su DBA, la siguiente solución funcionará:

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

Esto da el comienzo de esa semana. Aquí asumo que los domingos son el comienzo de las semanas. Si cree que el lunes es el comienzo, debe usar:

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

Esto funciona maravillosamente para mí:

CREAR FUNCIÓN [dbo]. [StartOfWeek]
(
  @INPUTDATE DATETIME
)
DEVOLUCIONES DATETIME

COMO
EMPEZAR
  - ESTO no funciona en función.
  - SET DATEFIRST 1 - establece el lunes como el primer día de la semana.

  DECLARE @DOW INT - para almacenar el día de la semana
  SET @INPUTDATE = CONVERT (VARCHAR (10), @INPUTDATE, 111)
  SET @DOW = DATEPART (DW, @INPUTDATE)

  - Conversión mágica de lunes a 1, martes a 2, etc.
  - independientemente de lo que piense el servidor SQL sobre el comienzo de la semana.
  - Pero aquí tenemos el domingo marcado como 0, pero lo arreglamos más tarde.
  SET @DOW = (@DOW + @@ DATEFIRST - 1)% 7
  IF @DOW = 0 SET @DOW = 7 - arreglar para el domingo

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

FINAL
trailmax
fuente
Esto parece volver el lunes dada la fecha de hoy, no el domingo. El OP ya tiene una función que regresa el lunes, quiere que regrese el domingo. :-)
Aaron Bertrand
doh! Debería leer las preguntas con más cuidado la próxima vez. Sin embargo, mi solución se puede ajustar fácilmente, si aún es necesario. Parece que OP está contento con la respuesta aceptada de todos modos -)
trailmax
Esta es la solución correcta en mi máquina, ya que para mí: DATEADD (ww, DATEDIFF (ww, 0, CONVERT (DATE, '2017-10-8')), 0) devuelve 2017-10-9!
Ejecute CMD el
3

Busqué en Google 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

Brusco
fuente
2

Quizás necesites esto:

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

O

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

Función

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
fuente
6
DATEPART(DWdepende de@@datefirst
Martin Smith
Me gusta la sencillez de este. También parece funcionar bastante bien para conjuntos de datos muy grandes.
Quick Joe Smith
2
¿Por qué no simplemente hacer el parámetro de entrada, DATEentonces no tiene que hacer ninguna conversión subóptima ay VARCHARviceversa solo para eliminar cualquier componente de tiempo accidental que se haya pasado?
Aaron Bertrand
Se utilizó la función Convertir porque el valor devuelto no necesita Timevalores.
Gandarez
1
Sí, pero el punto es que convertir a varchar y viceversa es caro. Si solo tiene un parámetro FECHA, entonces no le importa si se incluyó el tiempo ... se lo quita.
Aaron Bertrand
2
CREAR FUNCIÓN dbo.fnFirstWorkingDayOfTheWeek
(
    @currentDate fecha
)
DEVOLUCIONES INT
COMO
EMPEZAR
    - obtener el ajuste DATEFIRST
    DECLARE @ds int = @@ DATEFIRST 
    - obtener el número de día de la semana en la configuración actual DATEFIRST
    DECLARAR @dow int = DATEPART (dw, @ currentDate) 

    DECLARE @wd int = 1 + (((@ dow + @ ds)% 7) +5)% 7 - esto siempre es devolver Mon como 1, Tue como 2 ... Sun como 7 

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

FINAL
JG JIN
fuente
Esta es la única función que me funcionó en SQL Server 2005. Gracias
Fandango68
@ Fernando68 ¿Puedes explicar cómo otras soluciones no funcionaron?
Aaron Bertrand
@AaronBertrand lo siento, no lo recuerdo, pero creo que me estaba enfocando en una respuesta rápida y probé la tuya, pero por alguna razón no funcionó para mí.
Fandango68
@ Fernando68 Bueno, eso es muy útil. : - \
Aaron Bertrand
2

Para lo básico (el domingo de la semana actual)

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

Si la semana anterior:

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

Internamente, creamos una función que lo hace, pero si lo necesita rápido y sucio, esto lo hará.

JamesDavisSr
fuente
0

Dado que la fecha juliana 0 es un lunes, simplemente agregue el número de semanas al domingo, que es el día anterior a -1. seleccionar fechaadd (semana, fechadif (semana, 0, obtener fecha ()), - 1)

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

Esta es mi lógica. Establezca el primer día de la semana para que sea el lunes, luego calcule cuál es el día de la semana en un día determinado, luego, usando DateAdd y Case, calculo cuál habría sido la fecha el lunes anterior de esa semana.

usuario2479728
fuente
-1

No tengo ningún problema con ninguna de las respuestas que se dan aquí, sin embargo, creo que la mía es mucho más sencilla de implementar y comprender. No he realizado ninguna prueba de rendimiento en él, pero debería ser despreciable.

Entonces, obtuve mi respuesta del hecho de que las fechas se almacenan en el servidor SQL como números enteros (estoy hablando solo del componente de fecha). Si no me cree, intente esto SELECT CONVERT (INT, GETDATE ()), y viceversa.

Ahora, sabiendo esto, puedes hacer algunas ecuaciones matemáticas geniales. Es posible que se te ocurra uno mejor, pero aquí está el mío.

/*
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
fuente
1
Encuentro que esto no funciona para mí. My @@DATEFIRSTtambién es 7, pero si tu @testDatees el comienzo de la semana, esto devuelve una fecha que es el día anterior.
row1
-1

Tuve un problema similar. Dada una fecha, quería obtener la fecha del lunes de esa semana.

Usé la siguiente lógica: Encuentre el número de día de la semana en el rango de 0-6, luego reste eso de la fecha de origen.

Usé: DATEADD (día, - (DATEPART (día de la semana,) + 5)% 7,)

Dado que DATEPRRT (día de la semana,) devuelve 1 = domingo ... 7 = sábado, DATEPART (día de la semana,) + 5)% 7 devuelve 0 = lunes ... 6 = domingo.

Restando este número de días de la fecha original, se obtiene el lunes anterior. La misma técnica se puede utilizar para cualquier día de inicio de la semana.

Pat Lane
fuente
-1

Encontré esto simple y útil. Funciona incluso si el primer día de la semana es domingo o lunes.

DECLARAR @BaseDate como fecha

SET @BaseDate = GETDATE ()

DECLARAR @FisrtDOW COMO Fecha

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

ElJoel
fuente
-3

Quizás estoy simplificando demasiado aquí, y ese puede ser el caso, pero esto parece funcionar para mí. No he tenido ningún problema con él todavía ...

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
fuente
Puede obtener diferentes respuestas aquí si prueba diferentes configuraciones para SET DATEFIRST.
Aaron Bertrand
5
Bueno, no voté en contra, pero tu respuesta no mencionó DATEFIRSTnada (desde hace tres años y medio), y todavía no lo hace. Y usted debe también evitar los formatos regionales como m/d/y, incluso en escenarios donde el m y D son los mismos.
Aaron Bertrand