Объектно-реляционная СУБД PostgreSQL. Учебное пособие
.pdfFROM Коробки
WHERE V(Номер)>=V);
END
$$ LANGUAGE plpgsql;
Замечание. Конструктор ARRAY превращает результат запроса – таблицу, состоящую из одного столбца, – в массив. Если убрать ARRAY, то функция будет возвращать просто результат запроса, тогда следует объявить тип возвращаемого значения как setof int (столбец целых чисел).
4.Напишем функцию, которая возвращает набор записей таблицы Коробки
сданными о коробках, все размеры которых строго меньше некоторого значения L. Тип возвращаемого значения этой функции – "setof Коробки". Напомним, что PostgreSQL считает, что имя таблицы и есть тип содержащихся в ней записей.
CREATE OR REPLACE FUNCTION lessL(L int)
RETURNS setof Коробки AS $$
SELECT * FROM Коробки
WHERE L>ALL(Размеры);
$$ LANGUAGE sql;
Замечание: функция ALL(массив) позволяет реализовать проверку заданного условия для всех элементов массива. Её можно использовать только справа от знака операции сравнения (см. стр. Ошибка: источник перёкрестной ссылки не найден).
5. Напишем функцию, которая получает Номер коробки и возвращает наибольшее значение из трёх её измерений.
CREATE OR REPLACE FUNCTION maxS(N integer) RETURNS double precision AS $$
SELECT max(A)
FROM Коробки, unnest(Размеры) As A
WHERE Номер=N; $$ LANGUAGE sql;
Изменим немного задачу: определим наибольший из двух первых размеров коробки – её длины и ширины. Для этого в запросе SELECT используем срез массива Размеры.
SELECT max(A)
71
FROM Коробки, unnest(Размеры[1:2]) As A
WHERE Номер=N;
6. Напишем функцию, которая возвращает набор записей (таблицу) с полями Номер, МаксРазмер, где МаксРазмер есть наибольшее значение из трёх измерений коробки:
А) на языке SQL:
CREATE OR REPLACE FUNCTION maxR()
RETURNS TABLE(Номер int, МаксРазмер int) AS $$
SELECT Коробки.Номер,
(SELECT max(A) FROM unnest(Размеры) As A)
FROM Коробки;
$$ LANGUAGE SQL;
Б) на языке PL/pgSQL:
CREATE OR REPLACE FUNCTION maxR()
RETURNS TABLE(Номер int, МаксРазмер int) AS $$ BEGIN
RETURN QUERY
SELECT Коробки.Номер,
(SELECT max(A) FROM unnest(Размеры) As A) FROM Коробки;
END;
$$ LANGUAGE plpgSQL;
Замечание: поскольку имя «Номер» внутри функции maxR() является одновременно и полем выходного параметра функции и полем таблицы Коробки, то для его идентификации в тексте запроса мы дополняем его именем таблицы.
Чтобы вывести результат работы функции maxR(), напишем запрос:
SELECT Номер, МаксРазмер FROM maxR();
Вызов функции помещён в предложение FROM, так как результат вызова функции фактически есть таблица с полями Номер и МаксРазмер.
7. Напишем функцию, которая получает коды двух коробок и определяет, поместится ли одна из них в другую при условии параллельности стенок коробок, но с возможностью поворотов коробок в пространстве. Если все размеры коробок совпадают, то функция должна вернуть 0. Если первая коробка поместится во вторую, то функция должна вернуть 1, если вторая коробка поместится в первую,
72
то функция должна вернуть 2, если ни одну из коробок нельзя полностью поместить в другую, то функция должна вернуть значение -1.
Идея решения: нужно отсортировать размеры коробок, а затем сравнить попарно наименьшие, средние и наибольшие размеры двух коробок. Для сортировки массива используем следующий приём: сначала с помощью функции unnest превратим массив Размеры в столбец значений. Полученный столбец отсортируем стандартными средствами, а затем снова превратим в массив.
Приведём полный текст функции.
CREATE OR REPLACE FUNCTION cmp(i int, j int) RETURNS int AS $$
DECLARE |
|
|
|
res int; |
|
|
|
r1 int[]; |
r2 int[]; |
||
BEGIN |
|
|
|
r1 = ARRAY(SELECT |
unnest(Размеры) As A |
||
|
|
FROM Коробки |
|
|
|
WHERE Номер=i ORDER BY A); |
|
r2 = ARRAY(SELECT |
unnest(Размеры) As A |
||
|
|
FROM Коробки |
|
|
|
WHERE Номер=j ORDER BY A); |
|
IF |
r1[1]=r2[1] |
AND r1[2]=r2[2] AND r1[3]=r2[3] |
|
|
THEN |
res=0; |
|
ELSIF r1[1]<r2[1] |
AND r1[2]<r2[2] AND r1[3]<r2[3] |
||
|
THEN |
res=1; |
|
ELSIF r2[1]<r1[1] |
AND r2[2]<r1[2] AND r2[3]<r1[3] |
||
|
THEN |
res=2; |
|
ELSE |
res = -1; |
|
|
END IF; |
|
|
|
RETURN res; |
|
|
|
END |
|
|
|
$$ LANGUAGE plpgsql; |
|
||
Заметим, |
что если вместо сравнения элементов массивов сравнивать |
||
переменные r1 и r2, то мы получим неверные результаты. PostgreSQL «умеет»
73
сравнивать массивы, но делает это по-своему. PostgreSQL считает, что меньше тот массив, у которого меньше первый элемент, при равенстве первых элементов, сравниваются вторые элементы, при равенстве вторых – третьи и т.д.
3.6.Оператор EXECUTE
Вряде случаев для выполнения запроса внутри функции необходимо использовать оператор EXECUTE 'текст запроса';. В текст запроса можно «вклеить» значения переменных, правда, иногда это требует некоторых ухищрений.
Рассмотрим следующий пример. Пусть в базе данных имеются таблицы: Студенты (НомерЗачётки, ФИО)
Дисциплины (Код, Название)
Ведомость (Код, НомерЗачётки, КодДисциплины, Оценка, Дата). Необходимо создать таблицу СводнаяВедомость, которая в двух первых
колонках содержат данные НомерЗачётки и ФИО из таблицы Студенты, а в остальных столбцах – оценки по дисциплинам, названия которых берутся из таблицы Дисциплины. Так, например, если в списке дисциплин будут Математический анализ, Линейная алгебра, Основы информатики, то все эти названия должны использоваться в качестве имён столбцов таблицы СводнаяВедомость.
Оценки в таблице Ведомость проставляются в виде 'отл', 'хор', 'уд' и 'неуд'. Естественно, некоторые студенты могли ещё (по уважительным причинам) не прийти на экзамен, тогда клетка с оценкой останется пустой, или получить неудовлетворительную оценку и исправить её позже, тогда в ведомости у этого студента будет несколько записей по одному и тому же предмету. В этом случае,
всводную ведомость нужно занести последнюю полученную оценку.
Напишем сначала функцию Создать(), которая формирует структуру таблицы СводнаяВедомость:
CREATE OR REPLACE FUNCTION Создать()
RETURNS void AS $$
DECLARE
74
S Дисциплины%ROWTYPE; BEGIN
CREATE TABLE СводнаяВедомость ( НомерЗачётки integer PRIMARY KEY, ФИО text
);
FOR S IN SELECT * FROM Дисциплины LOOP EXECUTE 'ALTER TABLE СводнаяВедомость ' ||
'ADD COLUMN "'|| S.Название ||'" text;';
END LOOP; END;
$$ LANGUAGE plpgsql;
Таблица СводнаяВедомость создаётся в два приёма. На первом шаге с помощью запроса CREATE TABLE создаётся таблица из двух столбцов – ключевого поля НомерЗачётки и атрибута ФИО. На втором шаге с помощью запроса ALTER TABLE в таблицу добавляются столбцы с названиями дисциплин. Однако поскольку эти имена содержат пробелы, то их следует брать в двойные кавычки. Эта особенность не позволяет выполнить запрос без оператора EXECUTE.
Склеим текст запроса из отдельных частей:
1) |
'ALTER TABLE СводнаяВедомость ' – начало запроса |
|
2) |
'ADD COLUMN "' |
– ADD COLUMN и открывающие кавычки |
3) |
S.Название |
– собственно название дисциплины |
4) |
'" text;' |
– закрывающие кавычки и text; |
В результате оператор EXECUTE получит на исполнение текст запроса, в котором название дисциплины будет взято в двойные кавычки. Пример сформированного запроса:
ALTER TABLE СводнаяВедомость
ADD COLUMN "Основы информатики" text;
После вызова функции SELECT Создать(); будет получена требуемая таблица. Чтобы заполнить её данными, напишем функцию Заполнить().
75
CREATE OR REPLACE FUNCTION Заполнить()
RETURNS void AS $$ |
|
|
DECLARE |
|
|
num |
integer; |
-- номер зачётки |
fio |
text; |
-- ФИО студента |
name text; |
-- название дисциплины |
|
S Ведомость%ROWTYPE; |
||
BEGIN |
|
|
-- заполняем первые две колонки |
||
FOR |
num, fio IN SELECT НомерЗачётки, ФИО |
|
|
|
FROM Студенты |
LOOP
INSERT INTO СводнаяВедомость(НомерЗачётки, ФИО) VALUES(num, fio);
END LOOP;
--с помощью цикла по запросу
--просматриваем таблицу Ведомость,
--отсортированную по возрастанию даты экзамена FOR S IN SELECT * FROM Ведомость ORDER BY Дата LOOP
--получаем название дисциплины
name=(SELECT Название FROM Дисциплины WHERE Код=S.КодДисциплины);
--записываем оценку студента в поле
--с названием дисциплины
EXECUTE 'UPDATE СводнаяВедомость SET "' || name ||'"= '''|| S.Оценка ||'''
WHERE НомерЗачётки = '|| S.НомерЗачётки ||';'; END LOOP;
END;
$$
LANGUAGE plpgsql;
Текст запроса в операторе EXECUTE, как и в предыдущем примере, склеен из отдельных частей. Особенность этого запроса – наличие трёх идущих подряд символов апострофа ('), один из которых фиксирует границы склеиваемых частей. После склейки останется по две одиночные кавычки вокруг значения
76
оценки. Для наглядности приведём текст сформированного запроса для значений name = Основы информатики, S.Оценка = 'хор', S.НомерЗачетки = 112345:
UPDATE СводнаяВедомость SET "Основы информатики" = ''хор'' WHERE НомерЗачётки = 112345;
Две одиночные кавычки в тексте запроса интерпретируются как одна кавычка, в первом случае открывающая, а во втором – закрывающая текстовое значение оценки. При вклеивании текстового значения оценки в операторе EXECUTE добавится ещё по одной одиночной кавычке, вот почему в тексте программы мы видим три идущие подряд одиночные кавычки.
4. Курсоры, триггеры, индексы
4.1. Курсоры
Курсор представляет собой доступный только для чтения указатель на набор записей, возвращаемый запросом SELECT. Курсоры могут создаваться как неявно, например в случае выполнения цикла FOR … IN SELECT …, так и явно. Явные курсоры обеспечивают произвольное перемещение по записям набора, позволяют обрабатывать данные, находящиеся в разных записях набора. Использование курсора позволяет приложению организовать обработку данных без повторного выполнения запроса, особенно если обычная выборка возвращает много записей. Кроме того, вместо возвращения большого набора данных из функции, последняя может вернуть указатель на этот набор, т.е. курсор.
Курсор всегда указывает па «текущую» позицию результирующего набора записей (положение указателя при открытии курсора – перед первой записью набора), но его можно перемещать по набору командами MOVE: MOVE NEXT,
MOVE PRIOR, MOVE FIRST, MOVE LAST и др. Аналог курсора в MS
Access – набор данных RecordSet.
Переменная-курсор объявляется в разделе DECLARE:
имя_курсора [SCROLL | NO SCROLL] CURSOR [(параметры)] FOR запрос;
(вместо FOR можно использовать IS для совместимости с СУБД Оракл).
77
При установленной опции SCROLL перемещение по курсору возможно как вперёд, так и назад, при установленной опции NO SCROLL – только вперёд.
При объявлении курсора можно объявить «абстрактный» курсор, не связанный ни с каким набором данных, например:
cur refcursor;
В теле функции такая переменная должна получить конкретное значение. Также можно указать, с каким набором данных связан курсор, например:
cur CURSOR
FOR SELECT * FROM <таблица>;
В этом примере курсор cur связывается с набором записей указанной в запросе таблицы.
С помощью курсора можно реализовать запрос с параметрами, передавая в запрос параметры курсора:.
cur CURSOR (val integer) FOR SELECT * FROM <таблица> WHERE <атрибут> = val;
Конкретное значение параметра val передаётся запросу при открытии курсора.
Варианты открытия курсора:
А) для переменной, объявленной как refcursor, связываем объявленный ранее курсор с конкретным набором данных:
OPEN курсор [SCROLL | NO SCROLL] FOR запрос;
или
OPEN курсор [SCROLL | NO SCROLL] FOR EXECUTE 'строка запроса' [USING список параметров];
Б) для переменной, связанной при объявлении с конкретным запросом:
OPEN курсор;
или
78
OPEN курсор (список параметров); – если курсор объявлен с
параметрами.
После того, как курсор открыт, мы получаем доступ к содержимому курсора. Выборка записи из курсора осуществляется оператором FETCH (слово fetch в данном контексте переводится как «извлечь»).
FETCH [параметр {FROM|IN}] курсор INTO назначение;
Заметим, что FROM и IN – эквивалентные ключевые слова, обычно используется вариант FROM.
Необязательный параметр задаёт запись, которую следует извлечь по отношению к текущей записи: NEXT или FORWARD – следующую, PRIOR или BACKWARD – предыдущую, FIRST – первую, LAST – последнюю. Отсутствие параметра равносильно значению NEXT. Извлечённые данные записываются в переменные, указанные после ключевого слова INTO.
Оператор FETCH может вернуть пустой набор данных. В этом случае служебная переменная FOUND принимает значение false, если же хотя бы одна запись получена – то true. Заметим, что переменную FOUND можно использовать для проверки результата выполнения и других операторов, в частности, операторов SELECT, INSERT, UPDATE, DELETE и ряда других.
Оператор FETCH может извлекать из курсора сразу несколько записей. FORWARD количество_записей, BACKWARD количество_записей
извлекают заданное количество записей в указанном направлении. Вместо количества записей можно указать ключевое слово ALL – извлечь все записи после/до положения курсора.
Перемещение по курсору без выборки записи осуществляется командой MOVE. Синтаксис команды:
MOVE [параметр {FROM|IN}] курсор;
Набор параметров у команды MOVE такой же, как у команды FETCH. Закрыть набор записей можно командой CLOSE курсор;
79
В ряде случаев курсор закрывается автоматически, например, при завершении транзакции. Другой пример: курсор, неявно созданный в цикле по запросу, автоматически закрывается при выходе из цикла.
Проиллюстрируем использование курсора на следующем примере. Создадим таблицу значений некоторой функции:
CREATE TABLE Значения ( Номер serial PRIMARY KEY, X double precision,
Y double precision
);
Напишем функцию, которая заполняет таблицу данными, значение аргумента X выбирается случайным образом в интервале от 0 до 1, значение Y вычисляется по формуле.
CREATE OR REPLACE FUNCTION Данные() RETURNS void AS $$
DECLARE
x double precision; n integer = 100;
BEGIN
FOR k IN 0..n LOOP IF k=0 THEN x =0; ELSIF k=n THEN x=1; ELSE x = random(); END IF;
INSERT INTO Значения(X,Y) VALUES(x, cos(x)); END LOOP;
END
$$ LANGUAGE plpgsql;
и вызовем её:
SELECT Данные();
Вычислим интеграл по методу трапеций. Он есть сумма слагаемых вида 0.5*(Y[i] + Y[i-1])*(X[i] - X[i-1]), т.е., формально для вычисления каждого
80
