Первый день недели

Задача.

Определить дату, на которую выпал первый понедельник января 2013 года.

При некоторых предположениях решить эту задачу можно с помощью рекурсивного CTE:

WITH num(n) AS
(/* создаем таблицу со столбцом n и значениями от 0 до 6 */
    SELECT 0
    UNION ALL
    SELECT n+1 FROM num
    WHERE n < 6
),
dat AS 
(/* создаем таблицу с датами от 1 до 7 января 2013 года */
    SELECT DATEADD(dd, n, CAST('2013-01-01' AS DATETIME)) AS day 
    FROM num
)
SELECT day 
FROM dat 
WHERE DATEPART(dw, day) = 1; /* выбираем день, соответствующий первому дню недели */
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]

Предположение, о котором говорилось выше, состоит в том, что первым днем недели считается понедельник. Однако если вы выполните этот запрос на сайте sql-ex.ru, то получите

day
2013-01-06

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

WITH num(n) AS 
(
    SELECT 0
    UNION ALL
    SELECT n+1 FROM num
    WHERE n < 6
),
dat AS (
    SELECT DATEADD(dd, n, CAST('2013-01-01' AS DATETIME)) AS day 
    FROM num
)
SELECT day FROM dat WHERE DATENAME(dw, day) = 'monday';
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]
day
2013-01-07

Теперь правильно, но будет ли этот запрос всегда верен, если “зашить” его в код приложения? Ответ - нет. Если я поменяю языковые настройки:

SET LANGUAGE russian;

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

SELECT day FROM dat WHERE DATENAME(dw, day) = N'понедельник';

Функция @@DATEFIRST

@@DATEFIRST возвращает число, которое определяет первый день недели, установленный для текущей сессии. При этом 1 соответствует понедельнику, а 7, соответственно, воскресенью. Т.е. если

SELECT @@DATEFIRST;
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]

возвращает 7, то первым днем недели считается воскресенье (соответствует текущим настройкам на сайте).

Для того, чтобы решение нашей задачи не зависело от значения, установленного для первого дня недели, воспользуемся функцией @@DATEFIRST. Сделать это можно, например, так

WITH num(n) AS
(
    SELECT 0
    UNION ALL
    SELECT n+1 
    FROM num
    WHERE n < 6
),
dat AS (
    SELECT DATEADD(dd, n, CAST('2013-01-01' AS DATETIME)) AS day 
    FROM num
)
SELECT day, DATENAME(dw, day) week_day 
FROM dat 
WHERE DATEPART(dw, day) = 1+(8-@@DATEFIRST) % 7;
🚫
[[ error ]]
[[ column ]]
NULL [[ value ]]
dayweek_day
2013-01-07Monday

Чтобы изменить значение первого дня недели (на время текущей сессию), можно использовать оператор SET DATEFIRST. Так, если выполнить следующий код

SET DATEFIRST 1;
SELECT @@DATEFIRST;

то мы получим значение 1, т.е. неделя теперь начинается с понедельника.

Настройку первого дня недели может также поменять изменение языка сессии. Например, если мы выполним

SET LANGUAGE us_english;
SELECT @@DATEFIRST;

то опять получим 7 (воскресенье), поскольку это значение DATEFIRST принято по умолчанию для английского (американского) языка. В то время как выбор русского языка (SET LANGUAGE russian;) сделает первым днем недели понедельник.

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

SET DATEFIRST 1;
SET LANGUAGE us_english;
SELECT @@DATEFIRST;

Языковые настройки влияют, в частности, на символьное представление компонентов даты/времени. Сравните, например, результаты:

DECLARE @dt DATE = '2012-12-17'; -- 17 декабря 2012 года
SET LANGUAGE us_english;
SELECT DATENAME(DW, @dt) AS day_of_week, DATENAME(MONTH,@dt) AS month;
day_of_weekmonth
MondayDecember
SET LANGUAGE russian;
SELECT DATENAME(DW, @dt) AS day_of_week, DATENAME(MONTH, @dt) AS month;
day_of_weekmonth
понедельникДекабрь
SET LANGUAGE german;
SELECT DATENAME(DW, @dt) AS day_of_week, DATENAME(MONTH, @dt) AS month;
day_of_weekmonth
MontagDezember

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

Особенности: MSSQL (Гершович В.И.)

Решить эту задачу (“Определить дату, на которую выпал первый понедельник января 2013 года.”) можно и без применения функции @@DATEFIRST, воспользовавшись методом, предложенным Ициком Бен-Ганом [8]. Идея решения заключается в том, что:

  1. Первое января 1900г. было понедельником.

  2. Количество дней между двумя одинаковыми днями недели всегда кратно семи.

declare @anchor_date datetime   
declare @reference_date datetime  
select @anchor_date='19000101', @reference_date='20130505'  
  
SELECT DATEADD(day, DATEDIFF(day, @anchor_date,  
DATEADD(year, DATEDIFF(year, '19000101', @reference_date), '19000101') - 1) /7*7 + 7,  
@anchor_date);
Особенности: MSSQL (Курочкин П.А.)

Можно упомянуть еще один способ решения этой задачи без @@DATEFIRST, который использует сравнение любой из функций DATEPART/DATENAME с такой же функцией от даты, которая ЗАВЕДОМО является понедельником. Например, от уже упомянутого 1-го января 1900г.

Тогда первоначальный запрос из этой главы выглядел бы так:

SELECT day FROM dat WHERE DATEPART(dw, day) = DATEPART(dw, '19000101');

или так

SELECT day FROM dat WHERE DATENAME(dw, day) = DATENAME(dw, '19000101');

Рекомендуемые упражнения: 78, 99, 110, 118