Первый день недели
Задача.
При некоторых предположениях решить эту задачу можно с помощью рекурсивного 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; /* выбираем день, соответствующий первому дню недели */
[[ 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';
[[ 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;
[[ 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;
[[ column ]] |
---|
NULL [[ value ]] |
day | week_day |
---|---|
2013-01-07 | Monday |
Чтобы изменить значение первого дня недели (на время текущей сессию), можно использовать оператор 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_week | month |
---|---|
Monday | December |
SET LANGUAGE russian;
SELECT DATENAME(DW, @dt) AS day_of_week, DATENAME(MONTH, @dt) AS month;
day_of_week | month |
---|---|
понедельник | Декабрь |
SET LANGUAGE german;
SELECT DATENAME(DW, @dt) AS day_of_week, DATENAME(MONTH, @dt) AS month;
day_of_week | month |
---|---|
Montag | Dezember |
Вывод. Если вы хотите, чтобы зависящие от настроек сервера/базы запросы всегда давали верный результат, можно задавать необходимые настроечные параметры для сессии, в которой эти запросы выполняются, или же проверять в каждом подобном запросе значения соответствующих параметров, в частности, с помощью функции @@DATEFIRST.
Особенности: MSSQL (Гершович В.И.)
Решить эту задачу (“Определить дату, на которую выпал первый понедельник января 2013 года.”) можно и без применения функции @@DATEFIRST, воспользовавшись методом, предложенным Ициком Бен-Ганом [8]. Идея решения заключается в том, что:
Первое января 1900г. было понедельником.
Количество дней между двумя одинаковыми днями недели всегда кратно семи.
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');