На мой взгляд, возможности объектов просмотра слишком малы и далеки от идеала. Главная их проблема - статичность. Чтобы получить новый результат (добавить или изменить критерий поиска) приходится изменять саму хранимую процедуру, что достаточно проблематично и большая часть преимуществ объектов просмотра просто теряется.
Хранимые процедуры – это именованный набор операторов Transact-SQL хранящийся на сервере. Хранимые процедуры – это метод выполнения повторяющихся задач и при этом обладают большими возможностями, чем объекты просмотра.
Сервер SQL поддерживает 5 типов встроенных процедур:
Хранимые процедуры в MS SQL Server похожи на процедуры в других языках программирования. Если вы имели опыт программирования на каком-либо языке и не понаслышке знаете о таком понятии как процедуры, то материал этой главы покажется вам слишком простым. Но уровень подготовки читателей может быть разным, поэтому я постараюсь описать все максимально простым и доступным языком.
Итак, процедура - это блок из одной или более команд. Это может быть не просто один запрос, а целая программа, с собственной логикой (операторы IF), циклами. Процедура может принимать заранее определенные переменные и использовать их в своих расчетах, благодаря чему, результат работы процедуры может быть динамическим, и будет зависеть от определенных условий и/или состояния получаемых переменных.
В процедуре вы можете:
Выполнение хранимой процедуры включает ее создание, и после этого выполнение в первый раз, когда план выполнения помещается в кэш. Кэш процедур это пространство памяти, содержащее план выполнения всех выполняемых сейчас операторов Transact-SQL. Размер кэша изменяется, динамически соответствуя необходимому уровню. Если в кэше есть план выполнения, то процедура выполняется быстрее, за счет того, что серверу не нужно разбирать запрос и вырабатывать необходимые действия для решения поставленной задачи.
Когда хранимая процедура создается, операторы проверяются на синтаксическую корректность, чтобы в коде не было явных ошибок написания и использования операторов. На этом этапе логика работы еще не может быть проверена. Если синтаксис ошибочен, то возвращается ошибка и встроенная процедура не сохраняется. Если синтаксис корректен, то, сервер SQL сохраняет имя процедуры в системной таблице sysobjects, а текст в системной таблице syscomments текущей базы данных.
Процесс вызова имен с задержкой позволяет хранимым процедурам ссылаться на объекты, которые не существуют в момент создания процедуры. Этот процесс добавляет эластичности, потому что процедуры и объекты, на которые она ссылается, могут создаваться в любом порядке. Но при этом, все необходимые объекты должны существовать в момент выполнения процедуры. Имена проверяются именно во время выполнения процедуры.
Некоторые изменения в базе данных могут изменить план выполнения, делая его неэффективным или недействительным. Сервер определяет эти изменения и автоматически перекомпилирует план, когда возникает что-то из следующего:
Для определения оптимального плана выполнения, сервер определяет количество данных в используемых таблицах, наличие и тип индексов таблицы, распределение данных в индексных колонках, наличие объединений.
На этом теорию на время остановим и посмотрим, как на практике создаются хранимые процедуры. Для этого используется оператор CREATE PROCEDURE, который выглядит следующим образом:
CREATE PROC [ EDURE ] procedure_name [ ; number ] [ { @parameter data_type } [ VARYING ] [ = default ] [ OUTPUT ] ] [ ,...n ] [ WITH { RECOMPILE | ENCRYPTION | RECOMPILE , ENCRYPTION } ] [ FOR REPLICATION ] AS sql_statement [ ...n ]
Для создания процедуры, вы должны иметь соответствующие права, например, быть владельцем базы данных или администратором сервера базы данных.
Вы можете создать процедуры только в текущей базе данных, исключая временные процедуры, которые всегда создаются в базе данных tempdb. Создание процедуры похоже на создание объекта просмотра. Первым делом напишите и протестируйте операторы Transact-SQL. После этого, если вы получили результат, который ожидали, создавайте процедуру.
Для имен процедур лучше всего выбрать префикс, который будет указывать, что эта процедура создана именно вами. Только не используйте для этого префикс sp_, чтобы не было конфликтов с системными процедурами.
Давайте попробуем создать процедуру, которая будет получать из таблицы данные о работниках телефонах, т.е. следующий запрос:
SELECT pl.vcFamil, pl.vcName, pl.vcSurName, dDateBirthDay, vcPhoneNumber FROM tbPeoples pl, tbPhoneNumbers pn WHERE pn.idPeoples=*pl.idPeoples
Это простейшая процедура, которая не будет использовать переменных, поэтому для ее создания необходимо написать:
CREATE PROCEDURE GetPhones AS SELECT pl.vcFamil, pl.vcName, pl.vcSurName, dDateBirthDay, vcPhoneNumber FROM tbPeoples pl, tbPhoneNumbers pn WHERE pn.idPeoples=*pl.idPeoples
Как видите, в начало запроса всего лишь добавляется две строки (хотя, можно написать и в одну). В первой мы пишем операторы CREATE PROCEDURE и имя процедуры, а во второй строке ключевое слово AS. После этого идет простой запрос SELECT, который выбирает данные.
Чтобы разговор был более продуктивным, давайте рассмотрим, как можно выполнять процедуры. Для этого используется оператор EXECUTE, который выглядит следующим образом:
[ [ EXEC [ UTE ] ] { [ @return_status = ] { procedure_name [ ;number ] | @procedure_name_var } [ [ @parameter = ] { value | @variable [ OUTPUT ] | [ DEFAULT ] ] [ ,...n ] [ WITH RECOMPILE ]
В общем виде команда выглядит достаточно страшно, но к концу главы мы рассмотрим достаточно примеров, и вы увидите, что ничего страшного тут нет. Для процедуры GetPhones, которую мы создали ранее, необходимо указать только:
EXEC GetPhones
или
EXECUTE GetPhones
Результат выполнения команды:
vcFamil vcName vcSurName dDateBirthDay vcPhoneNumber -------------------------------------------------------------------ПОЧЕЧКИН ИВАН ИВАНЫЧ 2004-01-31 (925) 102-51-01 ПОЧЕЧКИН ИВАН ИВАНЫЧ 2004-01-31 (925) 163-31-52 ПОЧЕЧКИН ИВАН ИВАНЫЧ 2004-01-31 (095) 162-02-46 mr.ПЕТРОВ ИВАН ПАЛЫЧ 1971-04-03 (923) 112-02-46 mr.ПЕТРОВ ИВАН ПАЛЫЧ 1971-04-03 (095) 632-06-58 mr.СИДОРОВ ИВАН ПАЛЫЧ 1967-12-13 (923) 152-52-04 mr.СИДОРОВ ИВАН ПАЛЫЧ 1967-12-13 (095) 622-31-42 mr.СИДОРОВ ИВАН ПАЛЫЧ 1967-12-13 (095) 125-16-63 mr.КОНОНОВ ШВАРЦ ПЕТРОВИЧ 1981-12-13 (905) 100-10-10 ...
Наша процедура просто выбирает данные, и именно их мы видим в результате.
Теперь посмотрим, как можно удалять процедуры. Для этого используется оператор DROP PROCEDURE, который позволяет удалять несколько процедур сразу. В общем виде этот оператор выглядит:
DROP PROCEDURE { procedure } [ ,...n ]
Изменение процедур рассмотрим чуть позже, потому что у нас итак достаточно много информации и нужно рассмотреть немного примеров, чтобы на практике закрепить все возможности процедур.
Давайте создадим процедуру с параметрами. Допустим, что мы хотим создать процедуру, которая будет искать телефон нужного работника по фамилии. Для этого необходимо в качестве параметра получать искомую фамилию и использовать ее во время поиска:
CREATE PROCEDURE GetPhones @Famil varchar(50) AS SELECT pl.vcFamil, pl.vcName, pl.vcSurName, dDateBirthDay, vcPhoneNumber FROM tbPeoples pl, tbPhoneNumbers pn WHERE pn.idPeoples=*pl.idPeoples AND vcFamil=@Famil
Параметры перечисляются через запятую после имени процедуры в виде имя тип. Я выделил параметры отдельной строкой (вторая), которая идет после имени процедуры, но до ключевого слова AS. В данном примере только один параметр с именем @Famil и типом varchar длиной в 50 символов.
Имена параметров подчиняются тем же правилам именования, что и переменные и используются также. В данном примере, в секции WHERE происходит сравнение поля "vcName" с параметров @Famil.
Чтобы выполнить процедуру с параметром, нужно написать следующий запрос:
EXECUTE GetPhones 'ВАСИЛЬКОВ'
Значения, которые будут присвоены параметрам во время выполнения процедуры, перечисляются через запятую после имени процедуры.
Необходимо заметить, что некоторые программы, например, Query Analyzer, не требуют писать оператор EXEC. Достаточно написать имя процедуры и перечислить параметры:
GetPhones 'ВАСИЛЬКОВ'
Но такой пример может сработать далеко не всегда, вернее, не во всех программах. Я рекомендую всегда писать вначале оператор EXECUTE или сокращенно EXEC.
Параметры нужно передавать в том же порядке, как они были указаны в объявлении, но можно сделать и отступление, если указывать их в виде имя=значение. Например, в процедуре GetPhones параметр называется @Famil. Это значит, что мы можем вызвать процедуру следующим образом.
EXECUTE GetPhones @Famil='ВАСИЛЬКОВ'
В этом примере, после имени процедуры мы пишем имя параметра и присваиваем ему значение знаком равенства.
В коде процедуры вы можете использовать практически любые объекты базы данных MS SQL Server, а именно: объекты просмотра, таблицы, функции определенные пользователем и другие процедуры, а также временные таблицы. Если процедура создает временную локальную таблицу, то она существует только во время выполнения и невидима после завершения выполнения.
Хранимые процедуры представляют множество преимуществ, среди которых можно выделить следующее:
Помимо этого, процедура обладает всеми преимуществами, которыми обладают объекты просмотра, например, уменьшение сетевого трафика. Пользователи могут выполнять комплекс операций, посылая только один запрос, что уменьшает количество запросов между клиентом и сервером.
Давайте создадим процедуру, которая будет принимать несколько параметров и при этом использовать логику. В нашей таблице товаров нет ограничения уникальности на сочетание полей название товара и дату, а ведь по логике вещей должно быть. Если в таблице есть два товара, купленных в один день, то это не добавляет программе красоты. Такие покупки должны объединяться, ведь у нас есть поле для хранения количества.
Проблему можно решить и без добавления индекса уникальности, хотя в реальном приложении я бы создал. Будем считать, что индекс не создается только для того, чтобы администратор мог добавлять дубликаты покупок. Пользователям можно разрешить добавление записей только через процедуру.
Итак, давайте создадим такую процедуру (см. листинге 3.1).
Листинг 3.1.
CREATE PROCEDURE AddGoods @Date datetime, @Name varchar(50), @Cost money, @Number integer AS IF EXISTS (SELECT * FROM Товары WHERE Дата=@Date AND [Название товара]=@Name) PRINT 'Товар уже существует' ELSE BEGIN INSERT INTO Товары VALUES (@Date, @Name, @Cost, @Number) PRINT 'Товар добавлен' END
В качестве параметров передаются значения всех полей. Внутри процедуры делаем запрос на выборку строки с переданной в качестве параметра датой и названием товара. Если запрос вернет хоть одну строку (а больше и не должен, потому что это уже дубликат), то выводим на экран сообщение. Иначе, добавляем в таблицу запись с указанными параметрами.
Для выполнения процедуры выполним следующий запрос:
EXEC AddGoods '01.01.2003', 'Сок', 23, 1
Значения для всех полей перечислены в том же порядке, в котором они перечислены при создании процедуры. Попробуйте выполнить добавление одного и того же товара дважды. В ответ на это, вы должны увидеть сообщение о том, что товар уже существует.
Если вы хотите передать параметры в другом порядке, то необходимо указывать их имена. В следующем примере мы передаем сначала значение количества товаров:
EXEC AddGoods @Number=1, @Date='01.01.2003', @Name='Сок', @Cost=23
Благодаря явному указанию имен, порядок может быть любым.
Процедуры могут быть вложенными (одна процедура может вызывать другую). Вложенные процедуры должны удовлетворять следующим условиям:
Давайте рассмотрим вызов пользовательской процедуры из другой. Для этого создадим следующий вариант процедуры добавления товара:
CREATE PROCEDURE AddGoods1 @Date datetime, @Name varchar(50), @Cost money, @Number integer AS IF @Date>GETDATE() PRINT 'Дата больше текущей' ELSE EXEC AddGoods @Date, @Name, @Cost, @Number
В этом примере создается процедура AddGoods1, в которой вводится проверка, если дата товара больше текущей, то выводится соответствующее сообщение. Если дата корректна, то будет вызвана уже существующая процедура AddGoods.
Желательно чтобы права на объекты, которые используются в процедуре и сама процедура принадлежали одному и тому же пользователю. Для исключения ситуации, когда владелец процедуры и таблицы, на которую ссылается процедура, различны, все объекты должны принадлежать dbo. Я уже не раз говорил об этом и напоминаю еще раз – без особой надобности не указывайте владельцев. Лучше всего будет, если объекты будут принадлежать пользователю dbo.
Старайтесь создавать процедуры так, чтобы они выполняли по одной задаче. Дело в том, что одна из задач может в сочетании с другими решениями. Например, если бы мы добавили проверку даты, которую мы сделали в процедуре AddGoods1 в процедуру AddGoods, то нельзя было бы добавить товар, с датой более текущей. А так как у нас каждая процедура выполняет небольшую задачу, пользователям можно дать возможность выполнять более защищенную AddGoods1, а администраторы могут иметь возможность работы с AddGoods и при особой надобности добавлять товары с любой датой.
Вы можете создавать процедуры, которые будут работать как системные. Для этого вы должны войти под учетной записью администратора и использовать базу данных master, чтобы процедуры сохранялись в этой базе. В этом случае, такие процедуры, будут доступны из любой базы данных.
Процедуры могут и не выполнять каких-либо запросов из базы данных. Например, следующая пользовательская процедура просто возвращает текстовую строку:
CREATE PROCEDURE TestData AS SELECT 'Тестовая строка'
Выполнив команду EXEC TestData, на экране появится таблица с одной только строкой. Но процедура все же выполняет запрос SELECT, а ведь можно обойтись и без него, если достаточно только вывести на экран строку. Для вывода на экран достаточно воспользоваться оператором PRINT:
CREATE PROCEDURE PrintData AS PRINT 'Тестовая строка'
Для изменения процедуры используйте оператор ALTER PROCEDURE. Сервер заменяет существующее описание процедуры тем, что указано в ALTER PROCEDURE. Строго рекомендуется не изменять системные процедуры напрямую. Вместо этого создавайте свой собственный вариант, копируйте в нее операторы из существующей процедуры и после этого делайте необходимые изменения.
Если вы хотите изменить процедуру, которая была создана с какими-нибудь опциями, например WITH ENCRYPTION, вы должны включить эти опции в опции ALTER PROCEDURE, для сохранения функциональности, которую предоставляет опция.
Во время выполнения оператора ALTER PROCEDURE изменяется только одна процедура. Если она ссылается на другие, то они не изменяются.
Оператор ALTER PROCEDURE в общем виде выглядит следующим образом:
ALTER PROC [ EDURE ] procedure_name [ ; number ] [ { @parameter data_type } [ VARYING ] [ = default ] [ OUTPUT ] ] [ ,...n ] [ WITH { RECOMPILE | ENCRYPTION | RECOMPILE , ENCRYPTION } ] [ FOR REPLICATION ] AS sql_statement [ ...n ]
Следующий пример изменяет процедуру AddGoods1:
ALTER PROCEDURE AddGoods1 @Date datetime, @Name varchar(50), @Cost money, @Number integer = 1 AS IF @Date>GETDATE() PRINT 'Дата больше текущей' ELSE EXEC AddGoods @Date, @Name, @Cost, @Number
Изменения произошли в последнем параметре - @Number. Я установил для него значение по умолчанию 1. Теперь при вызове можно указывать только три значения. Если количество не указано, то будет использоваться значение 1.
EXEC AddGoods1 '01.01.2005', 'Шоколад', 25
Оператор INSERT может заполнять локальную таблицу результирующим набором, который возвращается из локальной или удаленной процедуры. Сервер SQL заполняет таблицу данными, которые возвращаются оператором SELECT в процедуре. Таблица должна существовать и типы данных должны совпадать.
Рассмотрим эту возможность. Для начала создадим процедуру, которая будет выбирать данные на определенную дату:
CREATE PROCEDURE GetGoods @Date datetime AS SELECT * FROM Товары WHERE Дата=@Date
Следующий пример использует процедуру для вставки данных в таблицу товаров:
INSERT INTO Товары EXEC GetGoods '01.01.2005'
Данный запрос вставляет в таблицу товаров результат выполнения процедуры GetGoods. Количество и типы полей должны совпадать, а у нас они будут совпадать, потому что процедура выбирает данные из той же таблицы товаров данные за указанную дату.
Теперь если просмотреть таблицу товаров, то вы увидите, что данные за первое января 2005-го года в таблице содержаться дважды. Именно на эту дату процедура выбирала данные, и их вставили в таблице товаров. Чтобы лучше было двойные записи, отсортируйте их по дате и названию:
SELECT * FROM Товары ORDER BY Дата, [Название товара]
Теперь посмотрим, какие дополнительные параметры можно использовать во время создания процедуры. Таких параметров два:
Посмотрим, как можно использовать шифрование:
CREATE PROCEDURE GetPhones WITH ENCRYPTION AS SELECT pl.vcFamil, pl.vcName, pl.vcSurName, dDateBirthDay, vcPhoneNumber FROM tbPeoples pl, tbPhoneNumbers pn WHERE pn.idPeoples=*pl.idPeoples
Опция WITH ENCRYPTION указывается после всех параметров процедуры, но до ключевого слова AS.
Теперь посмотрим, что сервер сохранил в своих системных таблицах. Для этого выполните следующий запрос:
SELECT * FROM sysobjects so, syscomments sc WHERE name='GetPhones' AND so.id=sc.id
В колонке text вы увидите только бессмысленные символы. Для любой другой процедуры здесь будет ее текст, а для зашифрованной будут данные, которые не несут полезной информации.
Давайте посмотрим на запрос, который мы использовали для получения информации о процедуре. Здесь у нас выбираются данные из двух таблиц sysobjects и syscomments. В первой таблице находятся имена всех объектов базы данных, а в таблице syscomments находятся параметры объекта. Для хранимой процедуры здесь можно увидеть текст самой процедуры в поле "text", если он не зашифрован.