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

97

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

Вот 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);

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

Быстрый Джо Смит
источник
9
(@@DATEFIRST + DATEPART(DW, @SomeDate)) % 7@@datefirstя думаю, остается постоянным независимо от настройки. С понедельником = 2.
Мартин Смит,

Ответы:

150

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

Вы добавляете число недель к дате 0. Что такое дата 0? 1900-01-01. Какой был день 01.01.1900? Понедельник. Итак, в вашем коде вы говорите, сколько недель прошло с понедельника, 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

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

Аарон Бертран
источник
Итак, если я считаю, что мои недели начинаются в воскресенье и заканчиваются в субботу, я могу получить последний день недели для любой даты @d следующим образом: SELECT DATEADD (wk, DATEDIFF (wk, '19041231', @d), '19041231')
Баодад
21

Для этого необходимо получить:

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

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

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

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

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

Каккарот
источник
Это также отлично работает, чтобы получить номер дня с начала недели, являющегося вс или пн. Спасибо
Fandango68
Альтернативно select (datediff(dd,5,cal.D_DATE)%7 + 1)иselect (datediff(dd,6,cal.D_DATE)%7 + 1)
vasja 02
8

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

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.....
Филип Чен
источник
5

У меня это прекрасно работает:

СОЗДАТЬ ФУНКЦИЮ [dbo]. [StartOfWeek]
(
  @INPUTDATE DATETIME
)
ВОЗВРАЩАЕТ СРОК

ТАК КАК
НАЧАТЬ
  - ЭТО не работает.
  - SET DATEFIRST 1 - установить понедельник как первый день недели.

  DECLARE @DOW INT - сохранить день недели
  НАБОР @INPUTDATE = ПРЕОБРАЗОВАТЬ (VARCHAR (10), @INPUTDATE, 111)
  УСТАНОВИТЬ @DOW = DATEPART (DW, @INPUTDATE)

  - Магическое преобразование понедельника в 1, вторника в 2 и т. Д.
  - неважно, что думает SQL-сервер о начале недели.
  - Но здесь воскресенье обозначено как 0, но мы исправим это позже.
  УСТАНОВИТЬ @DOW = (@DOW + @@ DATEFIRST - 1)% 7
  IF @DOW = 0 SET @DOW = 7 - исправить на воскресенье

  ВОЗВРАТ DATEADD (DD, 1 - @ DOW, @ INPUTDATE)

КОНЕЦ
trailmax
источник
Судя по сегодняшнему дню, это вернется в понедельник, а не в воскресенье. У OP уже есть функция, которая возвращает понедельник, он хочет, чтобы она вернула воскресенье. :-)
Аарон Бертран
дох! В следующий раз я должен читать вопросы более внимательно. Однако мое решение можно легко скорректировать, если оно все еще потребуется. Похоже, OP все равно доволен принятым ответом -)
trailmax
Это правильное решение для моей машины, поскольку для меня: DATEADD (ww, DATEDIFF (ww, 0, CONVERT (DATE, '2017-10-8')), 0) возвращает 2017-10-9!
Run CMD
3

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

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

Может вам это понадобится:

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
Гандарес
источник
6
DATEPART(DWзависит от@@datefirst
Мартин Смит
Мне нравится простота этого. Кажется, он неплохо работает и для очень больших наборов данных.
Quick Joe Smith
2
Почему бы просто не сделать входной параметр, DATEтогда вам не нужно будет выполнять какие-либо неоптимальные преобразования в VARCHARи обратно, просто чтобы удалить любой случайный компонент времени, который передается.
Аарон Бертран
Функция Convert использовалась, потому что возвращаемое значение не требует Timeзначений.
Гандарез,
1
Да, но дело в том, что преобразование в varchar и обратно обходится дорого. Если у вас есть только параметр DATE, вам все равно, было ли включено время ... оно удаляется для вас.
Аарон Бертран
2
СОЗДАТЬ ФУНКЦИЮ dbo.fnFirstWorkingDayOfTheWeek
(
    @currentDate дата
)
ВОЗВРАЩЕНИЕ INT
ТАК КАК
НАЧАТЬ
    - получить настройку DATEFIRST
    ОБЪЯВИТЬ @ds int = @@ DATEFIRST 
    - получить номер дня недели при текущей настройке DATEFIRST
    DECLARE @dow int = DATEPART (dw, @ currentDate) 

    DECLARE @wd int = 1 + (((@ dow + @ ds)% 7) +5)% 7 - всегда возвращать Mon как 1, Вт как 2 ... Sun как 7 

    ВОЗВРАТ DATEADD (dd, 1- @ wd, @ currentDate) 

КОНЕЦ
JG JIN
источник
Это единственная функция, которая работала у меня в SQL Server 2005. Спасибо
Fandango68
@ Fernando68 Можете объяснить, почему другие решения не работали?
Аарон Бертран
@AaronBertrand, извините, не помню, но я думаю, что сосредоточился на быстром ответе, и я попробовал ваш, но по какой-то причине у меня это не сработало.
Fandango68
@ Fernando68 Что ж, это очень полезно. : - \
Аарон Бертран
2

Для основного (воскресенье текущей недели)

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

Если на прошлой неделе:

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

Внутри мы создали функцию, которая делает это, но если вам нужно быстро и грязно, это сработает.

Джеймс Дэвис-старший
источник
0

Поскольку юлианская дата 0 - это понедельник, просто добавьте количество недель к воскресенью, то есть за день до -1. Например. выберите dateadd (wk, dateiff (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

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

user2479728
источник
-1

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

Итак, я получил свой ответ из того факта, что даты хранятся на сервере SQL как целые числа (я говорю только о компоненте даты). Если вы мне не верите, попробуйте этот SELECT 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))
Ryk
источник
1
Я считаю, что у меня это не работает. My @@DATEFIRSTтоже 7, но если у вас @testDateначало недели, то это возвращает дату, которая была накануне.
row1
-1

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

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

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

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

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

Пэт Лейн
источник
-1

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

ЗАЯВИТЬ @BaseDate как дату

НАБОР @BaseDate = GETDATE ()

ЗАЯВИТЬ @FisrtDOW КАК ДАТУ

ВЫБРАТЬ @FirstDOW = DATEADD (d, DATEPART (WEEKDAY, @ BaseDate) * -1 + 1, @BaseDate)

ЭльДжоэль
источник
-3

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

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'
отметка
источник
Здесь вы можете получить разные ответы, если попробуете другие настройки для SET DATEFIRST.
Аарон Бертран
5
Ну, я не голосовал против, но в вашем ответе вообще не упоминалось DATEFIRST(вот уже три с половиной года), и до сих пор нет. И вам также следует избегать региональных форматов, например m/d/y, даже в сценариях, где m и d совпадают.
Аарон Бертран