Получить первый день недели в SQL Server


Я пытаюсь сгруппировать записи по неделям, сохраняя агрегированную дату как первый день недели. Однако стандартная методика, которую я использую для округления дат, не работает правильно с неделями (хотя она работает для дней, месяцев, лет, кварталов и любого другого таймфрейма, к которому я ее применил).

вот SQL:

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

возвращает 2011-08-22 00:00:00.000, это понедельник, а не воскресенье. Выбор @@datefirst возвращает 7, который является кодом для воскресенья, так сервер настроен правильно, насколько я знаю.

Я могу обойти это достаточно легко, изменив приведенный выше код, чтобы:

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

но тот факт, что я должен сделать такое исключение, заставляет меня немного беспокоиться. Кроме того, извиняюсь, если это дублирующий вопрос. Я нашел некоторые связанные вопросы, но ни один из них не касался этого аспекта конкретно.

13 78

13 ответов:

чтобы ответить, почему вы получаете понедельник, а не воскресенье:

вы добавляете несколько недель к дате 0. Что дата 0? 1900-01-01. Какой был день на 1900-01-01? Понедельник. Так что в вашем коде вы говорите, сколько недель прошло с понедельника, 1 января 1900 года? Давайте назовем это [n]. Хорошо, теперь добавьте [n] недель к понедельнику, 1 января 1900 года. Вы не должны удивляться, что это заканчивается в понедельник. DATEADD понятия не имеет, что вы хотите добавить недели, но только пока вы не доберетесь до воскресенья, это просто добавление 7 дней, а затем добавление еще 7 дней ... так же, как DATEDIFF распознает только границы, которые были пересечены. Например, они оба возвращают 1, хотя некоторые люди жалуются, что должна быть какая-то разумная логика, встроенная в округление вверх или вниз:

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

чтобы ответить, как получить в воскресенье:

если вы хотите воскресенье, то выберите базовую дату, которая не понедельник, а скорее воскресенье. Для пример:

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

этот не сломается, если вы измените свой DATEFIRST настройка (или ваш код выполняется для пользователя с другой настройкой) - при условии, что вы все еще хотите воскресенье независимо от текущей настройки. Если вы хотите, чтобы эти два ответа jive, то вы должны использовать функцию, которая тут зависит от DATEFIRST настройки, например,

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

так что если вы измените свой DATEFIRST установка на понедельник, вторник, что у вас есть, поведение изменится. В зависимости от того, какое поведение вы хотите, вы можете использовать одну из этих функций:

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

...или...

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

теперь, у вас есть много вариантов, но какой из них работает лучше? Я был бы удивлен, если бы были какие - либо серьезные различия, но я собрал все ответы, представленные до сих пор, и провел их через два набора тестов-один дешевый и один дорогой. Я измерил статистику клиента, потому что я не вижу ввода-вывода или памяти, играющих роль в производительности здесь (хотя они могут вступать в игру в зависимости от того, как используется функция). В моих тестах результаты таковы:

" дешевый " запрос назначения:

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

" дорогой " запрос назначения:

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

я могу передать детали моих тестов при желании-остановившись здесь, поскольку это уже становится довольно многословным. Я был немного удивлен, увидев, что Курт вышел как самый быстрый в верхнем конце, учитывая количество вычислений и встроенный код. Может быть, я проведу более тщательную проверку тесты и блог об этом... если вы, ребята, не возражаете, чтобы я опубликовал ваши функции в другом месте.

для этих, которые нужно получить:

понедельник = 1 и воскресенье = 7:

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

воскресенье = 1 и суббота = 7:

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

выше был аналогичный пример, но благодаря двойному "%7" он будет намного медленнее.

Это прекрасно работает для меня:

CREATE FUNCTION [dbo].[StartOfWeek]
(
  @INPUTDATE DATETIME
)
RETURNS DATETIME

AS
BEGIN
  -- THIS does not work in function.
  -- SET DATEFIRST 1 -- set monday to be the first day of week.

  DECLARE @DOW INT -- to store day of week
  SET @INPUTDATE = CONVERT(VARCHAR(10), @INPUTDATE, 111)
  SET @DOW = DATEPART(DW, @INPUTDATE)

  -- Magic convertion of monday to 1, tuesday to 2, etc.
  -- irrespect what SQL server thinks about start of the week.
  -- But here we have sunday marked as 0, but we fix this later.
  SET @DOW = (@DOW + @@DATEFIRST - 1) %7
  IF @DOW = 0 SET @DOW = 7 -- fix for sunday

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

END

погуглил этот скрипт:

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

может быть, вам нужно это:

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

или

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

функции

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
CREATE FUNCTION dbo.fnFirstWorkingDayOfTheWeek
(
    @currentDate date
)
RETURNS INT
AS
BEGIN
    -- get DATEFIRST setting
    DECLARE @ds int = @@DATEFIRST 
    -- get week day number under current DATEFIRST setting
    DECLARE @dow int = DATEPART(dw,@currentDate) 

    DECLARE @wd  int =  1+(((@dow+@ds) % 7)+5) % 7  -- this is always return Mon as 1,Tue as 2 ... Sun as 7 

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

END

для тех, кому нужен ответ на работе и создание функции запрещено вашим DBA, будет работать следующее решение:

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

это дает начало этой недели. Здесь я предполагаю, что воскресенье-это начало недели. Если вы думаете, что понедельник-это начало, вы должны использовать:

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

поскольку юлианская дата 0-это понедельник, просто добавьте количество недель в воскресенье который за день до -1 например. выберите dateadd(wk,datediff (wk,0,getdate ()), -1)

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

Это моя логика. Установите первую неделю в понедельник, затем вычислите, какой день недели дает день, а затем с помощью DateAdd и Case я вычисляю, какая дата была бы в предыдущий понедельник этой недели.

У меня нет никаких проблем с любым из ответов, приведенных здесь, однако я думаю, что мой намного проще реализовать и понять. Я не запускал никаких тестов производительности на нем, но он должен быть отрицательным.

поэтому я получил свой ответ из того факта, что даты хранятся в SQL server как целые числа (я говорю только о компоненте даты). Если вы мне не верите, попробуйте выбрать CONVERT(INT, GETDATE ()) и наоборот.

теперь зная это, вы можете сделать некоторые классные математические уравнения. Вы могли бы придумать что-нибудь получше, но вот мое.

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

У меня была похожая проблема. Учитывая дату, я хотел получить дату понедельника на этой неделе.

Я использовал следующую логику: найти номер дня в неделю в диапазоне 0-6, а затем вычесть, что от даты originay.

Я использовал: DATEADD (day,-(DATEPART(weekday,)+5)%7,)

Так как DATEPRRT (будний день,) возвращает 1 = воскресенье ... 7=суббота, DATEPART (будний день,)+5)%7 возвращает 0=понедельник ... 6=воскресенье.

вычитание этого количества дней из исходная дата дает предыдущий понедельник. Такая же техника может быть использована для любого начального дня недели.

Я нашел это простым и полезным. Работает, даже если первый день недели воскресенье или понедельник.

объявить @BaseDate как дату

SET @BaseDate = GETDATE ()

объявить @FisrtDOW как дату

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

может быть, я слишком упрощаю здесь, и это может быть так, но это, кажется, работает для меня. Еще не сталкивался с какими-либо проблемами с ним...

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'