Объектно-реляционная СУБД PostgreSQL. Учебное пособие
.pdfGROUP BY НомерОтдела;
Усложним задачу: кроме максимального оклада для каждого отдела выведем и данные сотрудника с максимальным (в этом отделе) окладом. Покажем, что при написании данного запроса нам придётся использовать временные имена – псевдонимы – для таблицы Персонал. Предварительно отредактируем величины окладов сотрудников. Пусть оклад директора равен 40000 р., оклад бухгалтера – 20000 т.р., а оклад менеджера – 10000 р.
Итак, вначале задаём список вывода:
SELECT Персонал.НомерОтдела, ТабНомер, ФИО, Оклад
затем соединяем внутренним соединением таблицы Персонал и Отделы:
FROM Персонал, Отделы
WHERE Персонал.НомерОтдела=Отделы.НомерОтдела
и формально добавляем условие «оклад данного сотрудника равен максимальному окладу в том отделе, в котором этот сотрудник работает»:
AND Оклад=(SELECT max(Оклад)
FROM Персонал
WHERE Персонал.НомерОтдела =
Персонал.НомерОтдела);
Во вложенном подзапросе НомерОтдела сотрудника должен совпадать с НомеромОтдела, выбранным в основном запросе, поэтому и получилось такое странное условие для соединения таблицы с самой собой:
WHERE Персонал.НомерОтдела=Персонал.НомерОтдела
Однако синтаксических ошибок в таком запросе нет, PostgreSQL запрос выполнит, и мы получим результат (рис. 11) – данные о сотруднике с максимальным окладом по всем отделам! Его можно было предсказать заранее, так как последнее условие выполняется для сотрудников любого отдела.
Чтобы максимальный оклад вычислялся по сотрудникам конкретного отдела, нужно дать разные временные имена (псевдонимы) таблице Персонал в основном и во вложенном запросе, например имя "Т1" в основном запросе и "Т2" во вложенном.
41
Рис. 11. Результат выполнения первого запроса
Перепишем запрос с учётом сказанного. Получим
SELECT Т1.НомерОтдела, ТабНомер, ФИО, Оклад FROM Персонал As Т1, Отделы
WHERE Т1.НомерОтдела=Отделы.НомерОтдела AND Оклад=(SELECT max(Оклад)
FROM Персонал As Т2
WHERE Т1.НомерОтдела=Т2.НомерОтдела);
Результат выполнения исправленного запроса приведен на рис. 12.
Рис. 12. Результат выполнения исправленного запроса
–SUM(имя_поля) – нахождение суммы значений данного поля,
–AVG(имя_поля) – нахождение среднего значения данного поля.
После группировки записей можно отобрать только часть групп, добавив условие на группы с помощью предложения HAVING. Например, можно оставить группы с количеством записей, превышающим или не превышающим заданное значение, с помощью условия COUNT(*)>N или COUNT(*)<=N.
2.8. Сортировка результатов запроса
42
Формально порядок следования результатов, выводимых запросом, не определён. Чтобы вывести результаты в определённой последовательности, следует использовать предложение ORDER BY с указанием списка полей, по которым выполняется сортировка, и опций сортировки. Результаты сначала сортируются по первому полю списка, при равенстве значений первого поля сортировка выполняется по второму полю, затем по третьему полю и т.д. По умолчанию используется опция ASC, задающая сортировку по возрастанию. Опция DESC задаёт для соответствующего поля сортировку по убыванию.
Примеры.
1. Отсортируем список всех сотрудников в алфавитном порядке.
SELECT ФИО, НомерОтдела, Должность FROM Персонал
ORDER BY ФИО;
2. Отсортируем сотрудников каждого отдела в алфавитном порядке, причём отделы должны идти в порядке возрастания значений поля НомерОтдела.
SELECT НомерОтдела, ФИО, Должность FROM Персонал
ORDER BY НомерОтдела, ФИО;
3. Отсортируем сотрудников каждого отдела по убыванию величины значений поля Оклад.
SELECT НомерОтдела, ФИО, Должность, Оклад
FROM Персонал
ORDER BY НомерОтдела, Оклад DESC;
Рассмотрим ещё одну возможность, которую можно использовать только вместе с сортировкой. Это предложения LIMIT и OFFSET. Предложение LIMIT ограничивает общее количество выводимых записей заданным значением. Предложение OFFSET задаёт количество записей, пропущенных при выводе. Например, если в запросе указать LIMIT 10 OFFSET 7, то будут выведены 10 записей – с 8-ой по 17-ую.
Ещё раз отметим, что без использования сортировки результат применения LIMIT и OFFSET будет непредсказуемым.!
2.9. Представления и общие табличные выражения
43
Кроме таблиц PostgreSQL позволяет создавать так называемые «представления». Основу представления составляет запрос SELECT на выборку данных:
CREATE VIEW имя AS Запрос
Представление можно рассматривать как запрос, сохранённый под заданным пользователем именем. Представление создает виртуальную таблицу, которая в последующих запросах может использоваться как обычная таблица. Это свойство можно использовать для разбиения сложных запросов на несколько простых.
Пример. Вывести название отдела (отделов) с максимальным количеством сотрудников.
1. Создадим представление, в котором подсчитаем количество сотрудников в каждом отделе
CREATE VIEW ЧислоСотрудников AS
SELECT "Номер Отдела", COUNT(*) As Количество
FROM Персонал
GROUP BY НомерОтдела;
2. Определим отдел (отделы) с максимальным числом сотрудников, причём число сотрудников отдела будем извлекать из представления «Число сотрудников» как из обычной таблицы:
SELECT НазваниеОтдела, Количество FROM Отделы, ЧислоСотрудников
WHERE Отделы.НомерОтдела = ЧислоСотрудников.НомерОтдела
AND Количество = (SELECT MAX(Количество)
FROM ЧислоСотрудников);
Решить эту задачу можно и с использованием оператора WITH. Оператор WITH предоставляет разработчику возможность создания вспомогательных виртуальных таблиц для использования в основном запросе. Этим объясняется часто использующийся для оператора WITH термин Общие Табличные Выражения (Common Table Expressions или CTE). Создаваемые в операторе временные таблицы существуют только во время выполнения данного запроса.
44
Создадим в запросе оператором WITH вспомогательную таблицу ЧислоСотрудников, которую затем используем в основном запросе:
WITH ЧислоСотрудников AS(
SELECT НомерОтдела, COUNT(*) As Количество FROM Персонал
GROUP BY НомерОтдела
)
SELECT НазваниеОтдела, Количество FROM Отделы, ЧислоСотрудников WHERE Отделы.НомерОтдела = ЧислоСотрудников.НомерОтдела
AND Количество = (SELECT MAX(Количество) FROM ЧислоСотрудников);
Имена полей вспомогательной таблицы в данном примере были взяты из текста создающего её запроса. Однако в операторе WITH можно указать другие имена полей, тогда именно они и будут использоваться в основном запросе. Перепишем тот же пример в виде
WITH ЧислоСотрудников(number, count) AS( SELECT НомерОтдела, COUNT(*)
FROM Персонал
GROUP BY НомерОтдела
)
SELECT НазваниеОтдела, count FROM Отделы, ЧислоСотрудников
WHERE Отделы.НомерОтдела = ЧислоСотрудников.number AND count = (SELECT MAX(count)
FROM ЧислоСотрудников)
2.10.Рекурсивные запросы
Уоператора WITH есть еще одно назначение. С его помощью можно написать запрос, который будет повторяться многократно, пока истинно некоторое условие. Такой запрос называется рекурсивным, хотя по существу он ближе к циклическому алгоритму.
45
Рекурсивный запрос в PostgreSQL имеет следующий вид:
WITH RECURSIVE имя_подзапроса [(список столбцов)] AS(
SELECT … |
-- первая итерация |
|
UNION ALL |
-- присоединяем все следующие итерации |
|
SELECT … FROM имя_подзапроса … |
|
|
WHERE условие продолжения итераций |
||
) |
|
|
SELECT … FROM имя_подзапроса … |
-- основной запрос; |
|
Вместо UNION ALL в рекурсивном подзапросе можно использовать UNION, тогда дубликаты записей будут удаляться из результата запроса.
Наиболее естественным применением рекурсивных запросов является обработка таблиц, содержащих информацию о древовидной иерархической структуре. Покажем это на простом примере. Создадим таблицу и заполним её данными, соответствующими дереву следующей структуры (рис. 13):
CREATE TABLE Tree
(
id integer PRIMARY KEY, parent_id integer
);
INSERT INTO Tree
VALUES (1, NULL), (2, 1), (3, 1), (4, 1), (5, 2), (6, 2), (7, 3), (8, 3), (9, 7), (10, 7);
Рис. 13. Структура дерева и создающий её запрос
Напишем рекурсивный запрос, который для каждого узла дерева определяет уровень этого узла в дереве. Будем считать, что узел с id = 1 находится на нулевом уровне, узлы с id = 2, 3 и 4 – на первом уровне, втором уровне и т.д. Рекурсивный запрос должен создать временную таблицу tmp(id, lvl), где поле id есть номер узла дерева, а lvl – уровень этого узла в дереве. Начало запроса:
WITH RECURSIVE tmp(id, lvl) AS(
На первом шаге рекурсивного подзапроса находим корень всего дерева – узел, у которого нет узла-родителя:
46
SELECT id, 0 FROM Tree WHERE parent_id IS NULL
В результате выполнения этого запроса в таблице tmp появится запись (id = 1, lvl = 0). На следующем шаге будет выполнен запрос
SELECT Tree.id, lvl+1 FROM Tree, tmp WHERE Tree.parent_id = tmp.id)
создающий вспомогательную таблицу с записями об узлах, для которых parent_id = 1, т.е., об узлах первого уровня. Эта таблица будет содержать узлы с id = 2, id = 3 и id = 4.
Полученная таблица используется для выполнения следующего шага, на котором формируется вторая таблица с записями об узлах второго уровня, после чего вспомогательная таблица присоединяется к таблице tmp. На следующем шаге вспомогательной становится вторая таблица, и по ней генерируется третья таблица, а вторая присоединяется к результирующей таблице. Таким образом, в результате выполнения рекурсивной части подзапроса будет полностью сформирована таблица tmp, которая будет содержать все узлы дерева с указанием уровня каждого узла.
Приведём полный текст запроса:
WITH RECURSIVE tmp(id, lvl) AS(
SELECT id, 0 FROM Tree WHERE parent_id IS NULL UNION ALL
SELECT Tree.id, lvl+1 FROM Tree, tmp WHERE Tree.parent_id = tmp.id
)
SELECT * FROM tmp;
Результат выполнения запроса представлен на рис. 14:
47
Рис. 14. Результат выполнения рекурсивного запроса
2.11. Возврат данных из запросов на модификацию
Кроме SELECT в предложении WITH и в основном запросе можно использовать запросы INSERT, UPDATE или DELETE, при этом запросы на модификацию данных будут возвращать записи, если добавить к тексту запроса директиву RETURNING. Объясним, зачем это нужно, на следующем примере. Рассмотрим ситуацию, связанную с увольнением сотрудника. Соответствующую запись следует удалить из таблицы Персонал, но сохранить её в другой таблице. Чтобы решить эту задачу, создадим сначала таблицу Архив, структура которой полностью совпадает со структурой таблицы Персонал. Сделать это можно либо с помощью директивы LIKE, либо используя механизм наследования таблиц, который позволяет создавать производные таблицы, наследующие структуру базовой таблицы (этот механизм мы рассмотрим позже).
CREATE TABLE Архив
(
LIKE Персонал
);
48
Затем удалим сотрудника с табельным номером 300 с одновременным внесением информации в таблицу Архив:
WITH r AS(
DELETE FROM Персонал
WHERE ТабНомер = 300 RETURNING *
)
INSERT INTO Архив SELECT * FROM r;
Конечно, такой пример является упрощённым. На практике может потребоваться при каждом изменении данных о сотруднике (увольнении, приеме на работу, переводе на другую должность и т.д.) сохранять в таблице Архив (или другой таблице) дополнительную информацию, например, дату и номер приказа, прежнюю должность, поэтому структура реальной БД будет намного сложнее.
3. Встроенные и пользовательские функции. Язык PL/pgSQL
В PostgreSQL встроено большое число функций: математических, статистических, функций работы со строками и др., причём часто их даже больше, чем в универсальных языках программирования. Так, среди математических функций есть тангенс tan(x), котангенс cot(x), кубический корень cbrt(x), перевод радиан в градусы degrees(x) и градусов в радианы radians(x), три вида логарифмов – натуральный ln(x), десятичный log(x) и логарифм по заданному основанию log(a, x). Заметим, что последние два логарифма – это пример перегруженной функции (одинаковое имя, но разный набор аргументов).
Среди строковых операций отметим конкатенацию строк – операция | | (две вертикальные черты подряд) и функции преобразования текстовой строки к нижнему lower(текст) и верхнему upper(текст) регистру, а также большое число функций форматирования и перекодировки строк.
49
Также есть функции для работы с типом дата/время, с геометрическими типами данных, и вообще с любым встроенным типом данных. Но PostgreSQL не ограничивает пользователя встроенными функциями и операторами. Если приходится часто выполнять одну и ту же последовательность команд SQL или программных операций, пользователь может написать свою функцию и сохранить её в базе данных. Также в PostgreSQL предусмотрена возможность определения новых операторов с целью сделать SQL-запросы понятнее и эффективнее.
Функции и операторы, созданные пользователем, существуют как объекты базы данных, и поэтому связываются с конкретной базой. Такая функция доступна только для пользователей, подключившихся к этой базе. Написанная функция может использоваться во всех вновь создаваемых базах данных, если включить её в шаблон template1. Пользователь также может перегружать любые уже написанные (и даже встроенные) функции. Если перегружается встроенная функция, то при вызове написанной функции к её имени добавляется имя схемы, в которой создавалась база данных (по умолчанию – public).
Для написания функции пользователь может использовать «чистый» SQL или один из языков, поддержка которых заложена в PostgreSQL (в частности расширение PL/pgSQL).
3.1. Создание пользовательских функций
Пользовательская функция создаётся запросом
CREATE [OR REPLACE] FUNCTION имя(список аргументов) RETURNS тип_возвращаемого_значения AS '
тело функции ' LANGUAGE язык
Вместо почти незаметных апострофов рекомендуется использовать двойной знак доллара $$ или их полный вариант $BODY$. Язык функции можно указывать как после тела функции, так и перед ключевым словом AS.
50
