Объектно-реляционная СУБД PostgreSQL. Учебное пособие
.pdfслагаемого нам потребуется извлекать данные из двух соседних записей. Однако более простой и эффективный вариант – запоминать извлечённые данные в локальных переменных.
Приведём сразу текст функции Интеграл():
CREATE OR REPLACE FUNCTION Интеграл() RETURNS double precision AS $$ DECLARE
S double precision = 0; x0 double precision;
y0 double precision;
x1 double precision;
y1 double precision; dat refcursor;
BEGIN
--открываем набор записей, отсортированный по X OPEN dat FOR SELECT X,Y FROM Значения ORDER BY X;
--извлекаем из набора первую запись
FETCH NEXT FROM dat INTO x0,y0; -- накапливаем интегральную сумму LOOP
--извлекаем из набора следующую запись FETCH NEXT FROM dat INTO x1,y1;
IF NOT FOUND THEN EXIT; END IF; S = S + 0.5*(y1+y0)*(x1-x0);
--запоминаем данные для следующей итерации x0 = x1; y0 = y1;
END LOOP;
--закрываем набор данных CLOSE dat;
--и возвращаем результат RETURN S;
END
$$ LANGUAGE plpgsql; SELECT Интеграл();
81
Результат (с округлением до 6 знаков после запятой) равен 0.841438, и это отличается от результата аналитического вычисления определённого интеграла
1
òcos xdx = sin(1) , что даёт значение 0.841471. Погрешность обуславливается
0
небольшим количеством точек разбиения интервала интегрирования. Если увеличить число точек до 1000, то результат, возвращаемый функцией, будет равен (после округления) 0.841471.
В заключение заметим, что использование курсора увеличивает время работы программы, поэтому применять курсоры рекомендуется только в случае, если без них нельзя обеспечить требуемую функциональность приложения.
4.2. Триггеры
Триггер представляет собой механизм, позволяющий автоматически выполнять определённые действия при наступлении некоторого события. Этим он напоминает обработчик события в технологии визуального программирования.
Обработчиком события является триггерная функция, которая может выполняться ДО (в этом случае сначала вызывается функция триггера, а затем выполняется вызвавшее триггер событие) или ПОСЛЕ (наоборот, сначала выполняется вызывающее триггер событие, а затем выполняется функция триггера). В качестве события триггера могут выступать операции INSERT, UPDATE или DELETE. При объявлении триггера можно указать, будет ли функция триггера вызываться для каждой вставляемой, обновляемой или удаляемой строки, или только один раз для всего SQL выражения.
Когда функция PL/pgSQL выполняется в качестве триггера, автоматически создаются и доступны из функции следующие специальные переменные:
– NEW – переменная, которая хранит новую строку таблицы при выполнении триггерной функции, связанной с операторами INSERT или UPDATE для каждой строки. Эта переменная равна NULL при выполнении
82
триггера для всего запроса или при выполнении триггера для оператора DELETE;
–OLD – переменная, которая хранит старую строку таблицы при выполнении триггерной функции, связанной с операторами UPDATE или DELETE для каждой строки. Эта переменная равна NULL при выполнении триггера для всего запроса или при выполнении триггера для оператора INSERT;
–TG_NAME – переменная, которая хранит имя триггера, выполняемого в данный момент;
–TG_WHEN – строка, содержащая значение BEFORE (ДО), AFTER (ПОСЛЕ) или INSTEAD OF (ВМЕСТО), в зависимости от определения триггера;
–TG_LEVEL – строка, содержащая либо ROW (выполнение для каждой строки), либо STATEMENT (однократное выполнение для всего запроса);
–TG_OP – строка, равная INSERT, UPDATE, DELETE или TRUNCATE, определяющая операцию, с которой связан триггер;
–TG_RELID – идентификатор объекта таблицы, для которой выполняется триггер;
–TG_TABLE_NAME – имя таблицы, для которой выполняется триггер;
–TG_TABLE_SCHEMA – имя схемы таблицы, для которой выполняется триггер;
–TG_NARGS – количество аргументов, переданных функции триггера в операторе CREATE TRIGGER;
–TG_ARGV[] – аргументы (в виде массива строк) для оператора CREATE TRIGGER.
Приведём пример решения с помощью триггеров следующей задачи: реализовать систему регистрации событий добавления и удаления пользователей некоторого приложения (например, БД). Для хранения информации о пользователях создадим таблицу Пользователи:
CREATE TABLE Пользователи( name text
83
)
Вторая таблица будет хранить записи («логи») о событиях:
CREATE |
TABLE |
Журнал ( |
info |
text, |
-- информационная часть |
added timestamp without time zone -- дата и время -- создания записи
)
При добавлении в таблицу Пользователи нового пользователя, в таблицу Журнал функция триггера добавит запись «Пользователь <name> добавлен», при редактировании – «Пользователь <name> изменён», при удалении – «Пользователь <name> удалён». Функцию триггера напишем на PL\pgSQL:
CREATE OR REPLACE FUNCTION Добавить()
RETURNS TRIGGER AS $$
DECLARE
S text;
BEGIN
IF TG_OP = 'INSERT' THEN
S = |
'Пользователь ' || NEW.name || ' добавлен'; |
|
INSERT INTO Журнал VALUES (S, now()); |
||
RETURN NEW; |
|
|
ELSIF |
TG_OP = 'UPDATE' THEN |
|
S = |
'Пользователь ' || OLD.name || |
|
|
' изменён на' |
|| NEW.name || ' !'; |
INSERT INTO Журнал VALUES (S, now()); |
||
RETURN NEW; |
|
|
ELSIF |
TG_OP = 'DELETE' THEN |
|
S = |
'Пользователь ' || OLD.name || ' удалён'; |
|
INSERT INTO Журнал VALUES (S, now()); RETURN OLD;
END IF; END;
$$ LANGUAGE plpgsql;
84
В теле функции анализируем переменную TG_OP (это внутренняя переменная триггера, которая определяет, с какой операцией связан триггер). В зависимости от операции формируем строку S, которая будет записана в таблицу Журнал.
Напомним, что переменные NEW и OLD – это строки, которые обрабатывает триггер. В случае INSERT переменная NEW будет содержать новую строку, а OLD будет пустая, в случае UPDATE обе переменные будут заполнены соответствующими данными, а в случае DELETE переменная NEW будет пустая, а OLD будет содержать удаляемую строку.
Теперь создадим триггер:
CREATE TRIGGER TUser
AFTER INSERT OR UPDATE OR DELETE ON Пользователи FOR EACH ROW EXECUTE PROCEDURE Добавить();
Ключевое слово AFTER указывает, что функция триггера Добавить() будет вызываться после добавления (INSERT), изменения (UPDATE) или удаления (DELETE) каждой строки (FOR EACH ROW) в таблице Пользователи.
Теперь при выполнении запросов на модификацию таблицы Пользователи в таблице Журнал будет сохраняться информация о совершённых действиях.
Например, добавим несколько пользователей в таблицу, отредактируем данные одного из них, а затем удалим всех пользователей:
-- добавляем
INSERT INTO Пользователи VALUES('Степаныч'); INSERT INTO Пользователи VALUES('Сергеич'); INSERT INTO Пользователи VALUES('Иваныч'); -- редактируем
UPDATE Пользователи SET name = 'Иванов' WHERE name = 'Иваныч';
-- удаляем
DELETE FROM Пользователи;
Откроем теперь таблицу Журнал, в которой увидим следующие записи: "Пользователь Степаныч добавлен";"2016-09-02 13:56:02.14" "Пользователь Сергеич добавлен"; "2016-09-02 13:56:02.14"
85
"Пользователь Иваныч добавлен"; "2016-09-02 13:56:02.14" "Пользователь Иваныч изменён на Иванов !"; "2016-09-02 13:56:02.14"
"Пользователь Степаныч удалён"; |
"2016-09-02 13:56:02.14" |
"Пользователь Сергеич удалён"; |
"2016-09-02 13:56:02.14" |
"Пользователь Иванов удалён"; |
"2016-09-02 13:56:02.14" |
4.3. Индексы
Индексы служат для ускорения поиска нужных записей в таблицах базы данных. СУБД PostgreSQL обязательно создаёт индексы для первичных и внешних ключей, но пользователь может создать индекс для произвольных столбцов (или наборов столбцов) с помощью оператора CREATE INDEX.
Фактически в индексе в том или ином виде хранится информация о расположении записей в таблице. Наличие индекса ускоряет поиск в таблице примерно так же, как наличие алфавитного указателя в учебнике ускоряет поиск нужного материала. Алфавитный указатель содержит упорядоченный список ключевых слов и номера страниц, на которых эти слова встречаются. Читатель может сразу открыть нужную страницу вместо того, чтобы просматривать всю книгу.
Синтаксис создания индекса:
CREATE INDEX имя_индекса
ON имя_таблицы [UNING тип_индекса] (список_cтолбцов);
(по умолчанию тип индекса – btree – сбалансированное дерево)
Созданный индекс используется при выполнении запросов на выборку информации из базы данных. Он корректируется СУБД при операциях добавления записей в таблицу и удаления записей из таблицы, что требует дополнительных затрат времени. Если данные таблицы обновляются часто, а выборка информации осуществляется редко, то дополнительные индексы могут даже уменьшить эффективность работы с БД. Поэтому индексы, которые редко или никогда не используются в запросах, должны быть удалены.
86
4.4. Основные типы индексов
PostgreSQL предлагает несколько типов индексов: B-tree, hash, GiST и GIN. Для каждого типа индекса используется свой алгоритм, который лучше всего подходит для различных типов запросов. По умолчанию команда CREATE INDEX создает B-tree индексы, которые применяются в наиболее распространенных ситуациях (например, при использовании операций сравнения, попадания в список или интервал, и ряда других).
В-деревом порядка n называется структура, обладающая следующими свойствами:
1)все пути от корня до любых листьев имеют одинаковую длину h, называемую также высотой В-дерева;
2)в каждой вершине дерева, за исключением корня, должно располагаться в порядке возрастания минимум n, максимум 2n ключей;
3)в корне В-дерева может располагаться в порядке возрастания минимум один, максимум 2n ключей;
4)любая вершина дерева, за исключением листьев, имеющая j ключей, должна иметь j+1 подчиненную вершину.
Ниже приведен пример дерева порядка 2, при этом количество значений в каждом узле от 2-х до 4-х включительно (символами обозначены свободные места в узлах В-дерева).
[7, |
16, , ] |
– корень дерева; |
[1, |
4, 5, 6] [9, 12, 13, ] [18, 21, , ] |
– листья дерева. |
Поскольку в этом примере корень дерева содержит только два значения, то на следующем уровне дерево имеет три подчинённые вершины (в данном примере это листья дерева). Первый лист содержит значения, меньшие первого ключа родительского узла (меньшие 7), третий лист – значения, большие, чем второй ключ (большие 16), а второй лист – промежуточные значения (от 8 до 15).
Добавление новых узлов в дерево происходит только в листья, т.е. в конечные узлы дерева. Сначала, используя упорядоченность структуры дерева, определяется лист для вставки. Если в структуре листа есть свободное место, то элемент вставляется в этот лист с сохранением упорядоченности значений в листе.
87
Добавим, например, в показанное выше дерево значение 8. При поиске места вставки просмотр начинается с корневого узла, при этом определяется, в какое поддерево необходимо вставить значение 8 (в данном случае во второй лист). Поскольку в листе есть свободное место, то 8 добавится в этот узел (получим лист со значениями [8, 9, 12, 13]) и всё. А теперь попробуем добавить значение 2. Оно должно добавляться в первое поддерево, но соответствующий лист уже заполнен. Тогда среднее значение (4) из набора [1, 2, 4, 5, 6], полученного после «мысленной» вставки значения 2, переносится в родительский узел – на уровень выше (если там есть место), а текущий лист расщепляется на два: один – со значениями меньшими, чем 4, и второй – со значениями большими, чем 4. В результате получим новое дерево:
[4, |
7, |
16] |
– корень дерева; |
[1, |
2, |
, ] [5, 6, , ] [8, 9, 12, 13] [18, 21, , ] |
– листья дерева. |
При переполнении родительского узла его среднее значение «поднимается» в узел более высокого уровня, а текущий узел расщепляется на два. Если более высокий уровень отсутствует, то создаётся новый корневой узел, а высота дерева увеличивается на единицу (при этом новая длина также одинакова для всех листьев, т.е. сохраняется сбалансированность дерева).
Алгоритм удаления элемента несколько сложнее. Покажем его на нескольких примерах.
Пример 1. Удалим значение 12. Находим лист с этим значением, удаляем, в третьем листе остаются значения [8, 9, 13, ], больше ничего не делаем.
Пример 2. Удалим теперь значение 7. Значение принадлежит корню поддерева, поэтому его нельзя просто удалить – возникнет несоответствие структуры поддерева и его родительского узла. Поэтому вместо значения 7 «поднимем» в узел или максимальное значение левого поддерева (это 6), или минимальное значение правого поддерева (это 8). При этом в узле, из которого мы забрали значение, может остаться слишком мало элементов (такая ситуация называется антипереполнением). Например, если перенести выше значение 6,
88
то во втором листе останется только одно значение – 5. Если же перенести значение 8 из третьего листа, то получим корректный результат.
Чтобы автоматически находить верное решение, используем следующий приём. Выпишем значения элементов левого и правого узла, разделителем которых являлось значение 7. Получим список [5, 6, 8, 9, 13]. Теперь возьмём средний элемент (8) и заменим им значение 6 в родительском узле, а оставшиеся элементы распределим в левый и правый узлы. Получим:
[4, |
8, |
16] |
– корень дерева; |
[1, |
2, |
, ] [5, 6, , ] [9, 13, , ] [18, 21, , ] |
– листья дерева. |
Пример 3. Удалим из корневого узла значение 4. Его можно заменить значением 2 из левого поддерева или значением 5 из правого поддерева, однако в любом случае нарушается требование наличия минимального количества элементов в узле. Но и перераспределить элементы между узлами мы не сумеем
– их слишком мало, поэтому мы просто объединим оба узла:
[8, |
16] |
– корень дерева; |
[1, |
2, 5, 6] [9, 12, 13, ] [18, 21, , ] |
– листья дерева. |
Заметим, что при удалении значения из промежуточного узла также может возникнуть ситуация антипереполнения, т.е. описанную процедуру придётся повторить ещё один или несколько раз. Однако это не касается корня всего дерева – там минимальное количество элементов равно единице. Удаление последнего элемента корня приводит к уменьшению высоты дерева.
Следующий тип – hash-индексы. Hash-индексы предполагают хранение не самих значений, а результата применения специальной функции (так называемой хеш-функции) к этим значениям. При запросах с использованием hash-индексов сравниваться будут не искомое число со значениями поля, а их хеш-значения. Hash-индекс рекомендуется использовать для полей большого размера, например для текстовых полей, однако его использование имеет ряд ограничений. Так, из-за нелинейности хэш-функций данный индекс нельзя сортировать по значению, что приводит к невозможности использования в сравнениях больше/меньше и «is null». Кроме того, так как значения
89
хеш-функций не уникальны, то для совпадающих значений приходится применять дополнительно какой-либо метод разрешения коллизий. Заметим также, что hash-индекс может создаваться только по одному столбцу, в то время как B-tree-индекс – по нескольким. По этим причинам и некоторым проблемам администрирования в документации СУБД PostgreSQL не рекомендуется использовать этот тип индекса.
B-tree индекс (или его модификация) и hash-индексы используются для работы с одномерными данными, такими как строки и числа. Для них разработаны очень эффективные алгоритмы работы. Однако современные приложения, такие как ГИС (геоинформационные системы), мультимедийные системы, полнотекстовый поиск и другие, которые, по сути, используют многомерные данные, требуют более эффективных методов доступа.
Для работы с такими данными СУБД PostgreSQL предлагает два типа индекса: GiST (Generalized Search Tree) и GIN (Generalized Inverted Index).
GiST (обобщённое дерево поиска) был предложен как обобщение нескольких классов индексов (такие как B-tree, R-tree и др., ранние версии PosgreSQL использовали R-tree). Он позволяет создавать индексы на базе произвольных типов данных и их массивов. Для использования GIST разработчик должен написать специальные функции-адаптеры к своему типу данных. Индексы GIST имеют хорошую производительность при вставке нового ключа, но производительность при поиске может сильно зависеть от особенностей проиндексированного типа данных и типа поискового запроса.
GIN-индекс (обобщённый обратный индекс) используется в PostgreSQL для реализации полнотекстового поиска. Это означает, что в структуре индексов с каждой лексемой сопоставляется отсортированный список номеров документов, в которых она встречается. Таким образом, поиск документа с заданной лексемой существенно ускоряется, однако процесс добавления новых документов в базу данных, наоборот, требует существенно большего времени.
4.5. Специальные типы индексов
90
