лекция. Учебник по sql если вы хотите узнать, что такое sql этот сайт для вас
Скачать 7.88 Mb.
|
Первый день недели Задача. Определить дату, на которую выпал первый понедельник января 2013 года. При некоторых предположениях решить эту задачу можно следующим образом: 1. WITH num ( n ) AS ( /* с помощью рекурсивного CTE создаем таблицу со столбцом n 2. и значениями от 0 до 6 */ 3. SELECT 0 4. UNION ALL 5. SELECT n +1 FROM num 6. WHERE n < 6 ) , 7. dat AS ( /* создаем таблицу с датами от 1 до 7 января 2013 года */ 8. SELECT DATEADD ( dd, n, CAST ( '2013-01-01' AS DATETIME )) AS day FROM num 9. ) 10. SELECT day FROM dat WHERE DATEPART ( dw, day ) = 1 ; /* выбираем день, соответствующий 11. первому дню недели */ Предположение, о котором говорилось выше, состоит в том, что первым днем недели считается понедельник. Однако если вы выполните этот запрос на сайте sql-ex.ru, то получите day 2013-01-06 А это - воскресенье. Причина в том, что настройки на сайте полагают первым днем недели воскресенье. А можно ли написать решение, которое не зависело бы от настроек сервера? Попробуем сделать так: будем не номер дня использовать при фильтрации, а его название (поменяется лишь последняя строка, но я повторяю весь запрос с тем, чтобы его можно было выполнить без редактирования): 1. WITH num ( n ) AS ( SELECT 0 2. UNION ALL 3. SELECT n +1 FROM num 4. WHERE n < 6 ) , 5. dat AS ( 6. SELECT DATEADD ( dd, n, CAST ( '2013-01-01' AS DATETIME )) AS day FROM num 7. ) 8. SELECT day FROM dat WHERE DATENAME ( dw, day ) = 'monday' ; day 2013-01-07 Теперь правильно, но будет ли этот запрос всегда верен, если "зашить" его в код приложения? Ответ - нет. Если я поменяю языковые настройки: 1. SET LANGUAGE russian; то получу пустой набор строк, поскольку в этом случае последнюю строку запроса следовало бы написать так 1. SELECT day FROM dat WHERE DATENAME ( dw, day ) = N 'понедельник' ; Функция @@DATEFIRST @@DATEFIRST возвращает число, которое определяет первый день недели, установленный для текущей сессии. При этом 1 соответствует понедельнику, а 7, соответственно, воскресенью. Т.е. если 1. SELECT @@DATEFIRST; возвращает 7, то первым днем недели считается воскресенье (соответствует текущим настройкам на сайте). Для того, чтобы решение нашей задачи не зависело от значения, установленного для первого дня недели, воспользуемся функцией @@DATEFIRST. Сделать это можно, например, так 1. WITH num ( n ) AS ( 2. SELECT 0 3. UNION ALL 4. SELECT n +1 FROM num 5. WHERE n < 6 ) , 6. dat AS ( 7. SELECT DATEADD ( dd, n, CAST ( '2013-01-01' AS DATETIME )) AS day FROM num 8. ) 9. SELECT day, DATENAME ( dw, day ) week_day FROM dat WHERE DATEPART ( dw, day ) = 10. 1 + ( 8 -@@DATEFIRST ) % 7 ; day week_day 2013-01-07 Monday Чтобы изменить значение первого дня недели (на время текущей сессию), можно использовать оператор SET DATEFIRST. Так, если выполнить следующий код 1. SET DATEFIRST 1 ; 2. SELECT @@DATEFIRST; то мы получим значение 1, т.е. неделя теперь начинается с понедельника. Настройку первого дня недели может также поменять изменение языка сессии. Например, если мы выполним 1. SET LANGUAGE us_english; 2. SELECT @@DATEFIRST; то опять получим 7 (воскресенье), поскольку это значение DATEFIRST принято по умолчанию для английского (американского) языка. В то время как выбор русского языка (SET LANGUAGE russian;) сделает первым днем недели понедельник. Однако вы можете поменять параметры языка и первого дня недели одновременно, чтобы, скажем, получить английский язык, и при этом педеля будет начинаться с понедельника: 1. SET DATEFIRST 1 ; 2. SET LANGUAGE us_english; 3. SELECT @@DATEFIRST; Языковые настройки влияют, в частности, на символьное представление компонентов даты/времени. Сравните, например, результаты: 1. DECLARE @dt DATE = '2012-12-17' ; -- 17 декабря 2012 года 2. SET LANGUAGE us_english; 3. SELECT DATENAME ( DW, @dt ) AS day_of_week, DATENAME ( MONTH,@dt ) AS month; day_of_week month Monday December 1. SET LANGUAGE russian; 2. SELECT DATENAME ( DW, @dt ) AS day_of_week, DATENAME ( MONTH, @dt ) AS month; day_of_week month понедельник Декабрь 1. SET LANGUAGE german; 2. SELECT DATENAME ( DW, @dt ) AS day_of_week, DATENAME ( MONTH, @dt ) AS month; day_of_week month Montag Dezember Вывод. Если вы хотите, чтобы зависящие от настроек сервера/базы запросы всегда давали верный результат, можно задавать необходимые настроечные параметры для сессии, в которой эти запросы выполняются, или же проверять в каждом подобном запросе значения соответствующих параметров, в частности, с помощью функции @@DATEFIRST. Особенности: MSSQL Решить эту задачу (Определить дату, на которую выпал первый понедельник января 2013 года.) можно и без применения функции @@DATEFIRST, воспользовавшись методом, предложенным Ициком Бен-Ганом [ 8 ]. Идея решения заключается в том, что: 1. Первое января 1900г. было понедельником. 2. Количество дней между двумя одинаковыми днями недели всегда кратно семи. 1. declare @anchor_date datetime 2. declare @reference_date datetime 3. SELECT @anchor_date= '19000101' , @reference_date= '20130505' 4. SELECT DATEADD ( day, DATEDIFF ( day, @anchor_date, 5. DATEADD ( year, DATEDIFF ( year, '19000101' , @reference_date ) , '19000101' ) - 1 ) / 7 * 7 + 7 , 6. @anchor_date ) ; Особенности: MSSQL Можно упомянуть еще один способ решения этой задачи без @@DATEFIRST, который использует сравнение любой из функций DATEPART/DATENAME с такой же функцией от даты, которая ЗАВЕДОМО является понедельником. Например, от уже упомянутого 1-го января 1900г. Тогда первоначальный запрос из этой главы выглядел бы так: 1. SELECT day FROM dat WHERE DATEPART ( dw, day ) = DATEPART ( dw, '19000101' ) ; или так 1. SELECT day FROM dat WHERE DATENAME ( dw, day ) = DATENAME ( dw, '19000101' ) ; Функция DATEFROMPARTS Функция DATEFROMPARTS появилась в SQL Server версии 2012. У функции DATEFROMPARTS 3 целочисленных аргумента, представляющих собой год, месяц и день, а возвращаемое значение есть соответствующая дата типа DATE. Она даёт удобный способ формирования даты, когда её компоненты хранятся отдельно или передаются с клиента. Например, 1. SELECT DATEFROMPARTS ( 2017 , 5 , 25 ) ; вернёт 2017-05-25 Получить дату 25 числа текущего месяца можно так 1. SELECT DATEFROMPARTS ( YEAR ( CURRENT_TIMESTAMP ) , MONTH ( CURRENT_TIMESTAMP ) , 25 ) ; Помимо даты можно "собирать" значения типа времени и временной метки (datetime), используя компоненты времени - часы, минуты, секунды и доли секунды. Например, следующий запрос 1. SELECT TIMEFROMPARTS ( 9 , 38 , 59 , 998 , 7 ) ; даст значение типа TIME: 09:38:59.0000998 Обратите внимание на последний параметр, который указывает на число знаков в представлении долей секунды. Этот целочисленный параметр может принимать значение от 0 до 7, но не может быть меньше числа цифр в представлении долей секунды. Например, 1. SELECT TIMEFROMPARTS ( 9 , 38 , 59 , 998 , 3 ) ; вернет 09:38:59.998 в то время как 1. SELECT TIMEFROMPARTS ( 9 , 38 , 59 , 998 , 2 ) ; вернет ошибку: Cannot construct data type time, some of the arguments have values which are not valid. (Не удалось сконструировать тип данных time. Некоторые аргументы имеют недопустимые значения.) Сравните с 1. SELECT TIMEFROMPARTS ( 9 , 38 , 59 , 098 , 2 ) , TIMEFROMPARTS ( 9 , 38 , 59 , 098 , 3 ) ; Результат 09:38:59.98 09:38:59.098 Функция DATETIMEFROMPARTS формирует значение типа DATETIME. Она имет семь целочисленных параметров: год, месяц, день, часы, минуты, секунды, миллисекунды. При этом миллисекунды округляются с точностью до одного из значений: .000, .003, .007. 1. SELECT DATETIMEFROMPARTS ( 2017 , 5 , 13 , 9 , 38 , 59 , 998 ) , 2. DATETIMEFROMPARTS ( 2017 , 5 , 13 , 9 , 38 , 59 , 999 ) , 3. DATETIMEFROMPARTS ( 2017 , 5 , 13 , 9 , 38 , 59 , 993 ) ; Результат 2017-05-13 09:38:59.997 2017-05-13 09:39:00.000 2017-05-13 09:38:59.993 Функции работы со строками в MS SQL SERVER Вот полный перечень функций работы со строками, взятый из BOL: ASCII NCHAR SOUNDEX CHAR PATINDEX SPACE CHARINDEX REPLACE STR DIFFERENCE QUOTENAME STUFF LEFT REPLICATE SUBSTRING LEN REVERSE UNICODE LOWER RIGHT UPPER LTRIM RTRIM Функции ASCII и CHAR Начнем с двух взаимно-обратных функций — ASCII и CHAR: Функция ASCII возвращает ASCII-код крайнего левого символа строкового выражения, являющегося аргументом функции. Вот, например, как можно определить, сколько имеется разных букв, с которых начинаются названия кораблей в таблице Ships: 1. SELECT COUNT ( DISTINCT ASCII ( name )) 2. FROM Ships; Результат — 11. Чтобы выяснить, какие это буквы, мы можем применить функцию CHAR, которая возвращает символ по известному ASCII-коду (от 0 до 255): 1. SELECT DISTINCT CHAR ( ASCII ( name )) 2. FROM Ships 3. ORDER BY 1 ; Следует отметить, что аналогичный результат можно получить проще с помощью еще одной функции — LEFT. Функция LEFT Функция имеет следующий синтаксис: 1. LEFT ( строковое выражение, целочисленное выражение ) и вырезает заданное вторым аргументом число символов слева из строки, являющейся первым аргументом. Итак, 1. SELECT DISTINCT LEFT ( name, 1 ) 2. FROM Ships 3. ORDER BY 1 ; А вот как, например, можно получить таблицу кодов всех алфавитных символов: 1. SELECT CHAR ( ASCII ( 'a' ) + num -1 ) letter, ASCII ( 'a' ) + num - 1 [ code ] 2. FROM ( SELECT 5 * 5 * ( a -1 ) +5 * ( b -1 ) + c AS num 3. FROM ( SELECT 1 a UNION ALL SELECT 2 UNION ALL SELECT 3 4. UNION ALL SELECT 4 UNION ALL SELECT 5 5. ) x CROSS JOIN 6. ( SELECT 1 b UNION ALL SELECT 2 UNION ALL SELECT 3 7. UNION ALL SELECT 4 UNION ALL SELECT 5 8. ) y CROSS JOIN 9. ( SELECT 1 c UNION ALL SELECT 2 UNION ALL SELECT 3 10. UNION ALL SELECT 4 UNION ALL SELECT 5 11. ) z 12. ) x 13. WHERE ASCII ( 'a' ) + num -1 BETWEEN ASCII ( 'a' ) AND ASCII ( 'z' ) ; Здесь используется алгоритм генерации числовой последовательности , изложенный в главе 8. Как известно, коды строчных и прописных букв отличаются. Поэтому, чтобы получить полный набор без переписывания запроса, достаточно просто дописать к вышеприведенному коду аналогичный: 1. UNION 2. SELECT CHAR ( ASCII ( 'A' ) + num -1 ) letter, ASCII ( 'A' ) + num - 1 [ code ] 3. FROM ( SELECT 5 * 5 * ( a -1 ) +5 * ( b -1 ) + c AS num 4. FROM ( SELECT 1 a UNION ALL SELECT 2 UNION ALL SELECT 3 5. UNION ALL SELECT 4 UNION ALL SELECT 5 6. ) x CROSS JOIN 7. ( SELECT 1 b UNION ALL SELECT 2 UNION ALL SELECT 3 8. UNION ALL SELECT 4 UNION ALL SELECT 5 9. ) y CROSS JOIN 10. ( SELECT 1 c UNION ALL SELECT 2 UNION ALL SELECT 3 11. UNION ALL SELECT 4 UNION ALL SELECT 5 12. ) z 13. ) x 14. WHERE ASCII ( 'A' ) + num -1 BETWEEN ASCII ( 'A' ) AND ASCII ( 'Z' ) Чтобы таблица выглядела более патриотично, достаточно заменить латинские буквы a и A на неотличимые на вид русские — а и А, а z и Z на я и Я. Вот только буквы «ё» вы не увидите в этой таблице, так как в кодовой таблице ASCII эти символы лежат отдельно, что легко проверить: 1. SELECT ASCII ( 'ё' ) 2. UNION ALL 3. SELECT ASCII ( 'Ё' ) ; Полагаем, что для вас не составит сложности добавить эту букву в таблицу при необходимости. Рассмотрим теперь задачу определения нахождения искомой подстроки в строковом выражении. Для этого могут использоваться две функции — CHARINDEX и PATINDEX. Обе они возвращают начальную позицию (позицию первого символа подстроки) подстроки в строке. Функции CHARINDEX и PATINDEX Функция CHARINDEX имеет синтаксис: 1. CHARINDEX ( искомое_выражение, строковое_выражение [ , стартовая_позиция ]) Здесь необязательный целочисленный параметр стартовая_позиция определяет позицию в строковом выражении, начиная с которой выполняется поиск. Если этот параметр опущен, поиск выполняется от начала строкового_выражения. Например, запрос 1. SELECT name 2. FROM Ships 3. WHERE CHARINDEX ( 'sh' , name ) > 0 ; будет выводить те корабли, в которых имеется сочетание символов «sh». Здесь используется тот факт, что если искомая строка не будет обнаружена, то функция CHARINDEX возвращает 0. Результат выполнения запроса будет содержать следующие корабли: name Kirishima Musashi Washington Следует отметить, что если искомая подстрока либо строковое выражение есть NULL, то результатом функции тоже будет NULL. Следующий пример определяет позиции первого и второго вхождения символа a в имени корабля California: 1. SELECT CHARINDEX ( 'a' ,name ) first_a, 2. CHARINDEX ( 'a' , name, CHARINDEX ( 'a' , name ) +1 ) second_a 3. FROM Ships 4. WHERE name= 'California' ; Обратите внимание, что при определении второго символа в функции CHARINDEX используется стартовая позиция, которой является позиция следующего за первой буквой a символа — CHARINDEX('a', name)+1. Правильность результата — 2 и 10 — легко проверить. Функция PATINDEX имеет следующий синтаксис: 1. PATINDEX ( '%образец%' , строковое_выражение ) Главное отличие этой функции от CHARINDEX заключается в том, что поисковая строка может содержать подстановочные знаки — % и _. При этом концевые знаки % являются обязательными. Например, использование этой функции в первом примере будет иметь вид 1. SELECT name 2. FROM Ships 3. WHERE PATINDEX ( '%sh%' , name ) > 0 ; А вот, например, как можно найти имена кораблей, которые содержат последовательность из трех символов, первый и последний из которых есть e: 1. SELECT name 2. FROM Ships 3. WHERE PATINDEX ( '%e_e%' , name ) > 0 ; Результат выполнения этого запроса выглядит следующим образом: Name Revenge Royal Sovereign Функция RIGHT Парная к LEFT функция RIGHT возвращает заданное число символов справа из строкового выражения: 1. RIGHT ( строковое выражения, число символов ) Вот, например, как можно определить имена кораблей, которые начинаются и заканчиваются на одну и ту же букву: 1. SELECT name 2. FROM Ships 3. WHERE LEFT ( name, 1 ) = RIGHT ( name, 1 ) ; То, что в результате мы получим пустой результирующий набор, означает, что таких кораблей в базе данных нет. Давайте возьмем комбинацию значений — класс и имя корабля. Соединение двух строковых значений в одно называется конкатенацией, и в SQL Server для этой операции используется знак «+» (в стандарте «||»). Итак, 1. SELECT * 2. FROM ( SELECT class + ' ' + name AS cn 3. FROM Ships 4. ) x 5. WHERE LEFT ( cn, 1 ) = RIGHT ( cn, 1 ) ; Здесь мы разделяем пробелом имя класса и имя корабля. Кроме того, чтобы не повторять всю конструкцию в качестве аргумента функции, используем подзапрос. Результат будет иметь вид: |