«Умный» SQL
Многие программисты, работающие с Microsoft SQL Server, предпочитают писать логику приложения в коде своих программ, используя SQL Server только как хранилище данных. Одна из причин такого подхода - обычная лень (например, при переносе базы данных с Access на SQL Server), другая же причина - мнение, что язык SQL не столь гибок, как Delphi, C++ и другие языки программирования. Но ведь он и не предназначен для работы с конечным пользователем! На самом деле - SQL предоставляет необходимый минимум возможностей для работы с данными; надо только ими правильно воспользоваться.
В этой статье я хочу поделиться некоторыми хитростями работы с Microsoft SQL Server, которые я использую в своих проектах. Сразу оговорюсь, что все скрипты написаны для MS SQL Server 7.0.
Передача таблицы значений в хранимую процедуру
Нередко возникает вопрос: "Как передать в хранимую процедуру массив (таблицу) значений?". Стандартные решения - вызов процедуры для каждого значения по отдельности, либо передача массива в виде строки с разделителями, которая потом разбирается хранимой процедурой. Очевидно, что в случае, когда размер массива значений велик, первый подход очень сложен для сервера (и его выполнение занимает очень много времени), второй же ограничен максимальной длиной типа varchar (8000 байт).
Однако, есть решение, свободное от этих недостатков. Можно создать временную таблицу, занести в нее массив значений - и выполнить хранимую процедуру, которая будет брать данные из созданной таблицы. Все команды выполняются в одной сессии. Единственный недостаток такого подхода очевиден - процедура должна точно знать названия таблицы и полей.
Допустим, нам надо выбрать все записи из таблицы authors базы pubs, у которых поле state равно одному из значений [CA, MI, KS].
Создадим процедуру dbo.TestProc:
CREATE PROCEDURE dbo.TestProc
AS
BEGIN
SELECT *
FROM pubs..authors a
INNER JOIN #params t ON
a.state = t.state
END
GO
Теперь для решения нашей задачи осталось написать следующий скрипт:
CREATE TABLE #params
(state varchar(2))
INSERT INTO #params(state)
VALUES(CA)
INSERT INTO #params(state)
VALUES(MI)
INSERT INTO #params(state)
VALUES(KS)
EXEC dbo.TestProc
DROP TABLE #params
Просто, не правда ли? Кстати, команду DROP TABLE #params можно не выполнять. Временная таблица #params будет автоматически удалена при завершении сессии.
Сравнение двух таблиц
Допустим, нужно сравнить две таблицы по условию И. Например - выбрать издателей, выпускающих книги по темам "Психология"("psychology") и "Бизнес"("business"). Элементарно? Конечно; но что делать, если условий много и их количество неизвестно?
Можно объявить переменную @count, в которой будет храниться количество совпадений (количество уникальных записей из таблицы, с которой мы сравниваем). Затем надо сравнить количество неодинаковых совпадений интересующих нас тем каждого издателя с @count. Нужные нам записи - те, которые удовлетворяют последнему условию. Попробуем написать скрипт:
/* Создадим и заполним временную таблицу #TestTable, содержащую условия поиска. */
CREATE TABLE #TestTable
(type varchar(15))
INSERT INTO #TestTable
VALUES(business)
INSERT INTO #TestTable
VALUES(psychology)
/* Объявим переменную @count. */
DECLARE
@count int
/* И сохраним в ней количество неповторяющихся записей в таблице #test */
SELECT
@count = COUNT(DISTINCT type) FROM #TestTable
SELECT
DISTINCT p.pub_name
FROM #TestTable INNER JOIN
titles t ON
t.type = #TestTable.type INNER JOIN
publishers p ON t.pub_id = p.pub_id
GROUP BY
p.pub_name
HAVING
COUNT(distinct t.type) = @count
Этот прием вполне совместим с приемом, описанным в предыдущем пункте. Вместе они очень полезны для процедур поиска.
Выполнение динамического запроса при отсутствии прав на выборку данных из таблицы
Иногда бывают ситуации, когда проще сформировать условие WHERE в клиентском приложении, а затем передать его как параметр в хранимую процедуру. В случае, когда существует разрешение на выборку данных из желаемой таблицы, решение будет тривиальным. Но что делать, если пользователь имеет право пользоваться только хранимыми процедурами?
Ответ достаточно прост. Процедура работает с базой данный с "суммарными" правами владельца и пользователя, который ее вызвал. Так, любая процедура, созданная пользователем dbo, имеет все права на базу данных. Но если в процедуре встречается оператор EXEC, то он выполняется от имени пользователя, вызвавшего процедуру. Если мы не дали достаточно прав на выполнение запроса, присутствующего в операторе EXEC - произойдет ошибка. Решение этой проблемы простое - выгрузить данные во временную таблицу, а затем вывести их в соответствии с нашими условиями:
CREATE PROCEDURE dbo.TestProc
(@where varchar(8000) = NULL)
/* Создадим процедуру dbo.TestProc */
AS
BEGIN
SET NOCOUNT ON
/* Она будет производить выборку данных из таблицы dbo.authors
базы данных pubs, удовлетворяющих условиям, хранящимся в переменной @where */
/* Создадим таблицу #tmp. */
CREATE TABLE #tmp (
au_id varchar(11) NOT NULL ,
au_lname varchar(40) NOT NULL ,
au_fname varchar(20) NOT NULL ,
phone char(12),
address varchar(40) NULL ,
city varchar(20) NULL ,
state char(2) NULL ,
zip char(5) NULL ,
contract bit NOT NULL
)
/* Выгрузим данные из таблицы dbo.authors базы данных pubs в #tmp... */
INSERT INTO #tmp SELECT * INTO #tmp FROM pubs..authors
/* ...и выполним выборку, удовлетворяющую нашим условиям. */
if (@while = )
SELECT * FROM #tmp
ELSE
EXEC(SELECT * FROM #tmp WHERE + @where)
SET NOCOUNT OFF
END
Теперь можно проверить работу процедуры. Допустим, у нас существует пользователь с именем manager. Запретим ему выбирать данные из таблицы dbo.authors:
DENY SELECT ON dbo.authors TO manager
И разрешим выполнение процедуры dbo.TestProc.
GRANT EXECUTE ON dbo.TestProc TO manager
Изменим текущего пользователя на manager.
SETUSER manager
И выполним процедуру dbo.TestProc
EXEC dbo.TestProc au_lname LIKE Gree% AND state IN (CA, MI)
Использование базы данных TempDB
База данных TempDB существует для хранения временных объектов и создается заново при каждом запуске SQL Serverа. В ней же хранятся результаты выполнения сложных запросов и процедур. Здесь мне хотелось бы поговорить об использовании временных таблиц и временных процедур.
Достаточно часто программисты допускают ошибку, пользуясь конструкцией SELECT INTO в сложных запросах с выгрузкой данных во временную таблицу. Проблема в том, что во время выполнения такого запроса база данных TempDB блокируется. Это разумно только в случае, когда точно известно, что выборка данных будет кратковременной. Надо учитывать и то, что некоторые таблицы, из которых происходит выборка данных, могут быть заблокированы другими процессами. Если же количество полей временной таблицы велико или неизвестно, то для ее создания лучше воспользоваться таким запросом:
SELECT TOP 0 * INTO #TempTable FROM ...
Причем запрос не должен включать в себя условий отбора и сортировки, а таблицы должны быть перечислены через запятую.
Перейдем к временным процедурам. К сожалению, Microsoft только к середине 2000 года заметила огромное упущение в работе с временными хранимыми процедурами - они позволяют любому пользователю выполнить любую (даже запрещенную для него) хранимую процедуру. Давайте рассмотрим эту ситуацию на примере из предыдущей главы. Запретим пользователю с именем manager выполнение процедуры dbo.TestProc:
DENY EXECUTE ON dbo.TestProc TO manager
Изменим текущего пользователя на manager.
SETUSER manager
И выполним процедуру dbo.TestProc.
EXEC dbo.TestProc au_lname LIKE Gree% AND state IN (CA, MI)
В ответ мы получим:
Server: Msg 229, Level 14, State 5, Procedure TestProc, Line 1
EXECUTE permission denied on object TestProc , database pubs, owner dbo.
Теперь создадим временную процедуру #TestProc:
CREATE PROCEDURE #TestProc
(@where varchar(8000) = NULL)
AS
BEGIN
EXEC dbo.TestProc @where
END
И выполним ее:
EXEC #TestProc au_lname LIKE Gree% AND state IN (CA, MI)
Все прекрасно работает!
Как же обезопасить данные от таких ситуаций? Процедуре придется самой проверить, кто же ее запустил, и имеет ли он на это право. Допустим, у нас есть группа пользователей managers. Добавим в нее пользователя manager. Теперь с помощью функции IS_MEMBER() узнаем, принадлежит ли пользователь к этой группе. Добавим в начало процедуры dbo.TestProc строки:
IF (IS_MEMBER(managers) != 1)
BEGIN
RAISERROR(Только пользователи, входящие в группу managers могут вызывать эту процедуру, 16, 1)
END
Такую проверку следует включать в начало каждой процедуры.
Официальное решение этой проблемы см. на сервере Microsoft по адресу: http://www.microsoft.com/Downloads/Release.asp?ReleaseID=22470 (для платформы Intel) и http://www.microsoft.com/Downloads/Release.asp?ReleaseID=22469 (для Alpha).
Обработка результата работы хранимой процедуры
Часто бывают ситуации, когда одну большую процедуру хочется разделить на несколько маленьких; или в нескольких процедурах используется одинаковый код, который лучше вынести в отдельную процедуру. Но встает вопрос: как получить от нее данные о результате ее работы? Одного кода возврата зачастую бывает мало. Тем более интересен вопрос передачи возвращаемых процедурой данных. В Books Online, к сожалению, этот важный момент совершенно не рассмотрен. Тем не менее, эта возможность активно используется при создании базы данных master. Синтаксис такого запроса следующий: INSERT INTO таблица EXEC процедура.
Для примера создадим процедуру dbo.TestProc, выбирающую из таблицы dbo.authors поля au_id, au_lname и au_fname и временную таблицу #TestTable с полями au_id, au_lname и au_fname.
/* Создадим процедуру dbo.TestProc */
CREATE PROCEDURE dbo.TestProc
AS
BEGIN
SELECT au_id, au_lname, au_fname FROM dbo.authors
END
GO
/* Создадим временную таблицу #TestTable */
CREATE TABLE #TestTable
(AU_ID VARCHAR(20) NOT NULL,
AU_LNAME VARCHAR(40) NOT NULL,
AU_FNAME VARCHAR(20) NOT NULL)
GO
/* И выполним вставку значений из хранимой процедуры dbo.TestProc */
INSERT INTO #TestTable EXEC dbo.TestProc
GO
/* Теперь можно посмотреть на результат */
SELECT * FROM #TestTable
Шифрование данных
В приложениях, работающих с SQL Server, часто требуется провести дополнительную идентификацию пользователя. Например, для выполнения какой-то ответственной операции надо проверить, может ли пользователь ее выполнить. Можно, конечно, попросить его ввести пароль. Но как этот пароль хранить? Прописывать его в коде самого приложения - по крайней мере глупо; хранить как открытый текст в какой-то таблице базы данных - тоже небезопасно. Остается один выход - шифровать.
К сожалению, корпорация Microsoft не захотела документировать функции, которые позволяют шифровать данные. Этими функциями являются: encrypt(), pwdencrypt() и pwdcompare(). Функция encrypt(значение) возвращает тип varbinary(6). Функция pwdencrypt(значение) возвращает тип nvarchar(32). Функция pwdcompare({проверяемое значение}, {зашифрованное значение}) сравнивает введенное значение с зашифрованным, где проверяемое значение - строка типа nvarchar, зашифрованное значение имеет тип varbinary(16). Возвращаемые значения - 0 (если не совпадает), 1 (если совпадает), NULL (если произошла ошибка). Различия функций encrypt() и pwdencrypt() в том, что encrypt() кодирует значения по единому алгоритму, а pwdencrypt() - по меняющемуся. То есть, выполнив несколько раз SELECT encrypt(test) мы будем получать одно и то же значение, а выполнение SELECT convert(varbinary(16), pwdencrypt(test)) дает все время разные значения. Значение, зашифрованное с помощью pwdencrypt() можно сравнить с проверяемым только с помощью функции pwdcompare(), а зашифрованное с помощью encrypt() - сравнивая его с зашифрованным проверяемым значением. Но у функции pwdcompare() есть один большой минус - если на сервере установлена регистро-независимая кодовая страница, то проверка также будет регистро-независимой. То есть пароли test и Test будут признаны идентичными. Приходится выбирать - либо пользоваться мощным алгоритмом, предоставляемым функцией pwdencrypt(), либо пользоваться менее защищенной encrypt(). Приведу примеры использования этих функций:
/* Создадим таблицу dbo.TestTable, в которой будут храниться имена и пароли пользователей */
CREATE TABLE dbo.TestTable
( UserName varchar(30),
password varbinary(16))
/* Добавим пользователя User1 с паролем user1, зашифрованным функцией encrypt() */
INSERT INTO dbo.TestTable
(UserName, password)
VALUES(User1, encrypt(user1))
/* …пользователя User1_1 с паролем user1, функция encrypt() */
INSERT INTO dbo.TestTable
(UserName, password)
VALUES(User1_1, encrypt(user1))
/* User1_2, пароль User1, функция encrypt() */
INSERT INTO dbo.TestTable
(UserName, password)
VALUES(User1_2, encrypt(User1))
/* User2, пароль user2, pwdencrypt() */
INSERT INTO dbo.TestTable
(UserName, password)
VALUES(User2, convert( varbinary, pwdencrypt(user2)))
/* User2_1, пароль user2, pwdencrypt() */
INSERT INTO dbo.TestTable
(UserName, password)
VALUES(User2_1, convert( varbinary, pwdencrypt(user2)))
/* User2_2, пароль User2, pwdencrypt() */
INSERT INTO dbo.TestTable
(UserName, password)
VALUES(User2_2, convert( varbinary, pwdencrypt(User2)))
/* А теперь - посмотрим на результат. */
SELECT * FROM dbo.TestTable
-- UserName password
-- ------------------------------ ----------------------------------
-- User1 0x7ABA56BB8E350AC328AF
-- User1_1 0x7ABA56BB8E350AC328AF
-- User1_2 0x5AFAC964B710D95AC836
-- User2 0x2131235F272D2C375058555332214C2F
-- User2_1 0x213123253A58355621592D4634295358
-- User2_2 0x213124372C2B4C2228563C454F363D56
Видно, что User1 и User1_1 имеют одинаковые пароли, а User2 и User2_1 - разные.
Теперь поэкспериментируем с функцией pwdcompare() и тоже посмотрим на результаты:
SELECT UserName FROM dbo.TestTable WHERE pwdcompare(user2, password)=1
-- UserName
-- ----------------------------
-- User2
-- User2_1
-- User2_2
SELECT UserName FROM dbo.TestTable WHERE pwdcompare(uSeR2, password)=1
-- UserName
-- ----------------------------
-- User2
-- User2_1
-- User2_2
SELECT UserName FROM dbo.TestTable WHERE pwdcompare(user1, password)=1
-- UserName
-- ----------------------------
Аналогично для функции encrypt():
SELECT UserName FROM dbo.TestTable WHERE encrypt(user1) = password
-- UserName
-- ----------------------------
-- User1
-- User1_1
SELECT UserName FROM dbo.TestTable WHERE encrypt(User1) = password
-- UserName
-- ----------------------------
-- User1_2
www.sdteam.com
Базы данных 29-01-2007 В России началось первое внедрение SAP for Insurance 24-09-2008 Базы данных Сегодня компании «Национальная страховая группа», SAP и группа Energy Consulting объявили о начале этапа опытно-промышленной эксплуатации первого в России проекта внедрения специализированного отраслевого решения для страховых компаний SAP for Insurance.Проект создания в «Национальной страховой группе» корпоративной информационной системы на базе решений SAP стартовал в 2007 году и реализуется поэтапно. В рамках проекта внед...
"Комстар-ОТС" завершил внедрение системы Oracle E-Business Suite 07-07-2008 Базы данных Оператор связи «Комстар-Объединенные ТелеСистемы» завершил проект по внедрению полнофункциональной ERP-системы Oracle E-Business Suite. Общий объем инвестиций в проект, включая услуги, лицензии, обучение, поддержку и оборудование, составил около 12 млн. долларов. Внедрение ERP-системы в «Комстар-ОТС» началось в 2005 году. Проект был выполнен в несколько этапов. Генеральным подрядчиком выступила компания «Квазар-Микро» (теперь работающая под б...
Oracle купила компанию Bridgestream 09-09-2007 Базы данных Корпорация Oracle сегодня объявила о покупке компании Brigestream, частной компании, разрабатывающей программное обеспечение для разграничения прав доступа. Сумма покупки компаниями не сообщается.Купленная Bridgestream довольно хорошо известна на западном корпоративном рынке за счет разработок, которые внедряются в корпоративные системы управления и разграничивают доступ сотрудников к тем или иным данным в зависимости от должностных обязанностей ...
Oracle открывает научно-исследовательскую сеть в азиатско-тихоокеанском регионе 24-07-2007 Базы данных Компания Oracle сегодня объявила об открытии научно-исследовательской сети, объединившей 19 исследовательских и внедренческих центров, расположенных на материковой Азии и в Японии.Как сообщает компания, новая сеть центров займется тестированием самых передовых разработок и технологий в области программного и аппаратного обеспечения в регионе.Наиболее крупные центры расположены в Пекине (Китай), Гургаоне (Индия), Сеуле (Южная К...
Использование технологий WWW для доступа к базам данных 06-06-2007 Базы данных Глава 1. Основы использования WWW - технологий для доступа к существующим базам данныхГлава 2. Подготовка гипертекстовых документов для World Wide WebГлава 3. Установка и администрирование WWW - сервераГлава 4. Использование CGI при создании интерактивных интерфейсовГлава 5. Использование пакета Cold Fusion для MS Windows при построении WWW - интерфейсов к базам данныхГлава 6. Использование языка PerlГлава 7. Использование пакета We... |